Появление Node.js освободило JavaScript от оков браузеров и открыло широкие возможности для серверной разработки. Введение Node.js в модульную спецификацию CommonJS сделало JavaScript языком, который действительно можно адаптировать к крупномасштабным проектам.
Использование модулей в Node.js очень просто, и у нас почти есть этот опыт в нашей ежедневной разработке: написать фрагмент кода JavaScript, потребовать несколько нужных пакетов, а затем экспортировать экспорт продукта кода. Однако знаем ли мы что-нибудь о принципах загрузки и работы, лежащих в основе модульности Node.js? Сначала задайте следующие вопросы:
- Какие типы файлов поддерживаются модулями в Node.js?
- В чем разница между процессом загрузки и запуска основных модулей и сторонних модулей?
- Как написать модуль расширения C/C++, отличный от модуля JavaScript?
- ...
В этой статье я объединим исходный код Node.js, чтобы изучить ответы на эти вопросы.
1. Тип модуля Node.js
В Node.js модули в основном можно разделить на следующие типы:
- Базовый модуль: содержится в исходном коде Node.js и скомпилирован в исполняемый двоичный файл Node.js Модуль JavaScript, также называемый собственным модулем, например широко используемый http,
фс и т.д. - Модули C/C++, также называемые встроенными модулями, обычно не вызываются напрямую, а вызываются в собственных модулях, и тогда нам требуется
- Нативные модули, такие как buffer, fs, os и другие нативные модули, которые мы обычно используем в Node.js, все вызывают встроенные модули внизу.
- Сторонние модули: Модули, которые поставляются с исходным кодом, отличным от Node.js, могут в совокупности называться сторонними модулями, такими как экспресс, веб-пакет и т. д.
- Модули JavaScript, это самые распространенные, обычно мы пишем модули JavaScript при разработке
- Модуль JSON, это очень просто, просто файл JSON
- Модуль расширения C/C++, написанный на C/C++, с суффиксом .node после компиляции.
В этой статье мы рассмотрим принципы загрузки и работы вышеуказанных модулей один за другим.
2. Краткий обзор структуры исходного кода Node.js
Здесь мы используем исходный код версии Node.js 6.x в качестве примера для анализа. Перейдите на github, чтобы загрузить соответствующую версию исходного кода Node.js, вы можете увидеть, что общая структура кода выглядит следующим образом:
├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── deps
├── doc
├── lib
├── node.gyp
├── node.gypi
├── src
├── test
├── tools
└── vcbuild.bat
в:
-
./lib
Папка в основном содержит различные файлы JavaScript, и все наши часто используемые нативные модули JavaScript находятся здесь. -
./src
Папка в основном содержит исходные файлы C/C++ для Node.js, многие из которых являются здесь встроенными модулями. -
./deps
Папка содержит различные библиотеки, от которых зависит Node.js, обычно v8, libuv, zlib и т. д.
Релизная версия, которую мы используем в разработке, на самом деле представляет собой исполняемый файл, скомпилированный из исходного кода. Если мы хотим внести некоторые персонализированные настройки в Node.js, мы можем изменить исходный код, а затем запустить и скомпилировать, чтобы получить настроенную версию Node.js. Взяв за пример платформу Linux, вот краткое введение в процесс компиляции Node.js.
Прежде всего, нам необходимо знать организационные инструменты, используемые для составления, а именноgyp
. В исходном коде Node.js мы видимnode.gyp
, содержимое этого файла представляет собой некоторую JSON-подобную конфигурацию, написанную на python, которая определяет ряд задач строительного проекта. Давайте возьмем пример, где одно из полей выглядит следующим образом:
{
'target_name': 'node_js2c',
'type': 'none',
'toolsets': ['host'],
'actions': [
{
'action_name': 'node_js2c',
'inputs': [
'<@(library_files)',
'./config.gypi',
],
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/node_natives.h',
],
'conditions': [
[ 'node_use_dtrace=="false" and node_use_etw=="false"', {
'inputs': [ 'src/notrace_macros.py' ]
}],
['node_use_lttng=="false"', {
'inputs': [ 'src/nolttng_macros.py' ]
}],
[ 'node_use_perfctr=="false"', {
'inputs': [ 'src/perfctr_macros.py' ]
}]
],
'action': [
'python',
'tools/js2c.py',
'<@(_outputs)',
'<@(_inputs)',
],
},
],
}, # end node_js2c
Основная роль этой задачи исходит из названияnode_js2c
Видно, что это преобразование JavaScript в код C/C++. Мы обратимся к этой задаче ниже.
Сначала скомпилируйте Node.js, вам нужно заранее установить некоторые инструменты:
- gcc и g++ 4.9.4 и выше
- лязг и лязг++
- Python 2.6 или 2.7, здесь следует отметить, что это могут быть только эти две версии, а не python 3+
- GNU MAKE 3.81 и выше
С помощью этих инструментов, чтобы войти в исходный каталог Node.js, нам просто нужно последовательно выполнить следующие команды:
./configuration
make
make install
Вы можете скомпилировать и сгенерировать исполняемый файл и установить его.
3. Отnode index.js
Начинать
Начнем с самого простого случая. Предположим, есть файл index.js, содержащий только одну простую строку.console.log('hello world')
код. при входеnode index.js
js, как Node.js компилирует и запускает этот файл?
При вводе команды Node.js вызывается основная функция в исходном коде Node.js.src/node_main.cc
середина:
// src/node_main.cc
#include "node.h"
#ifdef _WIN32
#include <VersionHelpers.h>
int wmain(int argc, wchar_t *wargv[]) {
// windows下面的入口
}
#else
// UNIX
int main(int argc, char *argv[]) {
// Disable stdio buffering, it interacts poorly with printf()
// calls elsewhere in the program (e.g., any logging from V8.)
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
// 关注下面这一行
return node::Start(argc, argv);
}
#endif
Этот файл предназначен только для записи и различает среды Windows и Unix. Возьмем для примера Unix, в основной функции последний вызовnode::Start
, это вsrc/node.cc
В файле:
// src/node.cc
int Start(int argc, char** argv) {
// ...
{
NodeInstanceData instance_data(NodeInstanceType::MAIN,
uv_default_loop(),
argc,
const_cast<const char**>(argv),
exec_argc,
exec_argv,
use_debug_agent);
StartNodeInstance(&instance_data);
exit_code = instance_data.exit_code();
}
// ...
}
// ...
static void StartNodeInstance(void* arg) {
// ...
{
Environment::AsyncCallbackScope callback_scope(env);
LoadEnvironment(env);
}
// ...
}
// ...
void LoadEnvironment(Environment* env) {
// ...
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
if (try_catch.HasCaught()) {
ReportException(env, try_catch);
exit(10);
}
// The bootstrap_node.js file returns a function 'f'
CHECK(f_value->IsFunction());
Local<Function> f = Local<Function>::Cast(f_value);
// ...
f->Call(Null(env->isolate()), 1, &arg);
}
Весь файл относительно длинный.В приведенном выше сегменте кода перехватывается только тот сегмент процесса, на который нам нужно обратить внимание больше всего.Отношения вызова следующие:Start -> StartNodeInstance -> LoadEnvironment
.
существуетLoadEnvironment
Это требует нашего внимания, главное, чтобы вытащитьbootstrap_node.js
Строка кода в , преобразованная в функцию и, наконец, переданнаяf->Call
выполнить.
Хорошо, вот и настал момент, мы наконец-то видим первый файл JavaScript с момента запуска Node.js.bootstrap_node.js
, мы также можем видеть из имени файла, что это файл начального уровня. Итак, давайте посмотрим, путь к файлуlib/internal/bootstrap_node.js
:
// lib/internal/boostrap_node.js
(function(process) {
function startup() {
// ...
else if (process.argv[1]) {
const path = NativeModule.require('path');
process.argv[1] = path.resolve(process.argv[1]);
const Module = NativeModule.require('module');
// ...
preloadModules();
run(Module.runMain);
}
// ...
}
// ...
startup();
}
// lib/module.js
// ...
// bootstrap main module.
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
// ...
Здесь мы все же уделяем внимание основному процессу, как видите,bootstrap_node.js
, выполняетstartup()
функция. пройти черезprocess.argv[1]
получить имя файла, в нашемnode index.js
середина,process.argv[1]
очевидноindex.js
, а затем позвонитеpath.resolve
Разберите путь к файлу. в конце,run(Module.runMain)
для компиляции и выполнения нашегоindex.js
.
а такжеModule.runMain
функция определена вlib/module.js
, в конце приведенного выше фрагмента кода указана эта функция, и видно, что в основном она вызываетсяModule._load
загрузить и выполнитьprocess.argv[1]
.
Когда мы анализируем требования модуля ниже, мы также придем кlib/module.js
, также будет анализироватьModule._load
.Таким образом, мы видим, что процесс Node.js, запускающий файл, фактически в конце такжеrequire
Процесс работы с файлом можно понимать как немедленное требование файла.Разберем принцип require.
4. Ключ к принципу загрузки модуля: требуется
Идем дальше, полагая, что нашаindex.js
Имеет следующее:
var http = require('http');
Так что же происходит, когда эта строка кода выполняется?
Определение require все еще существуетlib/module.js
середина:
// lib/module.js
// ...
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
return Module._load(path, this, /* isMain */ false);
};
// ...
require
Методы определены в цепочке прототипов модуля. Вы можете видеть, что в этом методе вызовModule._load
.
Мы снова здесь так скороModule._load
Давайте посмотрим, что делает этот ключевой метод:
// lib/module.js
// ...
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];
if (cachedModule) {
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;
};
// ...
Поток этого кода относительно ясен, в частности:
- По имени файла вызовите
Module._resolveFilename
Разобрать путь к файлу - Кэш просмотра
Module._cache
Есть ли этот модуль в, если да, вернитесь напрямую - пройти через
NativeModule.nonInternalExists
Определите, является ли модуль основным модулем, если это основной модуль, вызовите метод загрузки основного модуля.NativeModule.require
- Если это не основной модуль, создайте новый объект модуля и вызовите
tryModuleLoad
функциональный загрузочный модуль
Давайте сначала посмотрим наModule._resolveFilename
, понимание этого метода очень полезно для понимания принципа разбора пути к файлу в Node.js:
// lib/module.js
// ...
Module._resolveFilename = function(request, parent, isMain) {
// ...
var filename = Module._findPath(request, paths, isMain);
if (!filename) {
var err = new Error("Cannot find module '" + request + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};
// ...
существуетModule._resolveFilename
позвонил вModule._findPath
, логика оценки загрузки модуля фактически сосредоточена в этом методе.Поскольку этот метод длинный, непосредственно прикрепите код метода github:
Видно, что логический поток разбора пути к файлу выглядит следующим образом:
- Сначала сгенерируйте cacheKey, определите, существует ли соответствующий кеш, и вернитесь напрямую, если он существует.
- если последний символ пути не
/
:
- Если путь является файлом и существует, вернуть путь к файлу напрямую
- Если путь является каталогом, вызовите
tryPackage
функция для анализа каталогаpackage.json
, а затем удалитеmain
Путь к файлу, в который записывается поле
- Если путь суждения существует, вернитесь напрямую
- Попробуйте добавить три суффикса .js, .json, .node к пути, чтобы определить, существует ли он, и вернуться, если он существует.
- Попробуйте добавить index.js, index.json, index.node после пути, чтобы определить, существует ли он, и вернуться, если он существует.
- Если это не удалось, попробуйте добавить суффикс .js, .json, .node непосредственно к текущему пути.
- Если последний символ пути
/
:
- перечислить
tryPackage
, процесс синтаксического анализа аналогичен описанному выше - Если это не удалось, попробуйте последовательно добавить index.js, index.json, index.node к пути, чтобы определить, существует ли он, и вернуться, если он существует.
используется при разборе файловtryPackage
а такжеtryExtensions
ссылка на github метода:
GitHub.com/node будет /node… GitHub.com/node будет /node…
Весь процесс можно представить на следующей картинке:
После завершения анализа пути к файлу проверьте, существует ли кеш в соответствии с путем к файлу, и вернитесь напрямую, если он существует.Если он не существует, перейдите к шагу 3 или 4.
Здесь сгенерированы две ветки 3, 4, то есть основной модуль и метод загрузки стороннего модуля отличаются. Так как мы предполагаем насindex.js
Чжунвэйvar http = require('http')
, http — это основной модуль, поэтому давайте сначала проанализируем эту ветку, загруженную основным модулем.
4.1 Принцип загрузки основного модуля
Основной модуль черезNativeModule.require
Загружен, NativeModule определен вbootstrap_node.js
, прикрепите ссылку на гитхаб:
GitHub.com/node будет /node…
Как видно из кода,NativeModule.require
Процесс выглядит следующим образом:
- Определить, был ли загружен кеш, и если да, вернуть экспорт напрямую
- Создайте новый объект nativeModule, затем кэшируйте, загрузите и скомпилируйте
Во-первых, давайте посмотрим, как скомпилировать, из кода это называетсяcompile
метод, находясь вNativeModule.prototype.compile
метод, сначала поNativeModule.getSource
Получен исходный код загружаемого модуля, так как же получить исходный код? посмотриgetSource
Определение метода:
// lib/internal/bootstrap_node.js
// ...
NativeModule._source = process.binding('natives');
// ...
NativeModule.getSource = function(id) {
return NativeModule._source[id];
};
Непосредственно изNativeModule._source
Получил, а где это присваивается? Он также перехватывается в приведенном выше коде черезNativeModule._source = process.binding('natives')
полученный.
Вот введение в то, как хранится собственный код модуля JavaScript. Когда исходный код Node.js скомпилирован, инструмент js2c.py, входящий в состав v8, будет использоваться для преобразования кода модуля js в папке lib в массив на языке C, и будет создан заголовочный файл node_natives.h для запишите массив:
namespace node {
const char node_native[] = {47, 47, 32, 67, 112 …}
const char console_native[] = {47, 47, 32, 67, 112 …}
const char buffer_native[] = {47, 47, 32, 67, 112 …}
…
}
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 },
{“console”, console_native, sizeof(console_native)-1 },
{“buffer”, buffer_native, sizeof(buffer_native)-1 },
…
}
в то время как вышеNativeModule._source = process.binding('natives');
Функция состоит в том, чтобы вынуть этот массив туземцев и назначить егоNativeModule._source
, так что вgetSource
можно напрямую использовать имя модуля в качестве индекса для извлечения исходного кода модуля из массива.
Здесь мы вставляем обзор вышесказанного, когда мы представили компиляцию Node.js, мы представилиnode.gyp
, и одна из задачnode_js2c
В то время автор упомянул, что, судя по названию, эта задача заключается в преобразовании JavaScript в код C, а код C в массиве нативов здесь является продуктом этой задачи построения. И вот, наконец, мы знаем роль этой задачи компиляции.
Знайте получение исходного кода, продолжайте смотреть внизcompile
метод, чтобы увидеть, как компилируется исходный код:
// lib/internal/bootstrap_node.js
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
this.loading = true;
try {
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
} finally {
this.loading = false;
}
};
// ...
NativeModule.prototype.compile
После получения исходного кода он в основном делает:wrap
Метод обрабатывает исходный код, затем вызывает runInThisContext для компиляции функции и, наконец, выполняет функцию. Метод обертывания заключается в добавлении головы и хвоста к исходному коду, что фактически эквивалентно обертыванию исходного кода в функцию, параметры которой — экспорт, требование, модуль и так далее. Вот почему нам не нужно определять экспорт, требование, модуль, когда мы пишем модули
можно использовать напрямую.
До сих пор процесс загрузки основного модуля Node.js был в основном объяснен. Говоря об этом, у вас могут возникнуть сомнения: вышеприведенный процесс анализа, по-видимому, задействует только нативный модуль JavaScript в основном модуле, а как насчет встроенного модуля C/C++?
На самом деле для встроенных модулей они импортируются не через require, а черезprecess.binding('模块名')
представил. Как правило, мы редко используем его непосредственно в собственном коде.process.binding
импортировать встроенные модули, но черезrequire
На собственный модуль ссылаются, а встроенный модуль вводится в собственный модуль. Например, наш часто используемый буферный модуль C/C++ вводится в его внутреннюю реализацию.
встроенный модуль, чтобы обойти ограничение памяти v8:
// lib/buffer.js
'use strict';
// 通过 process.binding 引入名为 buffer 的 C/C++ built-in 模块
const binding = process.binding('buffer');
// ...
Таким образом, мыrequire('buffer')
Фактически встроенный модуль C/C++ используется косвенно.
Снова здесьprocess.binding
! На самом деле метод process.binding определен вnode.cc
середина:
// src/node.cc
// ...
static void Binding(const FunctionCallbackInfo<Value>& args) {
// ...
node_module* mod = get_builtin_module(*module_v);
// ...
}
// ...
env->SetMethod(process, "binding", Binding);
// ...
Binding
Ключевым шагом в этой функции являетсяget_builtin_module
. Здесь нам нужно снова вставить, чтобы представить метод хранения встроенных модулей C/C++:
В Node.js встроенные модули создаются с помощьюnode_module_struct
определяется структурой. Таким образом, встроенные модули помещаются в файл с именемnode_module_list
в массиве. а такжеprocess.binding
роль использованияget_builtin_module
Получите соответствующий встроенный код модуля из этого массива.
Таким образом, мы полностью представили принцип загрузки основных модулей, в основном различая нативные модули типа JavaScript и встроенные модули типа C/C++. Вот картинка, описывающая процесс загрузки основного модуля:
Вспоминая то, что мы представили в начале, нативный модуль хранится в каталоге lib/ в исходном коде, а встроенный модуль хранится в исходном коде в каталоге src/ На следующем рисунке сортируются нативные и построенные из перспектива компиляции Как -in модули компилируются в исполняемые файлы Node.js:
4.2 Принцип загрузки сторонних модулей
Перейдём ко второй ветке, предполагая, что нашаindex.js
Требуется не http, а пользовательский модуль, тогда в module.js мы перейдем к методу tryModuleLoad:
// lib/module.js
// ...
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
// ...
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));
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};
// ...
посмотреть здесь,tryModuleLoad
на самом деле называетсяModule.prototype.load
Определенный метод, главное в этом методе - определить расширение имени файла, а затем вызвать разные расширения для разных расширений.Module._extensions
метод для загрузки и компиляции модулей. Тогда давайте посмотримModule._extensions
:
// lib/module.js
// ...
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(internalModule.stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path._makeLong(filename));
};
// ...
Видно, что всего поддерживается три типа загрузки модулей: .js, .json и .node. Среди них метод загрузки файла типа .json является самым простым, непосредственное чтение содержимого файла, а затемJSON.parse
Затем верните объект.
Давайте посмотрим на обработку .js, в первую очередь черезfs
Модуль синхронно считывает содержимое файла, а затем вызываетmodule._compile
, посмотрите на соответствующий код:
// lib/module.js
// ...
Module.wrap = NativeModule.wrap;
// ...
Module.prototype._compile = function(content, filename) {
// ...
// create wrapper function
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
// ...
var result = compiledWrapper.apply(this.exports, args);
if (depth === 0) stat.cache = null;
return result;
};
// ...
первый звонокModule.wrap
Оберните исходный код, а затем вызовитеvm.runInThisContext
Метод компилируется и выполняется и, наконец, возвращает значение экспорта. и изModule.wrap = NativeModule.wrap
Из этого предложения видно, что метод переноса стороннего модуля такой же, как метод переноса базового модуля. Давайте вспомним только что упомянутый код ключа загрузки основного js-модуля:
// lib/internal/bootstrap_node.js
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
this.loading = true;
try {
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
} finally {
this.loading = false;
}
};
Сравнивая хэтчбеки, выясняется, что компиляция и выполнение исходного кода между ними практически идентичны. С точки зрения общего процесса, самая большая разница между основным модулем JavaScript и сторонним модулем JavaScript заключается в том, что исходный код основного модуля JavaScript передается черезprocess.binding('natives')
извлекается из памяти, а исходный код стороннего модуля JavaScriptfs.readFileSync
способ чтения из файла.
Наконец, давайте рассмотрим загрузку сторонних модулей C/C++ (суффикс .node). Интуитивно это очень просто, просто позвонитеprocess.dlopen
метод. Этот метод определен вnode.cc
середина:
// src/node.cc
// ...
env->SetMethod(process, "dlopen", DLOpen);
// ...
void DLOpen(const FunctionCallbackInfo<Value>& args) {
// ...
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
// ...
}
// ...
на самом деле звонитDLOpen
функция, важнейшая из которых заключается в использованииuv_dlopen
Метод открывает библиотеку динамической компоновки, а затем загружает модуль C/C++.uv_dlopen
метод определен вlibuv
в библиотеке.libuv
Библиотека представляет собой кроссплатформенную библиотеку асинхронного ввода-вывода. Для функции динамической загрузки модулей расширения под платформой *nix фактически вызывается метод dlopen(), определенный в dlfcn.h, а под Windows
Ниже это метод LoadLibraryExW().Под двумя платформами они загружают файлы .so и .dll соответственно, в то время как в Node.js эти файлы имеют одинаковые имена с суффиксом .node, который скрывает различия платформ.
оlibuv
Библиотеки являются основной движущей силой асинхронного ввода-вывода в Node.js. Сама эта часть достойна изучения как отдельная тема, поэтому я не буду обсуждать ее здесь.
На данный момент мы прояснили процесс загрузки и компиляции трех сторонних модулей.
5. Сценарии разработки и применения модулей расширения C/C++
Выше был проанализирован процесс загрузки различных модулей в Node.js. Все должны быть знакомы с разработкой модулей JavaScript, но они могут быть незнакомы с разработкой модулей расширения C/C++. В этом разделе кратко представлена разработка модуля расширения и рассказывается о сценариях его применения.
Что касается разработки модулей расширения Node.js, в документации официального сайта Node.js есть специальный раздел.Вы можете перейти на документацию официального сайта, чтобы просмотреть:узел будет .org/docs/latest…. Вот просто пример приветствия, чтобы представить некоторые из наиболее важных концепций написания модулей расширения:
Предположим, мы хотим расширить модуль, чтобы получить функцию, эквивалентную следующей функции JavaScript:
module.exports.hello = () => 'world';
Сначала создайтеhello.ccфайл, напишите следующий код:
// hello.cc
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo
Несмотря на то, что файл короткий, некоторые коды, с которыми мы относительно незнакомы, уже появились, и здесь мы представим их один за другим, что очень полезно для понимания базовых знаний о модулях расширения.
Впервые представлен в началеnode.h
, это заголовочный файл, который необходимо использовать при написании расширений Node.js.Он содержит почти все необходимые нам библиотеки и типы данных.
Во-вторых, я видел многоusing v8:xxx
такой код. Мы знаем, что Node.js основан на движке v8, а движок v8 написан на C++. Если мы хотим разработать модули расширения C++, нам нужно использовать многие типы данных, представленные в v8, и эта серия кодов заявляет, что нам нужно использовать эти типы данных в пространстве имен v8.
Тогда см.Method
метод, его тип параметраFunctionCallbackInfo<Value>& args
, этот аргумент является параметром, переданным из JavaScript, в то же время, если вы хотитеMethod
В возвращаемой переменной JavaScript вам нужно вызватьargs.GetReturnValue().Set
метод.
Далее вам нужно определить метод инициализации модуля расширения, вотInit
функция, только одно простое предложениеNODE_SET_METHOD(exports, "hello", Method);
, что означает присвоение имени экспортуhello
метод, конкретное определение этого методаMethod
функция.
И, наконец, определение макроса:NODE_MODULE(NODE_GYP_MODULE_NAME, init)
, первый параметр — имя нужного модуля расширения, а второй параметр — метод инициализации модуля.
Чтобы скомпилировать этот модуль, нам нужно установить его через npm.node-gyp
инструмент сборки. Инструмент сочетает в себеgyp
Инструмент-оболочка для создания расширений Node.js. После установки этого инструмента мы добавляем файл с именемbingding.gyp
Конфигурационный файл, для нашего примера файл нужно только написать так:
{
"targets": [
{
"target_name": "addon",
"sources": [ "hello.cc" ]
}
]
}
Таким образом, бегиnode-gyp build
Модуль расширения может быть скомпилирован. Во время этого процессаnode-gyp
Он также отправится в указанный каталог (обычно ~/.node-gyp) для поиска некоторых заголовочных файлов и файлов библиотек нашей текущей версии Node.js.Если он не существует, он также поможет нам перейти к файлу Node. js официальный сайт для загрузки. Таким образом, при написании расширения передайте#include <>
, мы можем напрямую использовать все заголовочные файлы Node.js.
Если компиляция прошла успешно, он будет в текущей папкеbuild/Release/
увидеть путьaddon.node
, это наш скомпилированный требуемый модуль расширения.
Из вышеприведенного примера мы можем примерно увидеть режим работы модуля расширения, он может получать параметры из JavaScript, а затем в середине можно вызывать возможности языка C/C++ для выполнения различных операций и обработки, и, наконец, результаты могут быть возвращены в JavaScript.
Стоит отметить, что разные версии Node.js зависят от разных версий v8, что приводит к различиям во многих API, поэтому в процессе разработки расширений с использованием нативного C/C++ также требуется обработка совместимости для разных версий Node. js. Например, чтобы объявить функцию, в версиях ниже v6.x и v0.12 нужно написать:
Handle<Value> Example(const Arguments& args); // 0.10.x
void Example(FunctionCallbackInfo<Value>& args); // 6.x
Видно, что объявление функции, в том числе и способ записи параметров в функцию, не совпадают. Это заставляет людей думать, что при разработке Node.js для написания ES6 также необходимо использовать Babel, чтобы помочь с преобразованием совместимости. Итак, в области разработки расширений Node.js есть ли такая библиотека, как Babel, которая помогает нам решать проблемы совместимости? Ответ — да, его имя — NAN (Native Abstraction for Node.js). По сути, это набор макросов, которые помогают нам обнаруживать разные версии Node.js и вызывать разные API. Например, с помощью NAN, объявляя функцию, нам больше не нужно об этом думать Node.js, вам просто нужно написать такой фрагмент кода:
#include <nan.h>
NAN_METHOD(Example) {
// ...
}
Макрос NAN будет автоматически оцениваться во время компиляции, и разные результаты будут расширены в соответствии с разными версиями Node.js, что решит проблему совместимости. Для более подробного ознакомления с NAN заинтересованные студенты могут перейти на домашнюю страницу проекта на github:github.com/nodejs/nan.
После знакомства с разработкой такого большого количества модулей расширения некоторые студенты могут спросить, кажется, что функции, реализованные этими модулями расширения, можно быстро реализовать с помощью js, так зачем же разрабатывать расширения? Возникает вопрос: применимые сценарии для расширений C/C++.
Автор грубо суммирует несколько типов сценариев, в которых применим C/C++:
- Приложения с интенсивными вычислениями. Мы знаем, что модель программирования Node.js представляет собой один поток + асинхронный ввод-вывод, в котором один поток делает его слабым местом в приложениях с интенсивными вычислениями.Большое количество вычислений блокирует основной поток JavaScript, что приводит к невозможности ответа на другие запросы. Для этого сценария вы можете использовать модуль расширения C/C++ для ускорения вычислений, ведь скорость выполнения движка v8 хоть и высока, но все же уступает C/C++. Кроме того, использование C/C++ также может позволить нам открыть многопоточность и избежать блокировки основного потока JavaScript.В сообществе уже есть несколько многопоточных решений Node.js, основанных на модулях расширения, и самые популярные из них можно назвать
thread-a-gogo
, вы можете перейти на github для получения подробной информации:GitHub.com/hardwork/node-Тянь Хайронг…. - Приложения, потребляющие много памяти. Node.js основан на v8, а v8 изначально разрабатывался для браузеров, поэтому он имеет строгие ограничения по памяти, поэтому для некоторых приложений, требующих большой памяти, может быть несколько бессильно напрямую основываться на v8. В настоящее время вам нужно чтобы использовать модуль расширения, чтобы обойти ограничение памяти версии 8. Наиболее типичным из них является наш часто используемый модуль buffer.js, нижний уровень которого также вызывает C++ и применяется для памяти на уровне C++, чтобы избежать узкого места памяти v8.
Что касается первого пункта, автор также использует собственные расширения Node.js и Node.js для реализации тестового примера для сравнения производительности вычислений. Тестовый пример представляет собой классический расчет последовательности Фибоначчи.Во-первых, используйте собственный язык Node.js для реализации функции для расчета последовательности Фибоначчи, названной какfibJs
:
function fibJs(n) {
if (n === 0 || n === 1) {
return n;
}
else {
return fibJs(n - 1) + fibJs(n - 2);
}
}
Затем используйте C++ для написания функции расширения, которая реализует ту же функцию с именемfibC
:
// fibC.cpp
#include <node.h>
#include <math.h>
using namespace v8;
int fib(int n) {
if (n == 0 || n ==1) {
return n;
}
else {
return fib(n - 1) + fib(n - 2);
}
}
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
int n = args[0]->NumberValue();
int result = fib(n);
args.GetReturnValue().Set(result);
}
void init(Local < Object > exports, Local < Object > module) {
NODE_SET_METHOD(module, "exports", Method);
}
NODE_MODULE(fibC, init)
В тесте используйте эти две функции для вычисления последовательности Фибоначчи от 1 до 40:
function testSpeed(fn, testName) {
var start = Date.now();
for (var i = 0; i < 40; i++) {
fn(i);
}
var spend = Date.now() - start;
console.log(testName, 'spend time: ', spend);
}
// 使用扩展模块测试
var fibC = require('./build/Release/fibC'); // 这里是扩展模块编译产物的存放路径
testSpeed(fibC, 'c++ test:');
// 使用 JavaScript 函数进行测试
function fibJs(n) {
if (n === 0 || n === 1) {
return n;
}
else {
return fibJs(n - 1) + fibJs(n - 2);
}
}
testSpeed(fibJs, 'js test:');
// c++ test: spend time: 1221
// js test: spend time: 2611
После нескольких тестов среднее время, затрачиваемое на модули расширения, составляет около 1,2 с, а время, затрачиваемое на модули JavaScript, составляет около 2,6 с. Видно, что в этом сценарии производительность расширения C/C++ по-прежнему намного выше.
Конечно, эти пункты основаны только на понимании автора. В реальном процессе разработки, когда вы сталкиваетесь с проблемами, вы также можете попытаться подумать, можно ли решить проблему лучше, если вы используете модуль расширения C/C++.
Эпилог
Прочитав статью, вернемся назад и посмотрим на поставленные в начале вопросы, были ли на них даны ответы в ходе анализа статьи? Давайте еще раз рассмотрим логику этой статьи:
- сначала с
node index.js
Принцип работы начинается с того, что с помощьюnode
Запуск файла эквивалентен немедленному однократному выполнению.require
. - Затем вводится метод require в узле, где выделяются основные модули, встроенные модули и неосновные модули, а также подробно описываются принципы процесса загрузки и компиляции. В этом процессе он также включает описание точек знаний, таких как разрешение пути к модулю, кэширование модуля и так далее.
- Наконец, вводится разработка модулей расширения c/c++, с которыми вы не знакомы, и используется пример сравнения производительности, чтобы проиллюстрировать его применимые сценарии.
На самом деле, изучение процесса загрузки модуля Node.js поможет нам глубже понять основные принципы работы Node.js, а освоение разработки модулей расширения и обучение их использованию в соответствующих сценариях позволит нам Разработанное приложение Node.js имеет более высокую производительность.
Изучение принципов Node.js — долгий путь. Читателям, которые понимают базовый модульный механизм, рекомендуется узнать больше о v8, libuv и т. д., что будет очень полезно для опытных пользователей Node.js.