Углубленный механизм загрузки модулей Node.js, рукописная функция запроса

Node.js внешний интерфейс

Модуль — очень простая и важная концепция в Node.js.Различные собственные библиотеки классов предоставляются через модули, а сторонние библиотеки также управляются и ссылаются через модули. Эта статья начнется с базового принципа модуля, и, наконец, мы будем использовать этот принцип для самостоятельной реализации простого механизма загрузки модуля, то есть для реализацииrequire.

Полный код этой статьи загружен на GitHub:GitHub.com/Денис — см....

Простой пример

Старые правила, прежде чем мы поговорим о принципе, давайте возьмем простой пример и начнем с этого примера, чтобы шаг за шагом углубиться в принцип. Если вы хотите что-то экспортировать в Node.js, вам нужно использоватьmodule.exports,использоватьmodule.exportsМожно экспортировать практически любой тип объекта JS, включая строки, функции, объекты, массивы и многое другое. Давайте построим одинa.jsэкспортировать самое простоеhello world:

// a.js 
module.exports = "hello world";

затем еще одинb.jsЭкспорт функции:

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

module.exports = add;

затем вindex.jsиспользовать их внутрь, т.е.requireих,requireРезультатом, возвращаемым функцией, является соответствующий файлmodule.exportsЗначение:

// index.js
const a = require('./a.js');
const add = require('./b.js');

console.log(a);      // "hello world"
console.log(add(1, 2));    // b导出的是一个加法函数,可以直接使用,这行结果是3

require сначала запустит целевой файл

когда мыrequireКогда модуль используется, это не только егоmodule.exports, вместо этого запустит файл с нуля,module.exports = XXXНа самом деле это всего лишь одна строка кода, о которой мы поговорим позже Эффект этой строки кода фактически заключается в модификации кода в модуле.exportsАтрибуты. Например, у нас есть ещеc.js:

// c.js
let c = 1;

c = c + 1;

module.exports = c;

c = 6;

существуетc.jsВнутри мы экспортируемc,этоcПосле нескольких шагов расчета, при переходе кmodule.exports = c;эта линияcценность2, поэтому мыrequireизc.jsЗначение2, который будет позжеcЗначение для6Не влияет на предыдущую строку кода:

const c = require('./c.js');

console.log(c);  // c的值是2

Переднийc.jsПеременныеcявляется базовым типом данных, поэтому последнийc = 6;не влияет на предыдущийmodule.exportsа если он ссылочный тип? Попробуем напрямую:

// d.js
let d = {
  num: 1
};

d.num++;

module.exports = d;

d.num = 6;

затем вindex.jsвrequireон:

const d = require('./d.js');

console.log(d);     // { num: 6 }

мы нашли вmodule.exportsотдайd.numНазначение все еще работает, потому чтоdЭто объект и ссылочный тип, и мы можем изменить его значение с помощью этой ссылки. На самом деле для ссылочных типов не только вmodule.exportsЕго значение может быть изменено позже, а также может быть изменено вне модуля, напримерindex.jsВы можете напрямую изменить его внутри:

const d = require('./d.js');

d.num = 7;
console.log(d);     // { num: 7 }

requireа такжеmodule.exportsне черная магия

Как мы видим из предыдущего примера,requireа такжеmodule.exportsНичего сложного, допустим, что есть глобальный объект{}, который изначально пуст, когда выrequireКогда определенный файл найден, файл вынимается и выполняется, если файл в нем существуетmodule.exports, при запуске этой строки кода будетmodule.exportsК этому объекту добавляется значение , ключом является соответствующее имя файла, и, наконец, этот объект будет выглядеть так:

{
  "a.js": "hello world",
  "b.js": function add(){},
  "c.js": 2,
  "d.js": { num: 2 }
}

