Модульный синтаксический анализ JavaScript

Node.js внешний интерфейс JavaScript RequireJS
Модульный синтаксический анализ JavaScript

автор:zhijsиз Thunder Front End

Оригинальный адрес:Модульный синтаксический анализ JavaScript

С постепенным развитием языка JavaScript приложения JavaScript эволюционировали от простой проверки формы до сложного взаимодействия с веб-сайтом и языковой поддержки для серверных, мобильных и ПК-клиентов. Область применения JavaScript становится все более и более обширной, инженерный код становится все более и более объемным, а управление кодом становится все более и более сложным, поэтому в сообществе появилась схема модульности JavaScript. схемы модуляризации, Постепенно он стал спецификацией языка JavaScript. Давайте обсудим тему модульности JavaScript. Эта статья в основном состоит из нескольких частей.

  • Что такое модуль
  • Почему модульный
  • CommonJS для модульности JavaScript
  • AMD модульности JavaScript
  • Модульная командная строка JavaScript
  • Модульный модуль JavaScript ES
  • Суммировать

что такое модуль

Модуль, также известный как компонент, представляет собой набор операторов программы (то есть набор программных кодов и структур данных), которые могут быть индивидуально названы и независимо выполнять определенные функции. Он имеет две основные функции: внешние функции и внутренние функции. Внешние функции относятся к интерфейсу между модулем и внешней средой (то есть к тому, как другие модули или программы вызывают модуль, включая входные и выходные параметры, глобальные переменные, на которые ссылаются) и функции модуля, а внутренние функции относятся к особенности внутренней среды модуля (т. е. локальные данные и программный код для этого модуля). Короче говоря, модуль — это набор кода с независимой областью действия, который предоставляет внешнему миру определенный функциональный интерфейс.

Зачем нужна модульность

Сначала давайте вернемся в прошлое и посмотрим, как был написан исходный файл модуля JavaScript.

// add.js
function add(a, b) {
  return a + b;
}
// decrease.js
function decrease(a, b) {
  return a - b;
}

// formula.js
function square_difference(a, b) {
  return add(a, b) * decrease(a, b);
}

Выше мы реализовали несколько функциональных функций в трех файлах JavaScript. Среди них третья функция function должна зависеть от функциональных функций первого и второго файлов JavaScript, поэтому, когда мы ее используем, мы обычно пишем ее так:

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    <script src="add.js"></script>
    <script src="decrease.js"></script>
    <script src="formula.js"></script>
    <!--使用-->
    <script>
       var result = square_difference(3, 4);
    </script>
</body>
</html>

Такой вид управления вызовет следующие проблемы:

  • Модули могут быть импортированы в неправильном порядке
  • загрязняет глобальные переменные
  • Зависимости между модулями не очевидны

Основываясь на вышеуказанных причинах, существует решение вышеуказанных проблем, то есть спецификация модульности JavaScript.В настоящее время существует четыре основных спецификации: CommonJS, AMD, CMD и модуль ES6.

CommonJS для модульности Javascript

Основное содержание спецификации CommonJS заключается в том, что один файл является модулем. Каждый модуль представляет собой отдельную область. Модуль должен экспортировать внешние переменные или интерфейсы через module.exports и импортировать вывод других модулей в текущую область модуля через require(). Давайте поговорим о модуляризации CommonJS в механизме NodeJs.

Как использовать

// 模块定义 add.js
module.eports.add = function(a, b) {
  return a + b;
};

// 模块定义 decrease.js
module.exports.decrease = function(a, b) {
  return a - b;
};

// formula.js,模块使用,利用 require() 方法加载模块,require 导出的即是 module.exports 的内容
const add = require("./add.js").add;
const decrease = require("./decrease.js").decrease;
module.exports.square_difference = function(a, b) {
  return add(a, b) * decrease(a, b);
};

экспорт и модуль.экспорт

exports и module.exports — это переменные, указывающие на одно и то же: module.exports = exports = {}, так что вы также можете экспортировать такие модули

