webpack 5 如何在 Node 应用中打包 .node 原生模块并用 dlopen 动态加载
【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack
用 webpack 给 Node.js 应用做打包时,.node原生模块和普通 JS 模块不一样:它不能被合并进 JS bundle,webpack 5 的处理方式是把它当作资源(asset)随 bundle 一起复制到输出目录,然后在运行时用process.dlopen动态加载。webpack 仓库的nodejs-addons示例完整演示了这条路径,本文基于该示例(examples/nodejs-addons/README.md)说明配置、加载代码和验证方式。本仓库为 webpack 5(package.json 中版本 5.110.3),目标是node环境。
示例文件及其作用
nodejs-addons 目录下的文件分工如下:
| 文件 | 作用 |
|---|---|
| example.js | 入口代码:用dlopen加载./file.node,调用原生方法 |
| webpack.config.js | 构建配置:target: "node",.node按asset/resource处理 |
| file.cc | 原生 addon 的 C++ 源码,注册hello方法 |
| binding.gyp | 构建描述:target_name为file,sources为file.cc |
| file.node | 已编译好的 addon 产物,仓库已直接提供 |
file.cc 中的InitModule通过NODE_SET_METHOD(exports, "hello", Method)注册了hello方法,Method返回字符串"world";它按 binding.gyp 的描述编译为.node。仓库中已带有编译好的file.node,所以可以直接进入 webpack 打包环节,无需自己重新编译。
配置 webpack 打包 .node 文件
webpack.config.js 的完整内容:
"use strict"; /** @type {import("webpack").Configuration} */ const config = { // mode: "development" || "production", target: "node", output: { // We strong recommend use `publicPath: 'auto'` or do not set `publicPath` at all to generate relative URLs // publicPath: 'auto' }, module: { rules: [ { test: /\.node$/, type: "asset/resource" } ] } }; module.exports = config;三个关键配置点:
target: "node":本场景必须项,声明构建产物运行在 Node 环境。output.publicPath:配置中的注释强烈建议使用publicPath: 'auto'或不设置publicPath,以生成相对 URL。运行时dlopen需要基于 bundle 文件所在位置去解析.node的相对地址,相对 URL 才能保证可解析。module.rules中的{ test: /\.node$/, type: "asset/resource" }:让 webpack 不再把.node当 JS 解析,而是作为资源原样复制到输出目录,输出文件名带内容 hash。
运行时用 dlopen 加载 addon
入口代码 example.js 的完整内容:
import { dlopen } from 'node:process'; import { fileURLToPath } from 'node:url'; const file = new URL("./file.node", import.meta.url); const myModule = { exports: {} }; try { dlopen(myModule, fileURLToPath(file)); } catch (err) { console.log(err) // Handling errors } console.log(myModule.exports.hello()); // Outputs: worlddlopen从node:process导入,fileURLToPath从node:url导入,把 URL 对象转成文件系统路径。new URL("./file.node", import.meta.url)是这套方案的核心写法。由于输出后的.node文件名带内容 hash(文档示例中为5664f09ab8adf033e173.node),bundle 里不能再引用源码中的file.node原名。webpack 会识别这个new URL请求并把它替换为实际输出的资源地址,所以源码里写源文件名、运行时拿到的却是 hash 后的真实路径。这也是配置注释推荐相对publicPath的原因。dlopen(myModule, fileURLToPath(file))成功后,addon 的导出会写入myModule.exports,示例随后调用myModule.exports.hello()。- 加载失败的处理方式如示例所示:
try/catch捕获并打印错误,具体错误信息需要按运行时的实际输出排查。
执行打包
仓库 examples/README.md 在 “Building an Example” 一节给出了构建示例的通用步骤:在项目根目录依次执行yarn、yarn setup、yarn add --dev webpack-cli,再进入具体示例目录构建。不过nodejs-addons目录没有包含build.js脚本,该步骤不直接适用于本示例;根目录的npm run build:examples(由 examples/buildAll.js 驱动)会在每个示例目录依次执行node build.js,是面向全部示例的批量入口,同样不适合作为单任务构建路径。
仓库内实际用于编译各示例的是测试管线 test/Examples.test.js:它读取每个示例目录的webpack.config.js作为 options,未设置entry时默认使用./example.js,输出目录为dist/,publicPath设为"dist/"。在自己的项目里可以用同样的 webpack Node API 方式构建,以下脚本按该测试管线的做法整理:
// build.js(放在示例目录下,./webpack.config.js 即上一节的配置) "use strict"; const path = require("path"); const webpack = require("webpack"); const options = require("./webpack.config.js"); options.context = __dirname; options.output = options.output || {}; options.output.path = path.join(__dirname, "dist"); options.output.publicPath = "dist/"; if (!options.entry) options.entry = "./example.js"; webpack(options, (err, stats) => { if (err) { throw err; } if (stats.hasErrors()) { console.log(stats.toString({ all: false, errors: true, errorDetails: true, errorStacks: true })); return; } console.log("compiled"); });在示例目录执行:
node build.js验证结果
编译产物
nodejs-addons/README.md 记录了两种模式下编译完成的 stdout 输出,以下为文档示例,具体大小和 hash 值以你本地的实际输出为准。Unoptimized(未压缩):
asset 5664f09ab8adf033e173.node 16.5 KiB [emitted] [immutable] [from: file.node] (auxiliary name: main) asset output.js 5.75 KiB [emitted] (name: main) chunk (runtime: main) output.js (main) 457 bytes (javascript) 16.5 KiB (asset) 1.12 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 1.12 KiB 6 modules dependent modules 16.5 KiB (asset) 126 bytes (javascript) [dependent] 3 modules ./example.js 331 bytes [built] [code generated] [no exports] [used exports unknown] entry ./example.js main webpack X.X.X compiled successfullyProduction mode:
asset 5664f09ab8adf033e173.node 16.5 KiB [emitted] [immutable] [from: file.node] (auxiliary name: main) asset output.js 463 bytes [emitted] [minimized] (name: main) chunk (runtime: main) output.js (main) 457 bytes (javascript) 16.5 KiB (asset) 264 bytes (runtime) [entry] [rendered] > ./example.js main runtime modules 264 bytes 2 modules dependent modules 16.5 KiB (asset) 42 bytes (javascript) [dependent] 1 module ./example.js + 2 modules 415 bytes [not cacheable] [built] [code generated] [no exports] [no exports used] entry ./example.js main webpack X.X.X compiled successfully判断要点:.node以独立 asset 的形式[emitted],标记[from: file.node]表明来源,同时有output.js输出,最后一行为webpack X.X.X compiled successfully。
运行时
用 Node 运行产物output.js,示例最后一行打印myModule.exports.hello()的返回值,example.js 中的注释说明输出为:
world编译输出与运行时打印都符合上述预期,说明.node被正确复制、且dlopen加载成功。
限制与注意事项
target: "node"是本场景的必要配置,缺失时不会按 Node 环境处理产物。- 输出的
.node文件名是内容 hash,运行时的加载地址必须来自 webpack 替换后的资源地址(即源码中的new URL写法),不要硬编码file.node去访问输出目录。 - 仓库提供的是预编译好的
file.node二进制产物;若要换成自己的原生 addon,需要自行准备好对应的.node文件。README 只链接了 Node.js 官方 addons 文档来说明 addon 本身,未给出重新编译命令。 dlopen加载失败时,示例的处理方式是try/catch打印错误;README 未列举具体错误信息,加载失败时以实际打印的错误为准。- 本示例的配置未显式写
entry,仓库的示例管线默认取./example.js;自行构建时建议显式指定entry,避免歧义。
完成以上步骤后,你可以检查两个结果:编译统计中.node以独立资源形式输出且编译成功,运行时 bundle 打印出 addon 方法的返回值world。
【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考