Node.js 是一个开源的、跨平台的JavaScript运行时环境,它允许开发者在服务器端运行JavaScript代码。Node.js 是基于Chrome V8引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。
本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析到内置模块新增,从性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能问题剖析,再到 ABI 稳定的理解、基于 V8 封装 JavaScript 运行时、模块加载方式探究、内置模块外置以及 Node.js addon 的全面解读等主题,每一篇都干货满满。
在上一节中我们探讨了使用 Chrome DevTools 分析 Node.js 性能问题,在本节中则主要分享 Node.js 中的 ABI 稳定相关内容,本文内容为本系列第5篇,以下为正文内容。
前言
首先,我们需要明确的是,并非所有 node 用户都需要关注"ABI 稳定"这一概念。
如果一个用户对 node 的使用,只包含纯 JS 代码,那么用户没必要理解这个概念。 但是如果直接或间接使用了 V8 ABI,例如使用 addon、libnode、electron 等技术,就需要对这个概念有所理解。
本文主要以 addon 场景为例,对这个概念进行介绍。
创造 ABI 版本号的动机
先捋清楚一个疑问:
在我们的认知中,JS(V8)是向下兼容的,旧代码总是能够在高版本的 V8 下运行。而 ABI 也是 V8 的一部分,怎么又不能向下兼容了呢?
是因为 V8 提供的 JS API 和 ABI 有所不同。JS API 需要遵循 ECMAScript 标准;而 ABI 不需要。
即使抛开规范,程序接口一般都是向下兼容的,这是开发者常识,ABI 为什么不呢?
一方面,V8 是为性能而设计的,保持向下兼容可能会限制优化空间,从而降低性能改进的潜力;另一方面,使用 ABI 的开发者更偏底层,用户更少,不兼容问题影响面小。
(图片来源于网络)
所以问题的根源是 V8 提供的 ABI 无法保持向下兼容。
Node.js 引入 V8 的方式是,直接将 V8 代码内置到 Node.js 源码中(deps/v8)。但这不意味着 Node.js 从引入 V8 的时候就独立演进 V8,实际上 deps/v8 中的的代码始终和 V8 的官方仓库保持一致。 Node.js 和 V8 分别由各自的独立团队维护。
所以二者之间会有兼容性问题。即使 node 会在内部,通过修改源码,消弭一部分兼容性问题,但随着 V8 的迭代,破坏性更新难以避免。这种兼容性问题,node 通过文档来告知用户。
注意,node 并非为了处理自身与 V8 ABI 的兼容性问题而创造 ABI 版本号。
实际为的是 addon 。
由于 V8 ABI 不保证向下兼容,所以随着 V8 的更新,Node.js 生态中的众多 addon 们可能也要被迫更新。
以上这是第一重麻烦。 更麻烦的是,V8 频繁更新,各个 addon 维护者也不知道哪个 V8 版本的更新,会导致自己的 addon 也要同步更新。不能指望每个 addon 维护者都是 V8 专家。
于是前辈大佬们创造了 ABI 版本号,来解决这些问题。
什么是 ABI 与 ABI 稳定
ABI(Application Binary Interface)直译过来是应用程序二进制接口。与常说的 api 类似,都是用于程序之间的交互。只是 ABI 更强调交互对象是编译后的二进制文件。
在本章,ABI 特指 V8 提供的接口,其实就是 V8 的头文件(deps/v8/include/v8.h)。
那么所谓的 ABI 稳定,就是指 V8 提供的二进制接口的稳定。前文有分析过,V8 ABI 不保证向下兼容,换句话说,V8 ABI 是不稳定的。
大佬们创造的 ABI 版本号,就是用来表示 V8 提供的 ABI 的稳定性。如果没有大改,至少 v8.h 没有改,ABI 版本号也不会随 V8 版本更新而更新;ABI 版本号变了,说明 V8 有较大改动。
ABI 版本号是描述 V8 的,平时把 ABI 版本号放在 Node.js 的属性中,是因为特定版本的 Node.js,使用的 V8 版本是固定的,锁死的。
V8 的 ABI 并不保证向下兼容,但 Node.js 通过 ABI 版本号收敛了破坏性更新的发散程度。
在实操中,通常 ABI 版本号和 Node.js 主要版本号一一对应。可以理解为 Node.js 开发者,把 V8 的重大更新,放在自己的大版本中更新。这样就再次减少了 addon 更新次数。
有了 ABI 版本号以后,addon 开发者只需要盯紧 ABI 版本号。如果 ABI 版本号不更新,自己维护的 addon 也不用更新。
比如某个 addon 使用了 v8.h 的 abi1 接口。abi1 接口被移除了,同时 ABI 版本号更新。需要使用 abi1 接口的地方都换成 abi2 接口,这是一种可能的破坏性更新。这种情况下,如果要升级 node(注意 V8 内嵌在 node 中,升级 node 意味着升级 V8),addon 必须重新编译,甚至修改代码。
对应关系如下表所示:
集大成者 node-api
前面关于 ABI 稳定的讲解,都是指 addon 直接使用 V8 ABI 的情况,包括使用 NAN 的情况。
ABI 版本号出现以后,又出现了 node-api。node-api 封装了 V8 ABI,使用它的 addon,不再直接使用 V8 ABI,而是使用 node-api 的接口。
涉及的众多概念,其关系如下表所示:
开发者通过 node-api 调用 V8 ABI。
node-api 将针对多个 node 版本中 V8 ABI 的变化的兼容性代码逻辑封装在自己内部,从而保证对 addon 暴露的 api 不变,稳定性再次升级。
上表是 node-api 版本与 node 版本之间的关系,可以看到高版本的 node 支持所有低版本的 node-api。这意味着,使用任意版本 node-api 的 addon,在自身不使用新特性时,无需因 node/v8 升级而被迫升级。
使用 node-api 的 addon,其 package.json 文件中都会有个 binary 字段,标识当前使用的 node-api 版本号。
json
"binary": {
"napi_versions": [7]
},
比如前文例子中,a1 接口改 a2 的操作,可能只需要 node-api 去做,广大 addon 们,只在需要使用 v8 新特性时升级 node;如果不使用新特性,addon 无需更改。
另说一点题外话,假设某个 addon 使用了 node-api 8 中的特性,但 binary 继续标识为 7,整套程序运行不会有任何问题。只是应该约定俗成,让使用的 node-api 版本号正确地表示在 binary 字段中。
小结
ABI 稳定这个概念非常绕,如果你还是不太理解,可以直接记忆下面的结论:
如果你维护或使用的 addon,是直接使用 V8 或基于 NAN 开发的,那么你的 addon 需要注意 V8 ABI 不稳定造成的影响。也就是说使用 addon 时,需要警惕高版本的 nodejs/v8 不兼容你的 addon。
如果你维护或使用的 addon,是基于 node-api 开发的,那么你的 addon 不需要注意 V8 ABI 不稳定造成的影响。也就是说使用 addon 时,不用担心高版本的 nodejs/v8 不兼容你的 addon。
因为只要你的 addon 使用了 node-api,那么它是对 node 版本向上兼容的。只有在你的 addon 内部,需要使用 nodejs/v8 某个新特性时,你需要保证环境中的 nodejs/v8 版本够高。
参考资料:
下一节,将分享《基于 V8 封装一个自己的 JavaScript 运行时》相关内容,请大家持续关注本系列内容~学习完本系列,你将获得:
-
提升调试与性能优化能力
-
深入理解模块化与扩展机制
-
探索底层技术与定制化能力
同时欢迎大家给OpenTiny提建议:【OpenTiny调研征集】共创技术未来,分享您的声音!
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
OpenTiny 代码仓库:github.com/opentiny
TinyVue 源码:github.com/opentiny/ti...
TinyEngine 源码: github.com/opentiny/ti...
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~