когда ты сноваrequireКогда есть определенный файл, если в объекте есть соответствующее значение, оно будет возвращено вам напрямую.Если нет, повторите предыдущие шаги, выполните целевой файл, а затем преобразуйте его значение.module.exportsДобавьте этот глобальный объект и верните его вызывающей стороне. Этот глобальный объект на самом деле является кешем, о котором мы часто слышим. **такrequireа такжеmodule.exportsНет никакой черной магии, просто запустите и получите значение целевого файла, затем добавьте его в кеш и используйте, когда вам это нужно. **Посмотрите на этот объект еще раз, потому чтоd.jsЭто ссылочный тип, поэтому вы можете изменить его значение, когда получите эту ссылку в любом месте.Если вы не хотите, чтобы значение вашего собственного модуля было изменено, вам нужно иметь дело с этим, когда вы пишете свой собственный модуль, например с использованиемObject.freeze(),Object.defineProperty()такие методы, как.

Типы модулей и порядок загрузки

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

тип модуля

В Node.js есть несколько типов модулей. Те, которые мы использовали ранее, на самом деле文件模块, чтобы подвести итог, есть в основном два типа:

  1. Встроенные модули: это функция, изначально предоставляемая Node.js, напримерfs,httpПодождите, эти модули загружаются при обработке Node.js.
  2. файловый модуль: Несколько модулей, написанных нами ранее, а также сторонние модули, а именноnode_modulesСледующие модули являются файловыми модулями.

порядок загрузки

Порядок загрузки - это когда мыrequire(X)когда, в каком порядке и где искатьX, в официальной документации естьПодробный псевдокод, в таком порядке:

  1. Сначала загружается встроенный модуль, даже если есть файл с таким же именем, встроенный модуль будет использоваться первым.
  2. Это не встроенный модуль, сначала зайдите в кеш, чтобы найти его.
  3. Если кэша нет, найти файл, соответствующий пути.
  4. Если соответствующий файл не существует, этот путь загружается как папка.
  5. Если соответствующие файлы и папки не найдены, перейдите кnode_modulesИщите ниже.
  6. Ошибка, если он не может быть найден.

загрузить папку

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

  1. Посмотрите, есть ли что-нибудь в этой папкеpackage.json, если есть, поищитеmainполе,mainЕсли поле имеет значение, загрузите соответствующий файл. Поэтому, если вы не можете найти запись при просмотре исходного кода какой-либо сторонней библиотеки, просто посмотрите на негоpackage.jsonвнутриmainполя, такие какjqueryизmainПоля вот такие:"main": "dist/jquery.js".
  2. если нетpackage.jsonилиpackage.jsonнет внутриmainпросто найдиindexдокумент.
  3. Если эти два шага не будут найдены, будет сообщено об ошибке.

Поддерживаемые типы файлов

requireПоддерживаются три основных типа файлов:

  1. .js:.jsФайл является нашим наиболее часто используемым типом файлов.При загрузке сначала будет запущен весь файл JS, а затем ранее упомянутыйmodule.exportsтак какrequireВозвращаемое значение.
  2. .json:.jsonФайл представляет собой обычный текстовый файл, который можно использовать напрямуюJSON.parseПросто преобразуйте его в объект и верните.
  3. .node:.nodeФайл представляет собой двоичный файл, скомпилированный на C++, и чистый внешний интерфейс обычно редко касается этого типа.

почеркrequire

На самом деле мы уже объясняли принцип семьдесят семь-восемьдесят восемь, а теперь давайте подойдем к нашей изюминке и реализуем ее сами.require. выполнитьrequireПо сути, это реализация механизма загрузки модулей всего Node.js.Давайте рассмотрим проблемы, которые необходимо решить:

  1. Найдите соответствующий файл, передав имя пути.
  2. Выполнить найденный файл и вставить его одновременноmoduleа такжеrequireЭти методы и свойства используются файлами модулей.
  3. возвратный модульmodule.exports

Handwritten code in this article refer to all Node.js official source, function names and variable names as consistent as possible, in fact, streamlined version of the source code, we can see the shining, I will write to the specific method affix the corresponding адрес источника.总体的代码都在这个文件里面:GitHub.com/node будет /node…

Класс модуля

Все функции загрузки модуля Node.js находятся вModuleВнутри класса весь код использует объектно-ориентированное мышление,Если вы не очень хорошо знакомы с объектно-ориентированным JS, вы можете сначала прочитать эту статью..Moduleконструктор классаЭто не сложно, в основном инициализация некоторых значений, чтобы следовать официальнымModuleИмя различается, наш собственный класс называется какMyModule:

function MyModule(id = '') {
  this.id = id;       // 这个id其实就是我们require的路径
  this.path = path.dirname(id);     // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
  this.exports = {};        // 导出的东西放这里,初始化为空对象
  this.filename = null;     // 模块对应的文件名
  this.loaded = false;      // loaded用来标识当前模块是否已经加载
}

требуемый метод

мы использовалиrequireФактическиModuleМетод экземпляра класса, содержимое очень простое, сначала выполните проверку параметров, а затем вызовитеModule._loadметод, см. исходный код здесь:GitHub.com/node будет /node…. Сокращенная версия кодекса выглядит следующим образом:

MyModule.prototype.require = function (id) {
  return Module._load(id);
}

MyModule._load

MyModule._loadстатический метод, т.requireНастоящим предметом метода, что он делает на самом деле, является:

  1. Сначала проверьте, существует ли запрошенный модуль в кеше, и если да, вернитесь непосредственно к закешированному модулю.exports.
  2. Если не в кеше, тоnewОдинModuleЭкземпляр, используйте этот экземпляр для загрузки соответствующего модуля и возврата модуляexports.

Эти два требования мы реализуем сами, а кеш размещаем прямо наModule._cacheДля этой статической переменной официальная инициализация этой переменнойObject.create(null), чтобы созданный прототип указывал наnull, поступим так же:

MyModule._cache = Object.create(null);

MyModule._load = function (request) {    // request是我们传入的路劲参数
  const filename = MyModule._resolveFilename(request);

  // 先检查缓存,如果缓存存在且已经加载,直接返回缓存
  const cachedModule = MyModule._cache[filename];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }

  // 如果缓存不存在,我们就加载这个模块
  // 加载前先new一个MyModule实例,然后调用实例方法load来加载
  // 加载完成直接返回module.exports
  const module = new MyModule(filename);
  
  // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整
  MyModule._cache[filename] = module;
  
  module.load(filename);
  
  return module.exports;
}

Исходный код, соответствующий приведенному выше коду, находится здесь:GitHub.com/node будет /node…

Вы можете видеть, что приведенный выше исходный код также вызывает два метода:MyModule._resolveFilenameа такжеMyModule.prototype.load, давайте реализуем эти два метода ниже.

MyModule._resolveFilename

MyModule._resolveFilenameКак видно из названия, этот метод передается пользователемrequireПараметры для преобразования в реальный адрес файла, этот метод в исходном коде более сложен, потому что, согласно вышеизложенному, он должен поддерживать множество параметров: встроенные модули, относительные пути, абсолютные пути, папки и сторонние модули и т.д., если это папка или сторонний модуль, также необходимо разобрать внутриpackage.jsonа такжеindex.js. Здесь мы в основном говорим о принципе, поэтому реализуем поиск файлов только по относительным путям и абсолютным путям, и поддерживаем автоматическое добавлениеjsа такжеjsonДва суффикса:

MyModule._resolveFilename = function (request) {
  const filename = path.resolve(request);   // 获取传入参数对应的绝对路径
  const extname = path.extname(request);    // 获取文件后缀名

  // 如果没有文件后缀名,尝试添加.js和.json
  if (!extname) {
    const exts = Object.keys(MyModule._extensions);
    for (let i = 0; i < exts.length; i++) {
      const currentPath = `${filename}${exts[i]}`;

      // 如果拼接后的文件存在,返回拼接的路径
      if (fs.existsSync(currentPath)) {
        return currentPath;
      }
    }
  }

  return filename;
}

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

MyModule._resolveFilenameСоответствующий исходный код находится здесь:GitHub.com/node будет /node…

MyModule.prototype.load

MyModule.prototype.loadЭто метод экземпляра. Этот метод действительно используется для загрузки модулей. На самом деле это запись для разных типов файлов, которые нужно загрузить. Разные типы файлов будут соответствоватьMyModule._extensionsВнутри метода:

MyModule.prototype.load = function (filename) {
  // 获取文件后缀名
  const extname = path.extname(filename);

  // 调用后缀名对应的处理函数来处理
  MyModule._extensions[extname](this, filename);

  this.loaded = true;
}

Обратите внимание, что в этом кодеthisуказывает наmoduleэкземпляр, потому что он является методом экземпляра. Соответствующий исходный код находится здесь:GitHub.com/node будет /node…

Загрузите js-файл: MyModule._extensions['.js']

Как мы уже говорили, методы обработки различных типов файлов смонтированы вMyModule._extensionsВыше, давайте сначала реализуем.jsЗагрузка типовых файлов:

MyModule._extensions['.js'] = function (module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
}

можно увидетьjsМетод загрузки очень прост, просто прочитайте содержимое файла, а затем вызовите другой метод экземпляра._compileказнить его. Соответствующий исходный код находится здесь:GitHub.com/node будет /node…

Скомпилируйте и выполните файл js: MyModule.prototype._compile

MyModule.prototype._compileЭто ядро ​​загрузки файлов JS, а также наиболее часто используемый метод.Этот метод требует извлечения целевого файла и его повторного выполнения.Перед его выполнением весь код необходимо обернуть в слой для внедрения.exports, require, module, __dirname, __filename, поэтому мы можем использовать эти переменные прямо в JS-файле. Добиться такого впрыска несложно, еслиrequireФайл простойHello World, выглядит так:

module.exports = "hello world";

Так как мы вселить его?moduleА как насчет этой переменной? Ответ заключается в том, чтобы добавить слой функций вне его во время выполнения, чтобы он выглядел так:

function (module) { // 注入module变量,其实几个变量同理
  module.exports = "hello world";
}

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

MyModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

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

MyModule.wrap = function (script) {
  return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};

так черезMyModule.wrapУпакованный код можно получитьexports, require, module, __filename, __dirnameэти переменные. Как только вы это узнаете, вы можете написатьMyModule.prototype._compileсейчас:

MyModule.prototype._compile = function (content, filename) {
  const wrapper = Module.wrap(content);    // 获取包装后函数体

  // vm是nodejs的虚拟机沙盒模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数
  // 返回值就是转化后的函数,所以compiledWrapper是一个函数
  const compiledWrapper = vm.runInThisContext(wrapper, {
    filename,
    lineOffset: 0,
    displayErrors: true,
  });

  // 准备exports, require, module, __filename, __dirname这几个参数
  // exports可以直接用module.exports,即this.exports
  // require官方源码中还包装了一层,其实最后调用的还是this.require
  // module不用说,就是this了
  // __filename直接用传进来的filename参数了
  // __dirname需要通过filename获取下
  const dirname = path.dirname(filename);

  compiledWrapper.call(this.exports, this.exports, this.require, this,
    filename, dirname);
}

В приведенном выше коде следует обратить внимание на несколько параметров, которые мы вводим и передаем.callпрошел вthis:

  1. this:compiledWrapperчерезcallВызывается, первый параметр находится внутриthis, здесь мы переходим вthis.exports, то есть,module.exports, то есть мыjsвнутри файлаthisправдаmodule.exportsцитата из .
  2. exports: compiledWrapperПервый формально полученный параметрexports, мы тоже проходимthis.exports,такjsв файлеexportsдаmodule.exportsцитата из .
  3. require: этот метод, который мы передаем,this.require, по фактуMyModule.prototype.require, то есть,MyModule._load.
  4. module: мы проходим внутрьthis, который является экземпляром текущего модуля.
  5. __filename: Абсолютный путь, по которому находится файл.
  6. __dirname: Абсолютный путь к папке, в которой находится файл.

На данный момент наш JS-файл фактически записан, и соответствующий исходный код находится здесь:GitHub.com/node будет /node…

Загрузите файл json: MyModule._extensions['.json']

нагрузкаjsonФайл намного проще, просто прочитайте файл и разберите его наjsonПросто делать:

MyModule._extensions['.json'] = function (module, filename) {
  const content = fs.readFileSync(filename, 'utf8');
  module.exports = JSONParse(content);
}

exportsа такжеmodule.exportsразница

Люди часто спрашивают в Интернете,node.jsвнутриexportsа такжеmodule.exportsВ чем разница?На самом деле, наш рукописный код уже дал ответ.Мы подробно объясним этот вопрос здесь.exportsа такжеmodule.exportsОбе переменные вводят следующую строку кода.

compiledWrapper.call(this.exports, this.exports, this.require, this,
    filename, dirname);

В начальном состоянии,exports === module.exports === {},exportsдаmodule.exportsЦитата из , если вы использовали это так:

exports.a = 1;
module.exports.b = 2;

console.log(exports === module.exports);   // true

В приведенном выше кодеexportsа такжеmodule.exportsоба указывают на один и тот же объект{}, вы добавляете свойства к этому объекту, не изменяя ссылочный адрес самого объекта, поэтомуexports === module.exportsбыло установлено.

Но если вы когда-нибудь используете это так:

exports = {
  a: 1
}

Или используйте это так:

module.exports = {
	b: 2
}

Тогда вы на самом деле даетеexportsилиmodule.exportsПереназначение меняет их ссылочные адреса, тогда связь между этими двумя свойствами разрывается, и они перестают быть равными.Следует отметить, что выmodule.exportsПереназначение будет экспортировано как модуль, но выexportsПереназначение не меняет экспорт модуля, а просто меняетexportsЭта переменная только потому, что модуль всегдаmodule, содержимое экспортаmodule.exports.

циклическая ссылка

Node.js обрабатывает циклические ссылки. Вот официальный пример:

a.js:

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js:

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js:

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

когдаmain.jsнагрузкаa.jsчас,a.jsзагрузить сноваb.js. В настоящее время,b.jsпопробую загрузитьa.js. Чтобы предотвратить бесконечные циклы, вернитеa.jsизexportsобъектнезаконченная копияДатьb.jsмодуль. потомb.jsзавершена загрузка иexportsобъект, предоставленныйa.jsмодуль.

Так как этот эффект достигнут? Ответ в нашемMyModule._loadВ исходном коде обратите внимание на порядок этих двух строк кода:

MyModule._cache[filename] = module;

module.load(filename);

В приведенном выше коде мы сначала устанавливаем кеш, а затем выполняем настоящийload, в этом направлении мысли я могу взглянуть на процесс загрузки здесь:

  1. mainнагрузкаa,aЗанять место в кеше перед фактической загрузкой
  2. aзагружен официальной загрузкойb
  3. bзагружен сноваa, в это время уже есть в кэшеa, поэтому вернитесь напрямуюa.exports, даже в это времяexportsявляется неполным.

Суммировать

  1. requireЭто не черная магия, весь механизм загрузки модулей Node.jsJSосуществленный.
  2. в каждом модулеexports, require, module, __filename, __dirnameНи один из пяти параметров не является глобальной переменной, а вводится при загрузке модуля.
  3. Чтобы внедрить эти переменные, нам нужно обернуть код пользователя в функцию, написать строку и вызвать модуль песочницы.vmреализовать.
  4. В исходном состоянии в модулеthis, exports, module.exportsВсе указывайте на тот же объект, если вы их переназнаете, соединение будет нарушено.
  5. правильноmodule.exportsПереназначение будет экспортировано как модуль, но выexportsПереназначение не меняет экспорт модуля, а просто меняетexportsЭта переменная только потому, что модуль всегдаmodule, содержимое экспортаmodule.exports.
  6. Чтобы решить циклическую ссылку, модуль будет добавлен в кеш перед загрузкой, и при следующей загрузке он сразу вернется в кеш.Если модуль в это время не был загружен, вы можете получить незавершенныйexports.
  7. Механизм загрузки, реализованный Node.js, называетсяCommonJS.

Полный код этой статьи загружен на GitHub:GitHub.com/Денис — см....

использованная литература

Исходный код загрузки модуля Node.js:GitHub.com/node будет /node…

Официальная документация модуля Node.js:узел будет .capable/api/modules…

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

Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~

Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…

Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....