前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# Node8_napi

30 秒速记

  • N-API(Node 16 起官方叫 Node-API)= Node 提供的一层稳定 C 接口,原生扩展不再直接调 V8
  • 核心价值是 ABI 稳定:编译一次,换 Node 大版本不用重新编译
  • 版本线:Node 8.0 实验性引入,Node 8.12 / 10 起标记稳定;C++ 项目一般用封装库 node-addon-api
  • 原生代码没有 JS 的安全网:类型、长度、生命周期写错就是进程直接崩
  • 耗时操作别在主线程跑,用 napi_create_async_work 或线程安全函数把结果送回 JS

N-API 是 Node 给原生扩展提供的一层稳定接口,扩展通过它操作 JS 值,而不是直接调 V8 的 API。 以前写扩展直接依赖 V8,Node 一升级 V8 跟着变,扩展就得重新编译甚至改代码。有了这层隔离,只要 N-API 版本满足,同一个 .node 二进制可以跨 Node 大版本加载。不过它只解决了跨版本问题,原生代码本身的内存安全、阻塞主线程这些坑一个都没少,写错了不是抛异常,是整个进程崩掉。

理解「Node8_napi」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

写原生扩展时,代码大致分成三层:JS 调用方、N-API 这层稳定接口、你自己的 C / C++ 逻辑。JS 传进来的值在 C 里都是 napi_value 句柄,要先用 napi_get_value_* 系列函数取出真实数据,算完再用 napi_create_* 包回去。

#include <node_api.h>

// add(a, b):参数校验是必须的,传错类型不处理就可能崩
static napi_value Add(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
  if (argc < 2) {
    napi_throw_type_error(env, NULL, "need 2 numbers");
    return NULL;
  }
  double a, b;
  if (napi_get_value_double(env, argv[0], &a) != napi_ok ||
      napi_get_value_double(env, argv[1], &b) != napi_ok) {
    napi_throw_type_error(env, NULL, "args must be numbers");
    return NULL;
  }
  napi_value result;
  napi_create_double(env, a + b, &result);
  return result;
}

static napi_value Init(napi_env env, napi_value exports) {
  napi_value fn;
  napi_create_function(env, "add", NAPI_AUTO_LENGTH, Add, NULL, &fn);
  napi_set_named_property(env, exports, "add", fn);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
@前端进阶之旅: 代码已经复制到剪贴板

JS 侧 require('./build/Release/addon.node').add(1, 2) 输出 3,传 add('a', 2) 会抛 TypeError,而不是让进程崩掉。这就是写原生代码的基本功:每个 napi_* 调用都返回 napi_status,要检查;外部输入都不可信。

至于要不要写原生扩展,现在很多场景有更省事的选择:纯计算可以考虑 WebAssembly,调系统库可以用 FFI 类方案。只有性能极度敏感、或者必须对接现成的 C / C++ 库时,原生扩展才是首选。

面试官追问

追问 1用了 N-API 是不是就不用为每个 Node 版本单独编译了?
参考回答

基本是。ABI 稳定意味着同一个 .node 在支持对应 N-API 版本的 Node 上都能加载,但操作系统、CPU 架构、glibc 和 musl 的差异还是要分别出包。

追问 2扩展里有个压缩函数要跑 2 秒,调用时整个服务卡住,怎么改?
参考回答

原生代码直接在主线程跑,会把事件循环堵死。用 napi_create_async_work 把计算丢到 libuv 线程池里执行,完成回调里再把结果转成 napi_value 交回 JS,对外暴露成 Promise。

追问 3原生扩展偶尔让进程直接退出,日志里连 JS 报错都没有,怎么查?
参考回答

这种多半是段错误,JS 层根本拦不住。先查参数校验,比如期望字符串却传了 undefined,再查有没有在回调里用了已经被回收的值;本地用 lldb 或 gdb 挂上去复现,拿到 C 层调用栈。

追问 4新项目写原生扩展,直接用 C 的 N-API 还是 node-addon-api?
参考回答

写 C++ 的话用 node-addon-api,它把 N-API 包成类和异常,代码量少一半,底层还是同一套稳定 ABI。只有项目必须纯 C 才直接写 napi_* 函数。

追问 5发布带原生扩展的 npm 包,怎么让用户不用装编译工具?
参考回答

用 prebuildify 预先编好各平台的二进制放进包里,运行时由 node-gyp-build 按平台挑选,找不到再回退到本地编译。因为 N-API 跨版本,每个平台出一份就够,不用再按 Node 版本乘一遍。

# N-API简介

30 秒速记

  • 痛点:老式扩展直接调 V8 的 API,Node 升级换了 V8,扩展就编译失败
  • N-API 在中间加一层和 JS 引擎无关的接口,扩展只认这层,不认 V8
  • Node 8.0(2017 年 6 月)实验性引入,Node 10 起稳定,后来改名 Node-API
  • 它管的是接口兼容,不管编译工具链、系统库、平台架构这些问题

N-API 要解决的就是「升级 Node 原生扩展就挂」这个老问题。 以前扩展直接用 V8 的 C++ API,而 V8 的接口变得很勤,Node 换一个大版本,扩展作者就得跟着改代码重编译,用户这边就是 npm install 一片红。N-API 在扩展和引擎之间加了一层稳定的 C 接口,扩展只跟这层打交道,Node 内部怎么换 V8 都不影响。可以把它理解成操作系统的系统调用,内核怎么改,程序调的接口不变。

Node.js 8.0 在2017年6月份发布,升级的特性中,包含了N-API。编写过或者使用过 node扩展的同学,不少都遇到过升级node版本,node扩展编译失败的情况。因为node扩展严重依赖于V8暴露的API,而node不同版本依赖的V8版本可能不同,一旦升级node版本,原先运行正常的node扩展就编译失败了。

这种情况对node生态圈无疑是不利的,N-API的引入正是试图改善这种情况的一种尝试。它跟底层JS引擎无关,只要N-API暴露的API足够稳定,那么node扩展的编写者就不用过分担忧node的升级问题。

面试官追问

追问 1以前升级 Node,为什么经常看到 node-sass 这类包装不上?
参考回答

它们是直接依赖 V8 API 的原生扩展,二进制和 Node 的 ABI 版本(NODE_MODULE_VERSION)绑死。换了 Node 大版本,预编译包对不上,就得本地重编,没装编译工具就失败。

追问 2用了 N-API 是不是升级 Node 永远不会出问题了?
参考回答

不是。它保证的是接口层兼容,编译器版本、系统依赖库、扩展自己的代码问题照样会出问题。另外扩展用了高版本才有的 N-API 函数,在低版本 Node 上一样加载不了。

追问 3怎么判断一个原生依赖是不是基于 N-API 写的?
参考回答

看它的 binding.gyp 或源码里用的是 napi_* / node-addon-api,还是 v8:: 和 nan;package.json 里有 binary.napi_versions 字段的也基本是。用 nan 的那种升级 Node 还是要重编。

追问 4团队维护几个老扩展,每次升级 Node 都要改,值得迁到 N-API 吗?
参考回答

升级频繁的话值得,迁一次以后基本告别「升级就改代码」。可以先挑接口简单的扩展迁,用 node-addon-api 改写成本不高;迁移后要在目标 Node 版本上跑一遍构建和加载测试。

# 如何使用N-API

30 秒速记

  • N-API 不是新的扩展机制,只是换了一套和引擎无关的 API,写法、编译、加载流程都和以前一样
  • 四步:装环境 → 写 C 源码 → binding.gyp + node-gyp 编译 → JS 里 require 调用
  • 产物还是 .node 文件,本质是一个动态链接库
  • 版本边界:Node 8 早期是实验特性,需要加启动标记;Node 10 以后稳定,可以放心上生产

N-API

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
      • N-API简介
      • 如何使用N-API
      • 1、环境准备
      • 2、编写扩展
      • 3、编译扩展
      • 4、调用扩展
      • 相关链接
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

Node8_napi|Nodejs专题系列

# Node8_napi

30 秒速记

  • N-API(Node 16 起官方叫 Node-API)= Node 提供的一层稳定 C 接口,原生扩展不再直接调 V8
  • 核心价值是 ABI 稳定:编译一次,换 Node 大版本不用重新编译
  • 版本线:Node 8.0 实验性引入,Node 8.12 / 10 起标记稳定;C++ 项目一般用封装库 node-addon-api
  • 原生代码没有 JS 的安全网:类型、长度、生命周期写错就是进程直接崩
  • 耗时操作别在主线程跑,用 napi_create_async_work 或线程安全函数把结果送回 JS

N-API 是 Node 给原生扩展提供的一层稳定接口,扩展通过它操作 JS 值,而不是直接调 V8 的 API。 以前写扩展直接依赖 V8,Node 一升级 V8 跟着变,扩展就得重新编译甚至改代码。有了这层隔离,只要 N-API 版本满足,同一个 .node 二进制可以跨 Node 大版本加载。不过它只解决了跨版本问题,原生代码本身的内存安全、阻塞主线程这些坑一个都没少,写错了不是抛异常,是整个进程崩掉。

理解「Node8_napi」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

写原生扩展时,代码大致分成三层:JS 调用方、N-API 这层稳定接口、你自己的 C / C++ 逻辑。JS 传进来的值在 C 里都是 napi_value 句柄,要先用 napi_get_value_* 系列函数取出真实数据,算完再用 napi_create_* 包回去。

#include <node_api.h>

// add(a, b):参数校验是必须的,传错类型不处理就可能崩
static napi_value Add(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
  if (argc < 2) {
    napi_throw_type_error(env, NULL, "need 2 numbers");
    return NULL;
  }
  double a, b;
  if (napi_get_value_double(env, argv[0], &a) != napi_ok ||
      napi_get_value_double(env, argv[1], &b) != napi_ok) {
    napi_throw_type_error(env, NULL, "args must be numbers");
    return NULL;
  }
  napi_value result;
  napi_create_double(env, a + b, &result);
  return result;
}

static napi_value Init(napi_env env, napi_value exports) {
  napi_value fn;
  napi_create_function(env, "add", NAPI_AUTO_LENGTH, Add, NULL, &fn);
  napi_set_named_property(env, exports, "add", fn);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
@前端进阶之旅: 代码已经复制到剪贴板

JS 侧 require('./build/Release/addon.node').add(1, 2) 输出 3,传 add('a', 2) 会抛 TypeError,而不是让进程崩掉。这就是写原生代码的基本功:每个 napi_* 调用都返回 napi_status,要检查;外部输入都不可信。

至于要不要写原生扩展,现在很多场景有更省事的选择:纯计算可以考虑 WebAssembly,调系统库可以用 FFI 类方案。只有性能极度敏感、或者必须对接现成的 C / C++ 库时,原生扩展才是首选。

面试官追问

追问 1用了 N-API 是不是就不用为每个 Node 版本单独编译了?
参考回答

基本是。ABI 稳定意味着同一个 .node 在支持对应 N-API 版本的 Node 上都能加载,但操作系统、CPU 架构、glibc 和 musl 的差异还是要分别出包。

追问 2扩展里有个压缩函数要跑 2 秒,调用时整个服务卡住,怎么改?
参考回答

原生代码直接在主线程跑,会把事件循环堵死。用 napi_create_async_work 把计算丢到 libuv 线程池里执行,完成回调里再把结果转成 napi_value 交回 JS,对外暴露成 Promise。

追问 3原生扩展偶尔让进程直接退出,日志里连 JS 报错都没有,怎么查?
参考回答

这种多半是段错误,JS 层根本拦不住。先查参数校验,比如期望字符串却传了 undefined,再查有没有在回调里用了已经被回收的值;本地用 lldb 或 gdb 挂上去复现,拿到 C 层调用栈。

追问 4新项目写原生扩展,直接用 C 的 N-API 还是 node-addon-api?
参考回答

写 C++ 的话用 node-addon-api,它把 N-API 包成类和异常,代码量少一半,底层还是同一套稳定 ABI。只有项目必须纯 C 才直接写 napi_* 函数。

追问 5发布带原生扩展的 npm 包,怎么让用户不用装编译工具?
参考回答

用 prebuildify 预先编好各平台的二进制放进包里,运行时由 node-gyp-build 按平台挑选,找不到再回退到本地编译。因为 N-API 跨版本,每个平台出一份就够,不用再按 Node 版本乘一遍。

# N-API简介

30 秒速记

  • 痛点:老式扩展直接调 V8 的 API,Node 升级换了 V8,扩展就编译失败
  • N-API 在中间加一层和 JS 引擎无关的接口,扩展只认这层,不认 V8
  • Node 8.0(2017 年 6 月)实验性引入,Node 10 起稳定,后来改名 Node-API
  • 它管的是接口兼容,不管编译工具链、系统库、平台架构这些问题

N-API 要解决的就是「升级 Node 原生扩展就挂」这个老问题。 以前扩展直接用 V8 的 C++ API,而 V8 的接口变得很勤,Node 换一个大版本,扩展作者就得跟着改代码重编译,用户这边就是 npm install 一片红。N-API 在扩展和引擎之间加了一层稳定的 C 接口,扩展只跟这层打交道,Node 内部怎么换 V8 都不影响。可以把它理解成操作系统的系统调用,内核怎么改,程序调的接口不变。

Node.js 8.0 在2017年6月份发布,升级的特性中,包含了N-API。编写过或者使用过 node扩展的同学,不少都遇到过升级node版本,node扩展编译失败的情况。因为node扩展严重依赖于V8暴露的API,而node不同版本依赖的V8版本可能不同,一旦升级node版本,原先运行正常的node扩展就编译失败了。

这种情况对node生态圈无疑是不利的,N-API的引入正是试图改善这种情况的一种尝试。它跟底层JS引擎无关,只要N-API暴露的API足够稳定,那么node扩展的编写者就不用过分担忧node的升级问题。

面试官追问

追问 1以前升级 Node,为什么经常看到 node-sass 这类包装不上?
参考回答

它们是直接依赖 V8 API 的原生扩展,二进制和 Node 的 ABI 版本(NODE_MODULE_VERSION)绑死。换了 Node 大版本,预编译包对不上,就得本地重编,没装编译工具就失败。

追问 2用了 N-API 是不是升级 Node 永远不会出问题了?
参考回答

不是。它保证的是接口层兼容,编译器版本、系统依赖库、扩展自己的代码问题照样会出问题。另外扩展用了高版本才有的 N-API 函数,在低版本 Node 上一样加载不了。

追问 3怎么判断一个原生依赖是不是基于 N-API 写的?
参考回答

看它的 binding.gyp 或源码里用的是 napi_* / node-addon-api,还是 v8:: 和 nan;package.json 里有 binary.napi_versions 字段的也基本是。用 nan 的那种升级 Node 还是要重编。

追问 4团队维护几个老扩展,每次升级 Node 都要改,值得迁到 N-API 吗?
参考回答

升级频繁的话值得,迁一次以后基本告别「升级就改代码」。可以先挑接口简单的扩展迁,用 node-addon-api 改写成本不高;迁移后要在目标 Node 版本上跑一遍构建和加载测试。

# 如何使用N-API

30 秒速记

  • N-API 不是新的扩展机制,只是换了一套和引擎无关的 API,写法、编译、加载流程都和以前一样
  • 四步:装环境 → 写 C 源码 → binding.gyp + node-gyp 编译 → JS 里 require 调用
  • 产物还是 .node 文件,本质是一个动态链接库
  • 版本边界:Node 8 早期是实验特性,需要加启动标记;Node 10 以后稳定,可以放心上生产

N-API 没改原生扩展的玩法,只是把你在 C 代码里调用的接口换成了稳定的那一套。 流程还是老四步:准备 Node 和 node-gyp,写一个 .cc 文件导出方法,用 binding.gyp 描述怎么编译,编出 .node 文件后在 JS 里 require。区别在源码里,不再写 v8::Isolate、v8::Local 这些,而是 napi_value、napi_create_string_utf8 这类函数。早期 Node 8 里它还是实验特性,接口也变过,现在新版本里已经稳定,生产可以用。

先强调一点,N-API并不是对原有node扩展实现方式的替代,它只是提供了一系列底层无关的API,来帮助开发者编写跨版本的node扩展。至于如何编写、编译、使用扩展,跟原来的差不多。

本文会从一个超级简单的例子,简单介绍N-API的使用,包括环境准备、编写扩展、编译、运行几个步骤。

备注:当前N-API还处于试验阶段,官方文档提供的例子都是有问题的,如用于生产环境需格外谨慎。

面试官追问

追问 1用了 N-API 还需要 node-gyp 编译吗?
参考回答

需要。N-API 只是换了接口,源码还是 C / C++,照样要编成 .node 才能加载。它省掉的是「每个 Node 版本重新编一次」,不是编译本身。

追问 2扩展编译成功了,require 的时候报错,该看源码里的方法实现吗?
参考回答

先别看。require 失败说明还没走到你的方法,多半是加载或注册阶段的问题:路径对不对、NAPI_MODULE 注册的初始化函数有没有、编译用的 Node 和运行的是不是一个。

追问 3老扩展用 v8:: 写的跑得好好的,加一个小方法要不要整体重写成 N-API?
参考回答

不急着整体重写。如果近期没有升级 Node 的压力,小改动按原方式加就行;要是升级频繁,可以借这次机会规划迁移,但别在一个需求里把重写和新功能绑一起上线。

追问 4Node 8 早期示例里的初始化函数签名,在新版本还能用吗?
参考回答

不能照抄。现在标准写法是 napi_value Init(napi_env env, napi_value exports),配 NAPI_MODULE(NODE_GYP_MODULE_NAME, Init),旧的四参数 void 版本是实验期的接口。

# 1、环境准备

← log4js入门实例使用 async 控制并发 →

fe