Глубокое понимание механизма модуля Nodejs

Node.js

Мы все знаем, что Nodejs следуетCommonJSнорма, когда мыrequire('moduleA')Когда, как модуль получает модуль по имени или пути? Во-первых, давайте поговорим о трех понятиях: ссылка на модуль, определение модуля и идентификация модуля.

1 CommonJSТехнические характеристики

1.1 Справочник по модулям

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

//test.js
//引入一个模块到当前上下文中
const math = require('math');
math.add(1, 2);

1.2 Определение модуля

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

//math.js
exports.add = function () {
    let sum = 0, i = 0, args = arguments, l = args.length;
    while(i < l) {
        sum += args[i++];
    }
    return sum;
}

Это может быть какtest.jsВызовите свойство или метод модуля после require().

1.3 Идентификация модуля

Идентификаторы модулей передаются给require()Параметр метода, который должен быть строкой в ​​верблюжьем регистре или начинаться с.,..Относительный или абсолютный путь в начале не может иметь файлового суффикса..js.

2. Реализация модуля узла

Чтобы внедрить модуль в Node, вам необходимо пройти следующие четыре шага:

  • анализ пути
  • Позиционирование файла
  • Скомпилировать и выполнить
  • добавить память

2.1 Анализ пути

Модули в Node.js могут получать ссылки на модули по пути или имени файла.Ссылка на модуль сопоставляется с путем к файлу js.. Модули в Node делятся на две категории:

  • Одним из них является модуль, предоставляемый Node, который называетсяосновной модуль(встроенные модули), встроенные модули предоставляют разработчикам некоторые общие API-интерфейсы, и они предварительно загружаются при запуске процесса Node.
  • Другая категория — пользовательские модули, называемыефайловый модуль. Подобно сторонним модулям или локальным модулям, установленным через NPM, каждый модуль предоставляет общедоступный API. чтобы разработчики могли импортировать. Такие как
const mod = require('module_name')
const { methodA } = require('module_name')

После выполнения Node загрузит встроенные модули или модули, установленные через NPM. Функция require возвращает объект, а API, предоставляемый объектом, может быть функциями, объектами или свойствами, такими как функции, массивы или даже объект JS любого типа.

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

Вот механизм загрузки и кэширования модуля узла:

1. Загрузите встроенный модуль (основной модуль)

2. Загрузите файловый модуль (файловый модуль)

3. Загрузите модуль каталога файлов (модуль папки)

4. Загрузите модули в node_modules

5. Автоматически кэшировать загруженные модули  

1. Загрузите встроенный модуль

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

const http = require('http')

Возвращаемый http — это встроенный модуль Node, который реализует функцию HTTP.

2. Загрузите файловый модуль

абсолютный путь

const myMod = require('/home/base/my_mod')

или относительный путь

const myMod = require('./my_mod')

Обратите внимание, что расширение здесь игнорируется.js, следующие эквивалентны

const myMod = require('./my_mod')
const myMod = require('./my_mod.js')

3. Загрузите модуль каталога файлов

Вы можете напрямую потребовать каталог, предполагая, что существует каталог с именем папки, например

const myMod = require('./folder')

На этом этапе Node будет искать весь каталог папки, предположит, что папка является пакетом, и попытается найти файл определения пакета package.json. Если каталог папки не содержитpackage.jsonфайл, Node будет считать, что основным файлом по умолчанию являетсяindex.js, который загружаетindex.js. еслиindex.jsНе существует, то нагрузка не удастся.

4. Загрузите модули в node_modules

Если имя модуля не является путем и не является встроенным модулем, Node попытается перейти в текущий каталог.node_modulesПоиск в папке. Если текущий каталогnode_modulesне найден, Node запустится из родительского каталогаnode_modulesПоиск внутри и так далее рекурсивно до корневого каталога.

5. Автоматически кэшировать загруженные модули

Node будет кэшировать загруженные модули без необходимости повторного поиска каждый раз. Ниже приведен пример

// modA.js
console.log('模块modA开始加载...')
exports = function() {
    console.log('Hi')
}
console.log('模块modA加载完毕')
//init.js
var mod1 = require('./modA')
var mod2 = require('./modA')
console.log(mod1 === mod2)

Командная строкаnode init.jsвоплощать в жизнь:

模块modA开始加载...
模块modA加载完毕
true

Видно, что хотя require выполняется дважды, modA.js по-прежнему выполняется только один раз. mod1 и mod2 идентичны, т. е. обе ссылки указывают на один и тот же объект модуля.

Сначала загрузить из кеша

Точно так же, как браузеры кэшируют статические js-файлы, Node также кэширует импортированные модули, разница в том, что браузеры кэшируют только файлы, а nodejs кэширует скомпилированные и выполненные объекты (кэш-память)require()Вторичная загрузка того же модуля всегда будет использовать метод приоритета кеша, который является первым приоритетом.Проверка кеша основного модуля предшествует проверке кеша файлового модуля.

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

let count = {}; // 因模块是封闭的,这里实际上借用了js闭包的概念
exports.count = function(name){
     if(count[name]){
          count[name]++;
     }else{
          count[name] = 1;
     }
     console.log(name + '被访问了' + count[name] + '次。');
};

мы маршрутизируемactionилиcontrollerЦитирую так:

let count = require('count');

export.index = function(req, res){
    count('index');
};

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

Прежде чем перейти к поиску пути, необходимо описатьmodule pathЭта концепция в Node.js. Для каждого загружаемого файлового модуля при создании объекта модуля модуль будет иметь свойство paths, значение которого вычисляется из пути к текущему файлу. мы создаемmodulepath.jsТакой файл, его содержимое:

// modulepath.js
console.log(module.paths);

Мы помещаем его в любой каталог и выполняем команду node modulepath.js, и мы получим следующий вывод.

[ '/home/ikeepstudying/research/node_modules',
'/home/ikeepstudying/node_modules',
'/home/node_modules',
'/node_modules' ]

2.2 Местоположение файла

1. Анализ расширения файла

передачаrequire()Если параметр не имеет расширения файла в методе, Node нажмет.js,.json,.nodeНайдите расширение дополнения, попробуйте по очереди.

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

2. Анализ каталогов и пакеты

require()После анализа расширения файла соответствующий файл может быть не найден, но найден каталог, в это время Node будет рассматривать каталог как пакет.

Сначала Node просматривает каталог подпорной стены.package.json,пройти черезJSON.parse()Разберите объект описания пакета и извлеките имя файла, указанное основным атрибутом для позиционирования. Если в основном атрибуте указано неправильное имя файла или отсутствуетpachage.jsonфайл, Node будет использовать index в качестве имени файла по умолчанию.

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

1. Изmodule pathВ качестве ссылки для поиска берется первый каталог в массиве.

2. Найдите файл прямо в каталоге и завершите поиск, если он существует. Если его нет, перейдите к следующему поиску.

3. Попробуйте добавить.js,.json,.nodeИщите по суффиксу, если есть файл, заканчивайте поиск. Если он не существует, перейдите к следующей записи.

4. Попробуйте поставитьrequireПараметры ищутся пакетом, читаем каталогpackage.jsonфайл, получитьmainФайл, указанный параметром.

5. Попытайтесь найти файл, если он существует, завершите поиск. Если он не существует, выполните третий поиск.

6. Если он продолжает выходить из строя, выньте его.module pathСледующий каталог в массиве используется в качестве базового поиска, повторяя шаги с 1 по 5.

7. Если он продолжает давать сбой, повторите шаги с 1 по 6 до последнего значения в пути к модулю.

8. Если это все еще не удается, сгенерируйте исключение.

Весь процесс поиска очень похож на поиск цепочки прототипов и поиск области действия. К счастью, Node.js реализует механизм кэширования для поиска пути, иначе, поскольку каждое определение пути выполняется синхронно и с блокировкой, это приведет к серьезному снижению производительности.

После успешной загрузки кеш с путем к модулю

2.3 Компиляция модуля

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

function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if(parent && parent.children) {
        parent.children.push(this);
    }
    this.filename = null;
    this.loaded = false;
    this.children = [];
}

Для разных расширений способ загрузки тоже разный:

  • .jsСкомпилируйте и выполните после синхронного чтения файла через модуль fs.
  • .nodeЭто файл расширения, написанный на C/C++ черезdlopen()Метод загружает файл, сгенерированный последней компиляцией.
  • .jsonПосле чтения файла синхронно с модулем fs используйтеJSON.pares()Разобрать возвращаемый результат

другой как.js

Путь к файлу каждого успешно скомпилированного модуля будет кэширован как индекс вModule._cacheна объекте.

jsonкомпиляция файлов

.jsonМетод вызова файла следующий:JSON.parse

//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
    try {
        module.exports = JSON.parse(stripBOM(content));
    } catch(err) {
        err.message = filename + ':' + err.message;
        throw err;
    }
}

Module._extensionsбудет назначеноrequire()изextensionsсвойство, поэтому вы можете использовать:console.log(require.extensions);Вывести методы загрузки расширения уже в системе. Конечно, вы также можете добавить какую-то специальную загрузку самостоятельно:

require.extensions['.txt'] = function(){
//code
};。

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

jsкомпиляция модулейВ процессе компиляции Node оборачивает содержимое файла javascript, полученного с помощью head и tail, и оборачивает содержимое файла в функцию:

(function (exports, require, module, __filename, __dirname) {
    var math = require(‘math‘);
    exports.area = function(radius) {
       return Math.PI * radius * radius;
    }
})

Упакованный код будет передавать собственный модуль vmrunInThisContext()Метод выполняется (с четким контекстом, не загрязняет глобальную), возвращает конкретный объект функции и, наконец, передает параметры для выполнения и возвращает после выполненияmodule.exports.

Core Module Compilation

Основные модули делятся наC/C++Написание и написание JavaScript две части, гдеC/C++Файлы помещаются в каталог src проекта Node, а файлы JavaScript помещаются в каталог lib.

1. Дамп как код C/C++

Node использует инструмент js2c.py, который поставляется с V8, преобразует весь встроенный код JavaScript в массивы на C++ и генерирует заголовочный файл node_natives.h:

namespace node {
    const char node_native[] = { 47, 47, ..};
    const char dgram_native[] = { 47, 47, ..};
    const char console_native = { 47, 47, ..};
    const char buffer_native = { 47, 47, ..};
    const char querystring_native = { 47, 47, ..};
    const char punycode_native = { 47, 47, ..};
    ...
    struct _native {
        const char* name;
        const char* source;
        size_t source_len;
    }
    static const struct _native natives[] = {
      { "node", node_native, sizeof(node_native)-1},
      { "dgram", dgram_native, sizeof(dgram_native)-1},
      ...
    };
}

Во время этого процессаКод JavaScript хранится в пространстве имен Node в виде строки, которая не выполняется напрямую.. При запуске процесса Node код js загружается прямо в память. В процессе загрузки модуль ядра js находится непосредственно в памяти после анализа идентификатора.

2. Скомпилируйте основной модуль js

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

исходный файл основного модуля js черезprocess.binding('natives')Выньте, успешно скомпилированный модуль кэшируется вNativeModule._cacheначальство. код показывает, как показано ниже:

function NativeModule() {
    this.filename = id + '.js';
    this.id = id;
    this.exports = {};
    this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};

3 importа такжеrequire

просто скажиimportа такжеrequireсущественная разница

importспецификация модуля ES6,requireЭто спецификация модуля commonjs, я не буду вводить подробное использование, я просто хочу поговорить об их самых основных различиях.import — статическая (время компиляции) загрузка модулей, require (время выполнения) — динамическая загрузка, тогда в чем разница между статической нагрузкой и динамической нагрузкой?

При статической загрузке код уже выполняется при компиляции, а динамическая загрузка выполняется после компиляции при запуске кода, так в чем конкретно суть? Давайте сначала поговорим об импорте, следующий код

import { name } from 'name.js'

// name.js文件
export let name = 'jinux'
export let age = 20

Приведенный выше код означаетmain.jsпредставлен в файлеname.jsПеременные, экспортируемые файлом, выполняются на этапе компиляции кода, и код выглядит следующим образом:

let name = 'jinux'

Это мое собственное понимание, на самом деле, непосредственноname.jsКод вmain.jsфайл, как вmain.jsкак заявлено в файле. Давайте снова посмотрим на требование

var obj = require('obj.js');

// obj.js文件
var obj = {
  name: 'jinux',
  age: 20
}
module.export obj;

require находится в стадии выполнения, вам нужно загрузить весь объект obj в память, а затем использовать, какая переменная используется.Давайте сравним это здесьimport,importЭто статическая загрузка.Если ввести только имя, возраст не будет введен, поэтому он импортируется по запросу, и производительность лучше.

4 nodejs очищают, требуют кеша

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

Так вот вопрос, крепкий экскаватор Какой? О, нет!Нет!Нет!Чтобы сделать после того, как изменить файл, перезагрузите сервер автоматически.

server.jsФрагмент из:

const port = process.env.port || 1337;
app.listen(port);
console.log("server start in " + port);
exports.app = app;

Допустим, мы сейчас такие, фрагмент app.js:

const app = require('./server.js');

Если мы запустили сервер в server.js, мы остановим сервер, который можно вызвать в app.js

app.app.close()

Но когда мы снова вводим server.js

app =  require('./server.js')

Когда вы обнаружите, что не используете последний файл server.js, причиной является механизм кэширования require, когда первый вызовrequire('./server.js')время кэшируется.

Что нам делать в это время?

Следующий код решает проблему:

delete require.cache[require.resolve('./server.js')];
app = require('./server.js');