Node.js 10中原生模块的未来

Node.js 10即将推出,并且有很多改进。令我们兴奋的是对本地模块库n-api的更新。它在即将发布的版本中脱离实验状态。

Node.js 10中原生模块的未来

与其他语言相比,JavaScript总是拥有最低限度的标准库。一开始,我们只在浏览器中使用JavaScript。随着浏览器逐渐发展并成熟为应用程序虚拟机,需要通过浏览器库添加更多功能。这带来了新的应用程序,如Web蓝牙,Web USB等; 不断扩展我们可以使用JavaScript的东西。

一点历史

有一次,我们意识到,由于其事件驱动性,JavaScript将成为编写高度可扩展的服务器应用程序的绝佳语言。Node.js诞生了。一个新的最小标准库,由用于编写非浏览器应用程序(如文件系统绑定,TCP堆栈,模块加载程序等)所需的一些基本功能组成。很难准确估计未来的用例,所以为了使平台更加灵活,增加了用C / C ++编写模块的能力。这使得开发人员可以充分利用其平台上提供的任何API,但仍然将其公开为JavaScript API供用户使用。

许多伟大的模块是这样写的。 LevelDB是一个嵌入式的快速数据库,它被编写为一个本地模块,它将LevelDB C ++代码与易于使用的JavaScript API相结合。LevelDB引发了一个生态系统,在其中开发了许多有趣的模块和应用程序。很少有LevelDB的用户知道模块中的C ++是如何工作的,但幸运的是,我们不需要 - 本地模块将所有这些都抽象出来。

随着越来越多的人开始使用本地模块,我们也学到了一些缺点。事实证明,它们很难维护,因为用于实现模块的V8 API变化很大。对于用户使用模块,他们需要在他们的机器上安装一个完整的编译器堆栈(在Windows上,这个过程涉及用户必须安装Visual Studio!)。

Along Came NAN

为了解决不断变化的V8 API的问题, NAN 诞生了。NAN代表“Node.js的Native Abstractions”,它是一系列抽象出不断变化的V8 API之间差异的宏。实际上,NAN最初由Rod Vagg创建, 以帮助 LevelDB 开发。这意味着您可以使用最新版本的Node.js编写本地模块,并且可以在大多数以前的版本中工作,而且不会太复杂。这也意味着在大多数较新的Node.js版本中,您的旧模块将继续编译。这是能够维护原生模块的巨大改进。

// silly NAN backed module that prints a string from c++

#include

#include

using namespace v8;

NAN_METHOD(Print) {

if (!info[0]->IsString()) return Nan::ThrowError("Must pass a string");

Nan::Utf8String path(info[0]);

printf("Printed from C++: %s\n", *path);

}

NAN_MODULE_INIT(InitAll) {

Nan::Set(target,

Nan::New("print").ToLocalChecked(),

Nan::GetFunction(Nan::New(Print)).ToLocalChecked()

);

}

NODE_MODULE(a_native_module, InitAll)

(请参阅完整的NAN示例回购)

预构建

为了避免必须安装编译工具链,在发布模块之前执行了一系列实验。预编译的二进制文件将在GitHub发布的地方在线托管,并npm install 在模块安装时通过脚本下载 。如果没有可用的预构建可用,那么 npm install脚本将像往常一样回退编译模块。

你可以在leveldown仓库中看到这个例子 。

尽管如此,还是有很多问题继续出现。偶尔,NAN将不得不做出向后不兼容的改变。这意味着旧模块不会在较新版本的Node.js上编译而不升级它们。引入新的Node.js兼容运行时间(如Electron)使情况更加复杂。使用Node.js编译的模块不会在Electron上运行,从而让用户留下不明显的错误。

网络代理和从第三方来源下载二进制文件的安全问题使预编译下载变得困难。具有讽刺意味的是,prebuilds使得Electron也很难使用。下载的预制版针对的npm install 是正在运行的Node.js版本,而不是Electron 版本 。另外,对npm install 脚本的硬性要求 也可能是一个安全问题,因为用户禁用这些脚本以避免 运行npm蠕虫。

现在

NAN和prebuilds使事情变得更好; 但仍然不如我们想要的那么好。本地模块仍然被认为是“昂贵”的依赖关系,并且经常用作参数来包含Node.js核心与npm模块中的某些内容。

幸运的是,事情正在迅速改善,我们已经处于现在的阶段,我们拥有编写和发布本地模块的工具,用户可以在很少或没有技术开销的情况下安装它们。

N-API

为了使本地模块更易于编写和维护,Node.js核心贡献者一直在开发一种新的核心API,称为n-api(或node-api)。

n-api背后的想法是在您编写本机模块的V8 API上构建一个稳定的接口。这种方法引入了一系列好处:

不需要重新编译模块,因为接口永远不会中断(将其视为系统调用,但是对于Node.js)。

允许V8以外的JavaScript引擎执行n-api。

只要实现n-api,就可以在Electron和其他运行时使用本机模块。

