Когда мы используем Nodejs для повседневной разработки, мы часто используем требование для импорта модулей двух типов: один — это модуль, который мы написали сами, или сторонний модуль, установленный с помощью npm. Этот тип модуля в Node называется文件模块
; Это встроенный модуль Node, который мы можем использовать, например os
, fs
и другие модули. Эти модули называются核心模块
.
Следует отметить, что разница между файловым модулем и основным модулем заключается не только в том, встроен ли он в Node, но также в расположении файла, процессе компиляции и выполнения модуля. Между ними есть очевидные различия. . Мало того, файловые модули также можно разделить на обычные файловые модули, пользовательские модули или модули расширения C/C++ и т. д. Различные модули также имеют множество деталей, которые различаются позиционированием файлов, компиляцией и другими процессами.
В этой статье будут рассмотрены эти проблемы и разъяснены концепции файловых модулей и основных модулей, а также их конкретные процессы и детали, на которые необходимо обратить внимание при расположении файлов, компиляции или выполнении. Надеюсь, она будет вам полезна.
Начнем с файлового модуля.
Что такое файловый модуль?
В Node модули, для которых требуются идентификаторы модулей, начинающиеся с .、.. 或/
(то есть с использованием относительных или абсолютных путей), будут рассматриваться как файловые модули. Кроме того, существует особый тип модуля. Хотя он не содержит ни относительного, ни абсолютного пути и не является основным модулем, он указывает на пакет. Когда Node находит модуль этого типа, он будет использовать模块路径
.模块路径
для поиска модулей по одному. Этот тип модуля называется пользовательским модулем.
Таким образом, файловые модули включают два типа: один — обычные файловые модули с путями, а другой — пользовательские модули без путей.
Файловый модуль динамически загружается во время выполнения, что требует полного поиска файла, процесса компиляции и выполнения и работает медленнее, чем основной модуль.
При позиционировании файлов Node обрабатывает эти два типа файловых модулей по-разному. Давайте подробнее рассмотрим процессы поиска этих двух типов файловых модулей.
. Для обычных файловых модулей, поскольку путь, по которому они несут, очень ясен, поиск не займет много времени, поэтому эффективность поиска выше, чем у пользовательского модуля, представленного ниже. Однако есть еще два момента, на которые следует обратить внимание.
Во-первых, в обычных обстоятельствах при использовании require для введения файлового модуля расширение файла обычно не указывается, например:
const math = require("math");
Поскольку расширение не указано, Node не может определить конечный файл. В этом случае Node выполнит расширения в порядке .js、.json、.node
и попробует их одно за другим. Этот процесс называется文件扩展名分析
.
Следует также отметить, что в реальной разработке, помимо запроса конкретного файла, мы обычно также указываем каталог, например:
const axios = require("../network");
В этом случае Node сначала выполняет файл. анализ расширения. Если соответствующий файл не найден, но каталог получен, Node будет рассматривать каталог как пакет.
В частности, Node вернет в качестве результата поиска файл, на который указывает main
поле package.json
в каталоге. Если файл, на который указывает main, неправильный или файл package.json
вообще не существует, Node будет использовать index
в качестве имени файла по умолчанию, а затем использовать .js
и .node
для анализа расширений и поиска целевого файла. один за другим, если он не найден, выдаст ошибку.
(Разумеется, поскольку Node имеет два типа модульных систем, CJS и ESM, помимо поиска основного поля Node будет использовать и другие методы. Поскольку это выходит за рамки данной статьи, я не буду вдаваться в подробности. )
Только что упоминался. Когда Node ищет пользовательские модули, он будет использовать путь к модулю. Так каков путь к модулю?
Друзья, знакомые с анализом модулей, должны знать, что путь к модулю представляет собой массив, состоящий из путей. Конкретное значение можно увидеть в следующем примере:
// example.js. console.log(module.paths);
результаты печати:
Как вы можете видеть, модуль в Node имеет массив путей к модулю, который хранится в файле module.paths
и используется для указания того, как Node находит пользовательский модуль, на который ссылается текущий модуль.
В частности, Node будет проходить по массиву путей модуля, пробовать каждый путь один за другим и выяснять, есть ли указанный пользовательский модуль в каталоге node_modules
, соответствующий этому пути. Если нет, он будет шаг за шагом рекурсивно двигаться вверх, пока не достигнет. node_modules
в корневом каталоге, пока целевой модуль не будет найден, если он не найден, будет выдана ошибка.
Видно, что пошаговый рекурсивный поиск в каталоге node_modules
— это стратегия Node по поиску пользовательских модулей, а путь к модулю — это конкретная реализация этой стратегии.
При этом мы также пришли к выводу, что при поиске пользовательских модулей, чем глубже уровень, тем более трудоемким будет соответствующий поиск. Поэтому по сравнению с основными модулями и обычными файловыми модулями скорость загрузки пользовательских модулей самая низкая.
Конечно, по пути к модулю находится только каталог, а не конкретный файл. После нахождения каталога Node также будет выполнять поиск в соответствии с процессом обработки пакета, описанным выше. Конкретный процесс не будет описываться снова.
Выше описан процесс позиционирования файлов и детали, на которые необходимо обратить внимание для обычных файловых модулей и пользовательских модулей. Далее давайте посмотрим, как компилируются и выполняются два типа модулей.
Когдаи файл, на который указывает require, находится, идентификатор модуля обычно не имеет расширения. Согласно упомянутому выше анализу расширения файла, мы можем знать, что Node поддерживает компиляцию и выполнение файлов с. три расширения:
файл JavaScript. Файл считывается синхронно через модуль fs
, а затем компилируется и выполняется. За исключением файлов .node
и .json
, остальные файлы будут загружаться как файлы .js
.
Файл .node
, который представляет собой файл расширения, скомпилированный и созданный после записи на C/C++. Node загружает файл с помощью методаprocess.dlopen process.dlopen()
.
json, после синхронного чтения файла через модуль fs
используйте JSON.parse()
для анализа и возврата результата.
Перед компиляцией и выполнением файлового модуля Node обернет его с помощью оболочки модуля, как показано ниже:
(function(exports, require, Module, __filename, __dirname) { //Код модуля});
Видно, что через обертку модуля Node упаковывает модуль в область видимости функции и изолирует его от других областей видимости, чтобы избежать таких проблем, как конфликты имен переменных и загрязнение глобальной области видимости. время, передавая параметры экспорта и требования, которые позволяют модулю иметь необходимые возможности импорта и экспорта. Это реализация модулей Node.
Разобравшись с оболочкой модуля, давайте сначала посмотрим на процесс компиляции и выполнения файла json.
Компиляция и исполнение json-файлов является самым простым. После синхронного чтения содержимого файла JSON через модуль fs
Node будет использовать JSON.parse() для анализа объекта JavaScript, затем назначит его объекту экспорта модуля и, наконец, вернет его в модуль, который ссылается на него. . Процесс очень простой и грубый.
. После использования оболочки модуля для упаковки файлов JavaScript завернутый код будет выполнен с помощью метода runInThisContext()
(аналогично eval) модуля vm
, возвращающего объект функции.
Затем экспорт, require, модуль и другие параметры модуля JavaScript передаются этой функции для выполнения. После выполнения атрибут экспорта модуля возвращается вызывающей стороне. Это процесс компиляции и выполнения файла JavaScript.
Прежде чем объяснять компиляцию и выполнение модулей расширения C/C++, давайте сначала представим, что такое модуль расширения C/C++.
Модули расширения C/C++ относятся к категории файловых модулей. Как следует из названия, эти модули написаны на C/C++. Отличие от модулей JavaScript заключается в том, что их не нужно компилировать после загрузки. Их можно вызывать извне. после прямого выполнения, поэтому они загружаются немного быстрее, чем модули JavaScript. По сравнению с файловыми модулями, написанными на JS, модули расширения C/C++ имеют очевидные преимущества в производительности. Для функций, которые не могут быть охвачены основным модулем Node или имеют особые требования к производительности, пользователи могут писать модули расширения C/C++ для достижения своих целей.
Так что же такое файл .node
и какое отношение он имеет к модулям расширения C/C++?
Фактически, после компиляции написанного модуля расширения C/C++ создается файл .node
. Другими словами, как пользователи модуля мы представляем не исходный код модуля расширения C/C++ напрямую, а скомпилированный двоичный файл модуля расширения C/C++. Таким образом, файл .node
не нужно компилировать. После того, как Node найдет файл .node
, ему нужно только загрузить и выполнить файл. Во время выполнения объект экспорта модуля заполняется и возвращается вызывающему объекту.
Стоит отметить, что файлы .node
, созданные при компиляции модулей расширения C/C++, имеют разные формы на разных платформах: в системах *nix
модули расширения C/C++ компилируются в файлы общих объектов динамической компоновки такими компиляторами, как g++/gcc. Расширение — .so
; в Windows
оно компилируется в файл динамической библиотеки компилятором Visual C++, а расширение — .dll
. Но на самом деле мы используем расширение .node
. На самом деле расширение .node
просто для того, чтобы выглядеть более естественно. На самом деле это файл .dll в Windows
и файл .dll
в файлах *nix
.so
.
После того как Node находит требуемый файл .node
, он вызывает process.dlopen()
для загрузки и выполнения файла. Поскольку файлы .node
имеют разные формы файлов на разных платформах, для достижения кросс-платформенной реализации метод dlopen()
имеет разные реализации на платформах Windows
и *nix
, а затем инкапсулируется через уровень совместимости libuv
. На следующем рисунке показан процесс компиляции и загрузки модулей расширения C/C++ на разных платформах:
Модуль ядра компилируется в двоичный исполняемый файл в процессе компиляции исходного кода Node. Когда процесс Node запускается, некоторые основные модули загружаются непосредственно в память. Поэтому, когда эти основные модули вводятся, два этапа поиска файла, компиляции и выполнения могут быть опущены и будут оцениваться перед файловым модулем в пути. анализ, поэтому скорость загрузки самая быстрая.
Основной модуль фактически разделен на две части, написанные на C/C++ и JavaScript. Файлы C/C++ хранятся в каталоге src проекта Node, а файлы JavaScript хранятся в каталоге lib. Очевидно, что процессы компиляции и выполнения этих двух частей модулей различны.
. Для компиляции основных модулей JavaScript в процессе компиляции исходного кода Node Node будет использовать инструмент js2c.py, поставляемый с V8, для преобразования всех встроенных кодов JavaScript, включая основные модули JavaScript. в массивы C++ код JavaScript хранится в пространстве имен узла в виде строк. При запуске процесса Node код JavaScript загружается непосредственно в память.
Когда вводится основной модуль JavaScript, Node вызывает process.binding()
чтобы определить его местоположение в памяти посредством анализа идентификатора модуля и получить его. После извлечения основной модуль JavaScript также будет обернут оболочкой модуля, затем выполнен, объект экспорта будет экспортирован и возвращен вызывающему объекту.
в базовом модуле. Некоторые модули написаны на C/C++, базовая часть некоторых модулей дополнена C/C++, а другие части упаковываются или экспортируются с помощью JavaScript для удовлетворения требований к производительности. Такие модули, как buffer
, fs
, os
и т. д. частично написаны на C/C++. Эта модель, в которой модуль C++ реализует ядро внутри основной части, а модуль JavaScript реализует инкапсуляцию вне основной части, является распространенным способом повышения производительности Node.
Части основного модуля, написанные на чистом C/C++, называются встроенными модулями, например node_fs
, node_os
и т. д. Обычно они не вызываются напрямую пользователями, но напрямую зависят от основного модуля JavaScript. Поэтому в процессе внедрения основного модуля Node существует такая цепочка ссылок:
Так как же основной модуль JavaScript загружает встроенный модуль?
Помните метод process.binding()
вызывающий Node, который удаляет основной модуль JavaScript из памяти? Этот метод также применяется к основным модулям JavaScript, чтобы помочь в загрузке встроенных модулей.
Что касается реализации этого метода, при загрузке встроенного модуля сначала создайте пустой объект экспорта, затем вызовите метод get_builtin_module()
чтобы извлечь объект встроенного модуля, заполните объект экспорта, выполнив register_func()
, и, наконец, верните его вызывающей стороне для завершения экспорта. Это процесс загрузки и выполнения встроенного модуля.
Согласно приведенному выше анализу, для введения ссылочной цепочки, такой как основной модуль, на примере модуля os, общий процесс выглядит следующим образом:
Таким образом, процесс внедрения модуля os включает в себя введение файлового модуля JavaScript, загрузку и выполнение основного модуля JavaScript, а также загрузку и выполнение встроенного модуля. Этот процесс очень громоздкий и сложный, но. для вызывающей стороны модуля из-за экранирования базового элемента. Для сложных реализаций и деталей весь модуль можно импортировать просто через require(), что очень просто. дружелюбно.
В этой статье представлены основные понятия файловых модулей и основных модулей, а также их конкретные процессы и детали, на которые необходимо обратить внимание при расположении файла, компиляции или выполнении. В частности:
файловые модули можно разделить на обычные файловые модули и пользовательские модули в соответствии с различными процессами позиционирования файлов. Обычные файловые модули можно найти напрямую благодаря их ясным путям, что иногда включает в себя процесс анализа расширений файлов и анализа каталогов; пользовательские модули будут искать на основе пути к модулю, и после успешного поиска окончательное местоположение файла будет выполняться посредством анализа каталога; .
Файловые модули можно разделить на модули JavaScript и модули расширения C/C++ в соответствии с различными процессами компиляции и выполнения. После того, как модуль JavaScript упакован оболочкой модуля, он выполняется с помощью метода runInThisContext
модуля vm
, поскольку модуль расширения C/C++ уже является исполняемым файлом, созданным после компиляции, его можно выполнить напрямую и вернуть экспортированный объект; звонящему.
Основной модуль разделен на основной модуль JavaScript и встроенный модуль. Модуль ядра JavaScript загружается в память при запуске процесса Node. Его можно вынуть, а затем выполнить с помощью process.binding()
; компиляция и выполнение встроенного модуля будет идти через process.binding()
. Обработка функций get_builtin_module()
и register_func()
.
Кроме того, мы также нашли цепочку ссылок для Node, позволяющую представить основные модули, то есть файловый модуль -> основной модуль JavaScript -> встроенный модуль. Мы также узнали , что модуль C++ внутренне дополняет ядро и JavaScript. модуль реализует метод инкапсуляции извне.