Модуль — очень простая и важная концепция в 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 есть несколько типов модулей. Те, которые мы использовали ранее, на самом деле文件模块
, чтобы подвести итог, есть в основном два типа:
- Встроенные модули: это функция, изначально предоставляемая Node.js, например
fs
,http
Подождите, эти модули загружаются при обработке Node.js.- файловый модуль: Несколько модулей, написанных нами ранее, а также сторонние модули, а именно
node_modules
Следующие модули являются файловыми модулями.
порядок загрузки
Порядок загрузки - это когда мыrequire(X)
когда, в каком порядке и где искатьX
, в официальной документации естьПодробный псевдокод, в таком порядке:
- Сначала загружается встроенный модуль, даже если есть файл с таким же именем, встроенный модуль будет использоваться первым.
- Это не встроенный модуль, сначала зайдите в кеш, чтобы найти его.
- Если кэша нет, найти файл, соответствующий пути.
- Если соответствующий файл не существует, этот путь загружается как папка.
- Если соответствующие файлы и папки не найдены, перейдите к
node_modules
Ищите ниже.- Ошибка, если он не может быть найден.
загрузить папку
Как было сказано ранее, если вы не можете найти файл, вы найдете папку, но загрузить всю папку невозможно.При загрузке папки также существует порядок загрузки:
- Посмотрите, есть ли что-нибудь в этой папке
package.json
, если есть, поищитеmain
поле,main
Если поле имеет значение, загрузите соответствующий файл. Поэтому, если вы не можете найти запись при просмотре исходного кода какой-либо сторонней библиотеки, просто посмотрите на негоpackage.json
внутриmain
поля, такие какjquery
изmain
Поля вот такие:"main": "dist/jquery.js"
.- если нет
package.json
илиpackage.json
нет внутриmain
просто найдиindex
документ.- Если эти два шага не будут найдены, будет сообщено об ошибке.
Поддерживаемые типы файлов
require
Поддерживаются три основных типа файлов:
- .js:
.js
Файл является нашим наиболее часто используемым типом файлов.При загрузке сначала будет запущен весь файл JS, а затем ранее упомянутыйmodule.exports
так какrequire
Возвращаемое значение.- .json:
.json
Файл представляет собой обычный текстовый файл, который можно использовать напрямуюJSON.parse
Просто преобразуйте его в объект и верните.- .node:
.node
Файл представляет собой двоичный файл, скомпилированный на C++, и чистый внешний интерфейс обычно редко касается этого типа.
почеркrequire
На самом деле мы уже объясняли принцип семьдесят семь-восемьдесят восемь, а теперь давайте подойдем к нашей изюминке и реализуем ее сами.require
. выполнитьrequire
По сути, это реализация механизма загрузки модулей всего Node.js.Давайте рассмотрим проблемы, которые необходимо решить:
- Найдите соответствующий файл, передав имя пути.
- Выполнить найденный файл и вставить его одновременно
module
а такжеrequire
Эти методы и свойства используются файлами модулей.- возвратный модуль
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
Настоящим предметом метода, что он делает на самом деле, является:
- Сначала проверьте, существует ли запрошенный модуль в кеше, и если да, вернитесь непосредственно к закешированному модулю.
exports
.- Если не в кеше, то
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
:
- this:
compiledWrapper
черезcall
Вызывается, первый параметр находится внутриthis
, здесь мы переходим вthis.exports
, то есть,module.exports
, то есть мыjs
внутри файлаthis
правдаmodule.exports
цитата из .- exports:
compiledWrapper
Первый формально полученный параметрexports
, мы тоже проходимthis.exports
,такjs
в файлеexports
даmodule.exports
цитата из .- require: этот метод, который мы передаем,
this.require
, по фактуMyModule.prototype.require
, то есть,MyModule._load
.- module: мы проходим внутрь
this
, который является экземпляром текущего модуля.- __filename: Абсолютный путь, по которому находится файл.
- __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
, в этом направлении мысли я могу взглянуть на процесс загрузки здесь:
main
нагрузкаa
,a
Занять место в кеше перед фактической загрузкойa
загружен официальной загрузкойb
b
загружен сноваa
, в это время уже есть в кэшеa
, поэтому вернитесь напрямуюa.exports
, даже в это времяexports
является неполным.
Суммировать
-
require
Это не черная магия, весь механизм загрузки модулей Node.jsJS
осуществленный. - в каждом модуле
exports, require, module, __filename, __dirname
Ни один из пяти параметров не является глобальной переменной, а вводится при загрузке модуля. - Чтобы внедрить эти переменные, нам нужно обернуть код пользователя в функцию, написать строку и вызвать модуль песочницы.
vm
реализовать. - В исходном состоянии в модуле
this, exports, module.exports
Все указывайте на тот же объект, если вы их переназнаете, соединение будет нарушено. - правильно
module.exports
Переназначение будет экспортировано как модуль, но выexports
Переназначение не меняет экспорт модуля, а просто меняетexports
Эта переменная только потому, что модуль всегдаmodule
, содержимое экспортаmodule.exports
. - Чтобы решить циклическую ссылку, модуль будет добавлен в кеш перед загрузкой, и при следующей загрузке он сразу вернется в кеш.Если модуль в это время не был загружен, вы можете получить незавершенный
exports
. - Механизм загрузки, реализованный 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/Денис — см....