N-api的性能影响很小,因为您必须通过一个纤细的抽象层而不是原始的V8代码。但是,它有 很好的 文档。

一段时间以来,n-api一直是一个非实验性的API。它将在Node.js 10中被释放,并且后端将会到达Node.js 8和6(虽然现在是6的实验)。

这里是我们的NAN例子,从上面移植到n-api:

// silly n-api backed module that prints a string from c

#include

#include

napi_value print (napi_env env, napi_callback_info info) {

napi_value argv[1];

size_t argc = 1;

napi_get_cb_info(env, info, &argc, argv, NULL, NULL);

if (argc < 1) {

napi_throw_error(env, "EINVAL", "Too few arguments");

return NULL;

}

char str[1024];

size_t str_len;

if (napi_get_value_string_utf8(env, argv[0], (char *) &str, 1024, &str_len) != napi_ok) {

napi_throw_error(env, "EINVAL", "Expected string");

return NULL;

}

printf("Printed from C: %s\n", str);

return NULL;

}

napi_value init_all (napi_env env, napi_value exports) {

napi_value print_fn;

napi_create_function(env, NULL, 0, print, NULL, &print_fn);

napi_set_named_property(env, exports, "print", print_fn);

return exports;

}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init_all)

(查看完整的n-api示例回购, 并 查看Node.js核心中的示例)

除了C API之外,还有更高级别的C ++包装器,也称为 node-addon-api。C ++包装器是由n-API合作者维护的npm模块。使用C ++包装器,您可以获得支持,直到Node.js 4为止,因为它具有旧版本的兼容性层。

通常,在封装C接口时使用C API,而在封装C ++ API时使用C ++ API。

捆绑预制

使用n-api支持预编译变得更容易。由于n-api具有稳定的API,因此我们可以为Node.js 10预先生成一个模块,并且它可以在Node.js 11,12和更新版本上运行。

为了解决在安装时不得不下载预构建的问题,我和其他一些贡献者最近发布了一组模块,称为 prebuildify, node- gyp -build和 prebuildify-ci。

  1. prebuildify 将预先建立你的模块

  2. node- gyp -build可以在安装时测试预构建,并支持使用JavaScript API从磁盘加载预构建。

  3. prebuildify-ci 可帮助您在ci上设置prebuildify,以便为Linux 32/64位,MacOS和Windows 32/64位自动构建。

其他预建模块存在于npm上。那么他们与prebuildify有什么不同呢?使用prebuildify而不是在安装时为您的平台下载预构建,我们只需在node_modules 发布之前将./prebuilds文件夹中的所有平台的所有预构建绑定 到npm。在安装时,我们使用node-gyp-build来简单测试模块中捆绑的任何预构建组件是否可以加载到平台上。如果不是的话,我们通常会把编译器工具链叫做npm。

如果出于安全原因禁用安装脚本,预构建仍然会在运行时加载。安装脚本只是为了测试它是否工作。

当我们第一次尝试这种方法时,我们prebuildify的合作者担心在node_modules 文件夹内添加多个预构建的脚印 会由于更大的包装大小而使安装速度变慢。具有讽刺意味的是,它实际上使我们移植的所有模块能够更快地安装。下载所需的下载特定预构建的所有依赖通常需要更长的时间,而不是简单地将它们一起下载,并与模块的其余部分一起下载。

将prebuildify与n-api结合在一起非常合适。N-API意味着你需要做很少的预构建 - 每个平台需要一个平台来支持每个平台和一个Node.js版本。当发布新的Node.js版本时,您不需要发布新模块。

您可以在n-api示例回购中看到如何使用prebuildify与n-api和ci的完整示例 。

在您正在开发的平台以外的平台上构建预构建可能有点乏味。这就是为什么我们创建了prebuildify-ci; 它会设置 travis 和 appveyor 来为新版本添加标签时构建模块。

1.首先设置你的模块。运行 prebuildify-ci init 将设置一个 appveyor.yml 与 travis.yml 文件,当它被标记的是你的预构建模块。在成功构建之后,它会暂时将预构建体上传到GitHub发布版,以便在发布模块之前从那里下载。

prebuildify-ci init

2.git push 一个标签发布(并且不会在npm上发布)。

git commit -am "new cool stuff"

npm version minor

git push && git push --tags

3.等待ci完成。

4.从GitHub下载发行版。

prebuildify-ci download

这应该下载并提取预构建 ./prebuilds。

5.只需将预编译模块发布到npm即可。

npm publish

就是这么简单。

未来

对于本地模块,未来是光明的。在一年之内,所有活跃的Node.js发行版都将支持n-api。

原生模块可以非常强大,并且使我们能够模块化Node.js内核,因为它允许像 替代的tcp堆栈, 有效的bloomfilters和 现代的加密库一样 在内核之外构建,而不会强制用户在安装时进行编译。这将帮助我们继续将诸如LevelDB和其他非JavaScript项目的伟大事物带入Node.js生态系统。


分享到:


相關文章: