最近在启动一个老旧的 vue 项目的时候,发现在npm install
的时候,node-sass
在进行编译时报关于node-gyp
的错,于是花了点时间来搞清楚node-gyp
的作用和背后的机理。
javascript: 一个跨平台脚本语言
我们知道,JavaScript 脚本语言不依赖于操作系统,仅需要浏览器的支持。因此一个 JavaScript 脚本在编写后可以带到任意机器上使用,前提上机器上的浏览器支持JavaScript脚本语言。
像 C/C++ 它把源程序由特定平台的编译器一次性编译为平台相关的机器码,是无法跨平台。像JavaScript 它不直接面向底层,它使用特定的解释器(chrome 或者 nodejs),把代码一行行解释为机器码,类似于同声翻译,它的优点是可以跨平台,缺点是执行速度慢,暴露源程序。
所以,nodejs
也是跨平台的,所以对于任何的 nodejs
模块理论也应该是跨平台的。然而,有些 nodejs
模块直接或间接使用原生 C/C++ 代码,这些东西要跨平台,就需要使用源码根据实际的操作平台环境进行原生模块编译。除了nodejs
模块外,还有一些 Node 插件,它们也是由 C/C++ 编写的,所以在使用的时候也需要编译。
Node 插件
Node 插件是用 C/C++ 语言编写的动态链接库,使用 require()
加载到 Node 环境中,能够像普通 Node 模块一样使用。Node 插件可以用于编写高性能 C++ 算法,也可以用在Node 环境与其他 C/C++ 库之间提供接口封装,实现互相调用。
在早期的 Node 插件开发中,严重依赖 V8 引擎的 API,可能都遇到过升级 Node 版本后插件不可用的情况,需要重新编译。这是因为 Node 版本升级,V8 引擎的二进制 ABI 接口发生变化,导致之前编译的 Node 插件不可用。
为了解决这一问题,在 Node 8.0 版本中发布了新的 N-API 接口。 N-API 并不是一种新的插件编写模式,N-API 是对 V8 引擎 API 的封装,以 C 风格 API 提供对外接口,并且保证接口是 ABI 稳定的。使用 N-API 编写的 Node 插件能够一次编写、一次编译,跨多个Node 版本运行。N-API 接口在 8.12.0 以及更高版本中已经处于稳定状态(参见 abi-stable-node),可以放心在生产环境投入使用。
编译 Node 插件使用 node-gyp。下面我们尝试编译一个简单的hello-world插件。
在 github.com/nodejs/node… 中有官方提供的多个 Node 插件上手示例项目。其中多数小 demo 官方有提供了 3 种实现方式,分别是 NAN ,N-API 以及 node-addon-api。node-addon-api 是对 C 形式的 N-API 的 C++ 封装,同样是 ABI 兼容的。我个人推荐使用 node-addon-api。NAN 是早期的写插件使用的 API,需要和 V8 API 结合使用,现在已经不再推荐。
通过使用 node-addon-api,插件代码比直接使用 N-API 更加简洁、易读。 NODE_MODULE 第一个参数是插件名称,第二个参数是 Initialize 注册函数。Initialize 注册函数中,将 hello
绑定到函数 Method
上。
hello_world.cc
#include <node.h>void Method(const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Isolate* isolate = args.GetIsolate(); args.GetReturnValue().Set(v8::String::NewFromUtf8( isolate, "world").ToLocalChecked());}void Initialize(v8::Local<v8::Object> exports) { NODE_SET_METHOD(exports, "hello", Method);}NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
binding.gyp
{ "targets": [ { "target_name": "hello_world", "sources": [ "hello_world.cc" ] } ]}
index.js
const binding = require('./build/Release/hello_world'); console.log(binding.hello());
package.json
"scripts": { "build": "node-gyp configure && node-gyp build", "dev": "node index.js" }
执行npm run build
,生成hello_world.node
文件,然后像引入其他 js 模块一样就可以正常使用了。
在 javascript 端,也可以使用 bindings 来加载模块。因为 Node 插件历史发展中,二进制文件会被编译产出到很多不同的位置,使用 bindings 可以解决寻找插件路径的问题,bindings 检查所有可能的插件构建位置,返回第一个成功的加载位置。首先安装 bindings
npm install bindings
node-gyp
在上面编译 hello_world.cc
的工具是 node-gyp
,要理解 node-gyp
首先要知道什么是gyp
。
gyp
其实是一个用来生成项目文件的工具,一开始是设计给chromium
项目使用的,后来大家发现比较好用就用到了其他地方。生成项目文件后就可以调用 GCC, vsbuild, xcode 等编译平台来编译。至于为什么要有node-gyp
,是由于 node 程序中需要调用一些其他语言编写的工具甚至是dll,需要先编译一下,否则就会有跨平台的问题,例如在windows上运行的软件 copy 到 mac上 就不能用了,但是如果源码支持,编译一下,在 mac 上还是可以用的。
长久以来 linux 的二进制分发一直是巨坑,npm 为了方便干脆就直接源码分发,用户装的时候再现场编译。不过对另一些人二进制分发就比源码分发简单多了,所以还有个 node-pre-gyp
来干二进制扩展的分发。
node-pre-gyp
上面 node-gyp
固然相当方便了,但是每一次安装 node 原生模块的时候,都需要根据平台(Windows、Linux、macOS以及对应的x86、x64、arm64等等)进行源码编译,这样做费时费力。为什么不一开始就针对这些平台编译好了做成二进制制品发布呢?反正一般来说主流的平台架构就那么一些(Windows、Linux、macOS)。所以 node-pre--gyp
就帮我们做了这件事。原生模块开发者将代码编译生成各个平台架构的二进制包直接发布到 node-pre-gyp
上,当我们的node项目安装原生模块时候。处理流程就是首先去 node-pre-gyp
上找有没有当前平台的组件包,有的话直接拉取使用,如果没有则进行原生编译。下图是 node-sqlite3的二进制包:
于是乎,当我们进行node原生模块安装的时候,一般会有如下的流程:
针对当前平台架构优先考虑 node-pre-gyp
方式进行安装,但是为了防止无法获取针对对应平台编译好的二进制包(网络原因、暂时没有对应平台的二进制包),进入第2步;
下载原生模块源码,然后使用 node-gyp
进行项目构建,得到与平台相关的源码项目文件(Windows则生成vcxproj
项目,Linux下是Makefile
);在这个过程,node-gyp
会使用Python
进行自动化构建操作,这也是为什么有些朋友安装node原生模块的时候,会报错找不到Python
。
调用平台对应的编译工具进行编译。在Windows的环境下,node-gyp
会查找本地的MSBuild/CL
等编译工具,而这些编译工具又一般在Visual Studio
安装的时候,也一并安装在了机器上。这就是为什么有些朋友没有安装Visual Studio
的时候,会报错。