//add.js
exports.add = function(a, b) {
  return a + b;
};

Но нельзя напрямую изменять точку экспорта, например:

// add.js
exports = function(a, b) {
  return a + b;
};
// main.js
var add = require("./add.js");

Добавление, полученное в это время, является пустым объектом, потому что require импортирует содержимое module.exports соответствующего модуля.В приведенном выше коде, хотя в начале exports = module.exports, когда выполняется следующий код, на самом деле Экспорт точки в функцию, а содержимое module.exports не изменилось, поэтому экспорт этого модуля является пустым объектом.

exports = function(a, b) {
  return a + b;
};

Механизм загрузки модулей CommonJS в NodeJs

Согласно следующемуМодуль CommonJS загружает исходный код в NodeJsПроанализировать механизм загрузки модулей в NodeJS.

Чтобы внедрить модуль (require) в NodeJs, вам необходимо пройти следующие 3 шага:

  1. анализ пути
  2. местоположение файла
  3. Скомпилировать и выполнить

Так же, как клиентские браузеры кэшируют статические файлы скриптов для повышения производительности, NodeJs кэширует импортированные модули, чтобы уменьшить нагрузку на вторичный импорт. Разница в том, что браузеры кешируют только файлы, а в NodeJ кешируются скомпилированные и исполняемые объекты.

Анализ пути + расположение файла

Его процесс показан на следующем рисунке:

компиляция модуля

После обнаружения файла он сначала проверит, есть ли у файла кеш, и если да, прочитает кеш напрямую, в противном случае он создаст новый объект модуля, который определяется следующим образом:

function Module(id, parent) {
  this.id = id; // 模块的识别符,通常是带有绝对路径的模块文件名。
  this.exports = {}; // 表示模块对外输出的值
  this.parent = parent; // 返回一个对象,表示调用该模块的模块。
  if (parent && parent.children) {
    this.parent.children.push(this);
  }
  this.filename = null;
  this.loaded = false; // 返回一个布尔值,表示模块是否已经完成加载。
  this.childrent = []; // 返回一个数组,表示该模块要用到的其他模块。
}

Код действия запроса выглядит следующим образом:

Module.prototype.require = function(id) {
  // 检查模块标识符
  if (typeof id !== "string") {
    throw new ERR_INVALID_ARG_TYPE("id", "string", id);
  }
  if (id === "") {
    throw new ERR_INVALID_ARG_VALUE("id", id, "must be a non-empty string");
  }
  // 调用模块加载方法
  return Module._load(id, this, /* isMain */ false);
};

Следующий шаг — разобрать путь к модулю, определить, есть ли кеш, а затем сгенерировать объект Module:

Module._load = function(request, parent, isMain) {
  if (parent) {
    debug("Module._load REQUEST %s parent: %s", request, parent.id);
  }

  // 解析文件名
  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];

  // 判断是否有缓存,有的话返回缓存对象的 exports
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 判断是否为原生核心模块,是的话从内存加载
  if (NativeModule.nonInternalExists(filename)) {
    debug("load native module %s", request);
    return NativeModule.require(filename);
  }

  // 生成模块对象
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = ".";
  }

  // 缓存模块对象
  Module._cache[filename] = module;

  // 加载模块
  tryModuleLoad(module, filename);

  return module.exports;
};

Код для tryModuleLoad выглядит так:

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    // 调用模块实例load方法
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      // 如果加载出错,则删除缓存
      delete Module._cache[filename];
    }
  }
}

Объект модуля выполняет операцию загрузки module.load Код выглядит следующим образом:

Module.prototype.load = function(filename) {
  debug("load %j for module %j", filename, this.id);

  assert(!this.loaded);
  this.filename = filename;

  // 解析路径
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  // 判断扩展名,并且默认为 .js 扩展
  var extension = path.extname(filename) || ".js";

  // 判断是否有对应格式文件的处理函数, 没有的话,扩展名改为 .js
  if (!Module._extensions[extension]) extension = ".js";

  // 调用相应的文件处理方法,并传入模块对象
  Module._extensions[extension](this, filename);
  this.loaded = true;

  // 处理 ES Module
  if (experimentalModules) {
    if (asyncESM === undefined) lazyLoadESM();
    const ESMLoader = asyncESM.ESMLoader;
    const url = pathToFileURL(filename);
    const urlString = `${url}`;
    const exports = this.exports;
    if (ESMLoader.moduleMap.has(urlString) !== true) {
      ESMLoader.moduleMap.set(
        urlString,
        new ModuleJob(ESMLoader, url, async () => {
          const ctx = createDynamicModule(["default"], url);
          ctx.reflect.exports.default.set(exports);
          return ctx;
        })
      );
    } else {
      const job = ESMLoader.moduleMap.get(urlString);
      if (job.reflect) job.reflect.exports.default.set(exports);
    }
  }
};

Прочитайте модуль синхронно здесь, а затем выполните операцию компиляции:

Module._extensions[".js"] = function(module, filename) {
  // 同步读取文件
  var content = fs.readFileSync(filename, "utf8");

  // 编译代码
  module._compile(stripBOM(content), filename);
};

Процесс компиляции в основном выполняет следующие операции:

  1. Оберните код JavaScript в тело функции, чтобы изолировать область действия, например:
