前端轮子千千万, 但仍是有些瓶颈, 公司须要在前端调用自有 tcp 协议, 该协议只有 c++ 的封装版本. 领导但愿能够直接调该模块, 不要重复造轮子.html
实话说我对 C 还有点印象, 毕竟也是有二级 C 语言证的人..可是已经好久没用了, 看着一大堆的C 语言类型的定义, 让我这个常年使用隐式类型的 jser 情何以堪.这是我从业以来最难实现的 hello world
项目.前端
一个 Native Addon 在 Nodejs 的环境里就是一个二进制文件, 这个文件是由低级语言, 好比 C 或 C++实现, 咱们能够像调用其余模块同样 require() 导入 Native Addonnode
Native Addon 与其余.js 的结尾的同样, 会暴露出 module.exports
或者 exports
对象, 这些被封装到 node 模块中的文件也被成为 Native Module(原生模块).python
那么如何让 Native Addon 能够加载并运行在 js 的应用中? 让 Native Addon 能够兼容 js 的环境而且暴露的 API 能够像正常 node 模块同样被使用呢?linux
这里不得不说下 DLL(Dynamic Linked Library)动态库, 他是由 C 或 C++使用标准编译器编译而成, 在 linux 或 macOS 中也被称做 Shared Library. 一个 DLL 能够被一个程序在运行时动态加载, DLL 包含源 C 或 C++代码以及可通讯的 API. 有动态是否还有静态的呢? 还真有~ 能够参考这里来看这二者的区别, 简单来讲静态比动态更快, 由于静态不须要再去查找依赖文件并加载, 可是动态能够颗粒度更小的修改打包的文件.ios
在 Nodejs 中, 当编译出 DLL 的时候, 会被导出为.node 的后缀文件. 而后能够 require 该文件, 像 js 文件同样.不过代码提示是不可能有的了.c++
Nodejs 实际上是不少开源库的集合,能够看看他的仓库, 在 package.json 中找 deps. 使用的是谷歌开源的 V8 引擎来执行 js 代码, 而 V8恰好是使用 C++写的, 不信你看 v8 的仓库. 而对于像异步 IO, 事件循环和其余低级的特性则是依赖 Libuv 库.git
当安装完 nodejs 以后, 其实是安装了一个包含整个 Nodejs 以及其依赖的源代码的编译版本, 这样就不用一个一个手动安装这些依赖而. 不过Nodejs也能够由这些库的源代码编译而来. 那么跟 Native Addon 有什么关系呢? 由于 Nodejs 是由低层级的 C 和 C++编译而成的, 因此自己就具备与 C 和 C++相互调用的能力.github
Nodejs 能够动态加载 C 和 C++的 DLL 文件, 而且使用其 API 在 js 程序中进行操做. 以上就是基本的 Native Addon 在 Nodejs 中的工做原理.npm
ABI 是特指应用去访问编译好|compiled的程序, 跟 API(Application Programming Interface)很是类似, 只不过是与二进制文件进行交互, 并且是访问内存地址去查找 Symbols, 好比 numbers, objects, classes和 functions
那么这个 ABI 跟 Native Addon 有什么关系呢? 他是 Native Addon 与 Nodejs 进行通讯的桥梁. DDL 文件其实是经过 Nodejs 提供的ABI 来注册或者访问到值, 而且经过Nodejs暴露的 API和库来执行命令.
举个例子, 有个 Native Addon 想添加一个sayHello
的方法到exports
对象上, 他能够经过访问 Libuv 的 API 来建立一个新的线程,异步的执行任务, 执行完毕以后再调用回调函数. 这样 Nodejs 提供的 ABI 的工做就完成了.
一般来讲, 都会将 C 或 C++编译为 DLL, 会使用到一些被称做header 头文件的元数据. 都是以.h
结尾.固然这些头文件中, 能够是 Nodejs及node的库暴露出去的可让 Native Addon引用的.头文件的资料可参考
一个典型的引用是使用#include
好比#inlude<v8.h>
, 而后使用声明来写 Nodejs 可执行的代码.有如下四种方式来使用头文件.
好比v8.h
-> v8引擎, uv.h
-> Libuv库这两个文件都在 node 的安装目录中. 可是这样的问题就是 Native Addon 和 Nodejs 之间的依赖程度过高了.由于 Nodejs 的这些库有可能随着 Node 版本的更新而更改, 那么每次更改以后是否还要去适配更改 Native Addon? 这样的维护成本较高.你能够看看 node 官方文档中对这种方法的描述, 下面有更好的方法
NAN 项目最开始就是为了抽象 nodejs 和 v8 引擎的内部实现. 基本概念就是提供了一个 npm 的安装包, 能够经过前端的包管理工具yarn
或npm
进行安装, 他包含了nan.h
的头文件, 里面对 nodejs 模块和 v8 进行了抽象. 可是 NAN 有如下缺点:
因此更推荐如下两种方式
N-API相似于 NAN 项目, 可是是由 nodejs 官方维护, 今后就不须要安装外部的依赖来导入到头文件. 而且提供了可靠的抽象层 他暴露了node_api.h
头文件, 抽象了 nodejs 和包的内部实现, 每次 Nodejs 更新, N-API 就会同步进行优化保证 ABI 的可靠性 这里是 N-API 的全部接口文档, 这里是官方对 N-API 的 ABI 稳定性的描述
N-API 同时适合于 C 和 C++, 可是 C++的 API 使用起来更加的简单, 因而, node-addon-api 就应运而生.
跟上述两个同样, 他有本身的头文件napi.h
, 包含了 N-API 的全部对 C++的封装, 而且跟 N-API 同样是由官方维护, 点这里查看仓库.由于他的使用相较于其余更加的简单, 因此在进行 C++API 封装的时候优先选择该方法.
须要全局安装yarn global add node-gyp
, 由于还依赖于 Python, (GYP 全称是 Generate Your Project, 是一个用 Python 写成的工具). 具体制定 python 的环境及路径参考文档.
安装完成后就有了一个生成编译 C 或 C++到 Native Addon 或 DLL的模板代码的CLI, 一顿操做猛如虎后,会生成一个.node
文件. 可是这个模板是怎么生成的呢?就是下面这个 binding.gyp
文件
binding.gyp
binding.gyp
包含了模块的名字, 哪些文件应该被编译等. 模板会根据不一样的平台或架构(32仍是 64)包含必要的构建指令文件, 也提供了必要的 header 或 source 文件去编译 C 或 C++, 相似于 JSON 的格式, 详情可点击查看.
安装依赖后, 真正开始咱们的 hello world 项目, 总体的项目文件结构为:
├── binding.gyp
├── index.js
├── package.json
├── src
│ ├── greeting.cpp
│ ├── greeting.h
│ └── index.cpp
└── yarn.lock
复制代码
Native Module 跟正常的 node 模块或其余 NPM 包同样. 先yarn init -y
初始化项目, 再安装node-addon-apiyarn add node-addon-api
.
建立 greeting.h 文件
#include <string> std::string helloUser(std::string name); 复制代码
建立 greeting.cpp 文件
#include <iostream> #include <string> #include "greeting.h" std::string helloUser(std::string name) { return "Hello " + name + "!"; } 复制代码
建立 index.cpp 文件, 该文件会包含 napi.h
#include <napi.h>
#include <string>
#include "greeting.h"
// 定义一个返回类型为 Napi String 的 greetHello 函数, 注意此处的 info
Napi::String greetHello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
std::string result = helloUser('Lorry');
return Napi::String::New(env, result);
}
// 设置相似于 exports = {key:value}的模块导出
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(
Napi::String::New(env, "greetHello"), // key
Napi::Function::New(env, greetHello) // value
);
return exports;
}
NODE_API_MODULE(greet, Init)
复制代码
注意这里你看到不少的 Napi:: 这样的书写, 其实这就是在 js 与 C++之间的数据格式桥梁, 定义双方都看得懂的数据类型. 这里经历了如下流程:
napi.h
头文件, 他会解析到下面会说的 binding.gyp 指定的路径中greeting.h
自定义头文件. 注意使用 ""和<>的区别, ""会查找当前路径, 详情请查看node-addon-api
的头文件. Napi 是一个命名空间. 由于宏不支持命名空间, 因此 NODE_API_MODULE
前没有NODE_API_MODULE
是一个node-api
(N-API)中封装的NAPI_MODULE
宏中提供的函数(宏). 它将会在js 使用require
导入 Native Addon的时候被调用.binding.gyp
中的 target_name 保持一致, 只不过这里是使用一个标签 label 而不是字符串的格式env
和 exports
参数env
值是Napi::env
类型, 包含了注册模块时的环境(environment), 这个在 N-API 操做时被使用. Napi::String::New
表示建立一个新的Napi::String
类型的值.这样就将 helloUser的std:string
转换成了Napi::String
exports
是一个module.exports
的低级 API, 他是Napi::Object
类型, 可使用Set
方法添加属性, 参考文档, 该函数必定要返回一个exports
建立binding.gyp
文件
{
"targets": [
{
"target_name": "greet", // 定义文件名
"cflags!": [ "-fno-exceptions" ], // 不要报错
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ // 包含的待编译为 DLL 的文件们
"./src/greeting.cpp",
"./src/index.cpp"
],
"include_dirs": [ // 包含的头文件路径, 让 sources 中的文件能够找到头文件
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [
'NAPI_DISABLE_CPP_EXCEPTIONS' // 去掉全部报错
],
}
]
}
复制代码
生成模板文件
在 binding.gyp
同级目录下使用
node-gyp configure
复制代码
将会生成一个 build 文件夹, 会包含如下文件:
./build
├── Makefile // 包含如何构建 native 源代码到 DLL 的指令, 而且兼容 Nodejs 的运行时
├── binding.Makefile // 生成文件的配置
├── config.gypi // 包含编译时的配置列表
├── greet.target.mk // 这个 greet 就是以前配置的 target_name 和 NODE_API_MODULE 的第一个参数
└── gyp-mac-tool // mac 下打包的python 工具
复制代码
构建并编译
node-gyp build
复制代码
将会构建出一个.node
文件
./build
├── Makefile
├── Release
│ ├── greet.node // 这个就是编译出来的node文件, 可直接被 js require 引用
│ └── obj.target
│ └── greet
│ └── src
│ ├── greeting.o
│ └── index.o
├── binding.Makefile
├── config.gypi
├── greet.target.mk
└── gyp-mac-tool
复制代码
走到这一步你会发现.node
文件是没法被打开的, 由于他就不是给人读的, 是一个二进制文件.这个时候就能够尝试一波
// index.js const addon = require('./build/Release/greet.node') console.log(addon.greetHello()) 复制代码
直接使用node index.js
运行代码你会发现打印出 Hello Lorry !
, 正是 helloUser 里面的内容. 真是不容易啊.
仅仅到此吗? 还不够
传参
上述代码都是写死的 Lorry, 我要是 Mike, Jane, 张三王五呢?并且不能传参的函数不是好函数
因而以前说到的 info 就起做用了, 详情可参考, 由于info的[]运算符重载, 能够实现对类C++数组的访问. 如下是对 index.cpp
文件的 greetHello
函数的修改:
Napi::String greetHello(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); std::string user = (std::string) info[0].ToString(); std::string result = helloUser(user); return Napi::String::New(env, result); } 复制代码
而后使用
node-gyp rebuild
复制代码
在修改下引用的 index.js 文件
const addon = require('./build/Release/greet.node') console.log(addon.greetHello('张三')) // Hello 张三! 复制代码
至此, 终于算是比较完整的实现了咱们的 hello world.别急, 还有货
若是要像其余包同样能够进行发布的话, 操做就跟正常的npm打包流程差很少了. 在package.json
中的 main 字段中指定 index.js
,而后修改index.js
内容为:
const addon = require('./build/Release/greet.node') module.exports = addon.greetHello 复制代码
再使用 yarn pack
便可打包出一个.tgz
, 在其余项目中引入便可.还有没有?还有一点点
一般在发布模块的时候, 不会把build
文件夹算在内, 可是.node
文件是放在里面的. 并且.node
文件以前说了, 依赖于系统和架构, 若是是使用 macOS 打包的.node
确定是不能在 windows 上使用的. 那么怎么实现兼容性呢? 没错, 每次在用户安装的时候都从新按照对应硬件配置build 一遍, 也就是使用node-gyp rebuild
, npm或者 yarn 在安装依赖过程当中发现了binding.gyp
的话会自动在本地安装node-gyp
, 因此 rebuild
才能成功.
不过,还记得吗? 处理 node-gyp 以外还有别的前提条件, 这就是为何在安装一些库的时候常常会出现 node-gyp 的报错.好比 python 的版本? node 的版本? 都有可能致使安装这个模块的用户抓狂.因而还有一个办法:为每一个平台架构打包一份.node 文件, 这能够经过 pacakge.json 的 install 脚本实现区分安装, 有一个第三方包 node-pre-gyp
能够自动实现. 若是不想使用 node-pre-gyp 中那么复杂的配置, 还能够尝试 prebuild-install
这个轮子
可是还有一个问题, 咱们如何实现打包出不一样平台和架构的文件? 难道我买各类硬件来打包?不现实. 没事, 还有轮子 prebuild
, 能够设置不一样平台, 架构甚至 node 版本都能指定.
PS: 这里还有一个 vscode 的坑, 在使用 C++ 的 extension 进行代码提示的时候总是提醒我#include <napi.h>
找不到文件,可是打包是彻底没有问题的, 猜想是编辑器不支持识别 binding.gyp 里的头文件查找路径, 找了不少地方没有相应的解决办法.最后翻这个插件的文档发现能够配置clang.cxxflags
, 因而乎我在里面添加了一条头文件的指定路径-I${workspaceRoot}/node_modules/node-addon-api
就没问题了, 能够享受代码提示了, 否则真的很容易写错啊!!