exports.add = (function(a, b) {
  return a + b;
}

будет преобразован в

(
  function(exports, require, modules, __filename, __dirname) {
    exports.add = function(a, b) {
      return a + b;
    };
  }
);
  1. Выполните функцию, внедрите свойство экспорта объекта модуля, запросите глобальный метод и экземпляр объекта, __filename, __dirname, а затем выполните исходный код модуля.

  2. Возвращает свойство экспорта объекта модуля.

AMD модульности JavaScript

AMD, определение асинхронного модуля, представляет собой механизм асинхронной загрузки модуля, который загружает модуль асинхронным способом, и загрузка модуля не влияет на работу следующего оператора. Все операторы, которые зависят от этого модуля, определены в функции обратного вызова, которая не будет выполняться до тех пор, пока не будут загружены зависимости.

AMD была создана для решения этих двух проблем:

  1. Асинхронная загрузка файлов JavaScript, чтобы избежать неотвечающих веб-страниц
  2. Управление зависимостями между модулями для облегчения написания кода и обслуживания
 // 模块定义
 define(id?: String, dependencies?: String[], factory: Function|Object);

id — это имя модуля и необязательный параметр.

dependencies указывает список модулей, от которых зависит.Это массив и необязательный параметр. Вывод каждого зависимого модуля будет передан на фабрику в качестве аргумента один раз. Если зависимости не указаны, по умолчанию используется ["require", "exports", "module"].

factory — последний параметр, он оборачивает конкретную реализацию модуля, это функция или объект. Если это функция, ее возвращаемое значение является выходным интерфейсом или значением модуля, а если это объект, то объект должен быть выходным значением модуля.

Например:

// 模块定义,add.js
define(function() {
  let add = function(a, b) {
    return a + b;
  };
  return add;
});

// 模块定义,decrease.js
define(function() {
  let decrease = function(a, b) {
    return a - b;
  };
  return decrease;
});

// 模块定义,square.js
define(["./add", "./decrease"], function(add, decrease) {
  let square = function(a, b) {
    return add(a, b) * decrease(a, b);
  };
  return square;
});

// 模块使用,主入口文件 main.js
require(["square"], function(math) {
  console.log(square(6, 3));
});

Здесь для анализа используется RequireJS, реализующий спецификацию AMD, исходный код RequireJS более сложен, здесь анализируется только принцип асинхронной загрузки модулей. Во время загрузки модуля RequireJS вызывает следующие функции:

/**
 *
 * @param {Object} context the require context to find state.
 * @param {String} moduleName the name of the module.
 * @param {Object} url the URL to the module.
 */
req.load = function(context, moduleName, url) {
  var config = (context && context.config) || {},
    node;
  // 判断是否为浏览器
  if (isBrowser) {
    // 根据模块名称和 url 创建一个 Script 标签
    node = req.createNode(config, moduleName, url);

    node.setAttribute("data-requirecontext", context.contextName);
    node.setAttribute("data-requiremodule", moduleName);

    // 对不同的浏览器 Script 标签事件监听做兼容处理
    if (
      node.attachEvent &&
      !(
        node.attachEvent.toString &&
        node.attachEvent.toString().indexOf("[native code") < 0
      ) &&
      !isOpera
    ) {
      useInteractive = true;

      node.attachEvent("onreadystatechange", context.onScriptLoad);
    } else {
      node.addEventListener("load", context.onScriptLoad, false);
      node.addEventListener("error", context.onScriptError, false);
    }

    // 设置 Script 标签的 src 属性为模块路径
    node.src = url;

    if (config.onNodeCreated) {
      config.onNodeCreated(node, config, moduleName, url);
    }

    currentlyAddingScript = node;

    // 将 Script 标签插入到页面中
    if (baseElement) {
      head.insertBefore(node, baseElement);
    } else {
      head.appendChild(node);
    }
    currentlyAddingScript = null;

    return node;
  } else if (isWebWorker) {
    try {
      //In a web worker, use importScripts. This is not a very
      //efficient use of importScripts, importScripts will block until
      //its script is downloaded and evaluated. However, if web workers
      //are in play, the expectation is that a build has been done so
      //that only one script needs to be loaded anyway. This may need
      //to be reevaluated if other use cases become common.

      // Post a task to the event loop to work around a bug in WebKit
      // where the worker gets garbage-collected after calling
      // importScripts(): https://webkit.org/b/153317
      setTimeout(function() {}, 0);
      importScripts(url);

      //Account for anonymous modules
      context.completeLoad(moduleName);
    } catch (e) {
      context.onError(
        makeError(
          "importscripts",
          "importScripts failed for " + moduleName + " at " + url,
          e,
          [moduleName]
        )
      );
    }
  }
};

// 创建异步 Script 标签
req.createNode = function(config, moduleName, url) {
  var node = config.xhtml
    ? document.createElementNS("http://www.w3.org/1999/xhtml", "html:script")
    : document.createElement("script");
  node.type = config.scriptType || "text/javascript";
  node.charset = "utf-8";
  node.async = true;
  return node;
};

Видно, что асинхронный тег Script создается в основном на основе URL-адреса модуля, а имя идентификатора модуля добавляется в data-requiremodule тега, а затем тег Script добавляется на html-страницу. При этом добавлен обработчик события load тега Script, при загрузке файла модуля будет срабатывать context.onScriptLoad. Мы добавляем точку останова в onScriptLoad, вы можете увидеть структуру страницы, как показано ниже:

Как видно из рисунка, в Html добавлен тег Script, что является принципом асинхронной загрузки модулей.

Модульная командная строка JavaScript

CMD (Common Module Definition) — это общее определение модуля. Реализация CMD на стороне браузера — это SeaJS. Как и RequireJS, принцип загрузки SeaJS заключается в динамическом создании асинхронных тегов Script. Разница между ними в основном заключается в способе написания зависимостей: AMD рекомендует загружать все зависимости с самого начала, а CMD рекомендует загружать зависимости только там, где они необходимы.

// ADM 在执行以下代码的时候,RequireJS 会首先分析依赖数组,然后依次加载,直到所有加载完毕再执行回到函数
define(["add", "decrease"], function(add, decrease) {
  let result1 = add(9, 7);
  let result2 = decrease(9, 7);
  console.log(result1 * result2);
});

// CMD 在执行以下代码的时候, SeaJS 会首先用正则匹配出代码里面所有的 require 语句,拿到依赖,然后依次加载,加载完成再执行回调函数
define(function(require) {
  let add = require("add");
  let result1 = add(9, 7);
  let add = require("decrease");
  let result2 = decrease(9, 7);
  console.log(result1 * result2);
});

Модульный модуль JavaScript ES

Модуль ES — это модульная функция, представленная в ECMAScript 6. Функция модуля в основном состоит из двух команд: экспорта и импорта. Команда экспорта используется для указания внешнего интерфейса модуля, а команда импорта используется для импорта функций, предоставляемых другими модулями.

Он используется следующим образом:

// 模块定义 add.js
export function add(a, b) {
  return a + b;
}

// 模块使用 main.js
import { add } from "./add.js";
console.log(add(1, 2)); // 3

Ниже описаны еще несколько важных моментов.

экспортировать и экспортировать по умолчанию

В файле или модуле может быть несколько экспортов, и существует только один экспорт по умолчанию, экспорт подобен именованному экспорту, а умолчание аналогично экспорту переменной с именем по умолчанию. При этом при импорте для переменных экспорта необходимо использовать именованные объекты для их выполнения, а по умолчанию можно указывать имена переменных произвольно, например:

// a.js
 export var a = 2;
 export var b = 3 ;
// main.js 在导出的时候必须要用具名变量 a, b 且以解构的方式得到导出变量
import {a, b} from 'a.js' // √ a= 2, b = 3
import a from 'a.js' // x

// b.js export default 方式
const a = 3
export default a // 注意不能 export default const a = 3 ,因为这里 default 就相当于一个变量名

// 导出
import b form 'b.js' // √
import c form 'b.js' // √ 因为 b 模块导出的是 default,对于导出的default,可以用任意变量去承接

Процесс загрузки и экспорта модуля модуля ES

В качестве примера возьмем следующий код:

 // counter.js
 export let count = 5

 // display.js
 export function render() {
   console.log('render')
 }
 // main.js
 import { counter } from './counter.js';
 import { render } from './display.js'
 ......// more code

В процессе загрузки модуля он в основном проходит следующие этапы:

Строительство

Этот процесс выполняет поиск, загрузку и преобразование файла в запись модуля. Так называемая запись модуля относится к синтаксическому дереву, в котором записан соответствующий модуль, информация о зависимостях и различные атрибуты и методы (здесь не очень понятно). Также в этом процессе кэшируются записи модуля.На следующем рисунке показана таблица записей модуля:

На следующем рисунке показана таблица записей кэша:

Создание экземпляра

Этот процесс откроет место для хранения в памяти (значение еще не заполнено в это время), а затем все переменные, которые экспортируют и импортируют модуль модуля, указывают на эту память.Этот процесс называется связыванием. Принципиальная схема его записи на экспорт выглядит следующим образом:

Затем идет импорт ссылок, принципиальная схема которого выглядит следующим образом:

Задание (Оценка)

Этот процесс выполнит код модуля и заполнит пространство памяти, открытое на предыдущем этапе, реальным значением.После этого процесса значение, связанное импортом, является реальным значением, экспортируемым экспортом.

В соответствии с описанным выше процессом мы можем знать. Экспорт и импорт модуля модуля ES фактически указывают на один и тот же фрагмент памяти, но следует отметить, что значение этой памяти не может быть изменено при импорте, а экспорт может.Схема выглядит следующим образом:

Суммировать

В этой статье в основном рассматриваются и разбираются текущие основные схемы модуляции JavaScript CommonJs, AMD, CMD, ES Module, а также проводится простой анализ наиболее репрезентативных модульных реализаций (NodeJs, RequireJS, SeaJS, ES6). Для модулей на стороне сервера, поскольку модули хранятся локально и загрузка модулей удобна, загрузка модулей обычно выполняется путем синхронного чтения файлов. Для браузера его модули обычно хранятся в удаленной сети, а загрузка модуля — очень трудоемкий процесс, поэтому для загрузки файла модуля обычно используется метод динамической асинхронной загрузки скрипта. Кроме того, модульные реализации JavaScript как на стороне клиента, так и на стороне сервера будут кэшировать модули, чтобы уменьшить нагрузку на вторичную загрузку.

Справочная статья:ES modules: A cartoon deep-dive

Пролистайте и обратите внимание на внешний публичный аккаунт Xunlei.