Skip to content

Commit 18fe62a

Browse files
authored
Merge pull request #7 from vite-plugin/v2.0.0
V2.0.0
2 parents 496e441 + c51515a commit 18fe62a

File tree

19 files changed

+3955
-396
lines changed

19 files changed

+3955
-396
lines changed

.docs/esm-cjs.md

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# vite-plugin-native
2+
3+
> 2024-06-11
4+
5+
支持在 Node/Electron 中使用 C/C++ native addons. 这是一个基于 [Webpack](https://github.com/webpack/webpack) 的 bundle 方案。
6+
7+
> 感谢 [Erick Zhao](https://github.com/erickzhao) 提供灵感 :)
8+
9+
[![NPM version](https://img.shields.io/npm/v/vite-plugin-native.svg)](https://npmjs.org/package/vite-plugin-native)
10+
[![NPM Downloads](https://img.shields.io/npm/dm/vite-plugin-native.svg)](https://npmjs.org/package/vite-plugin-native)
11+
12+
[English](./README.md) | 简体中文
13+
14+
## Install
15+
16+
```bash
17+
npm i -D vite-plugin-native
18+
```
19+
20+
## Usage
21+
22+
```javascript
23+
import native from 'vite-plugin-native'
24+
25+
export default {
26+
plugins: [
27+
native(/* options */)
28+
]
29+
}
30+
```
31+
32+
## API
33+
34+
```ts
35+
export interface NativeOptions {
36+
/**
37+
* Where we want to physically put the extracted `.node` files
38+
* @default 'dist-native'
39+
*/
40+
outDir?: string
41+
/**
42+
* - Modify the final filename for specific modules
43+
* - A function that receives a full path to the original file, and returns a desired filename
44+
* - Or a function that returns a desired file name and a specific destination to copy to
45+
* @experimental
46+
* @todo better calculation value of `id` automatically
47+
*/
48+
map?: (mapping: {
49+
/** `.node` file path */
50+
native: string
51+
/** require id of `.node` file */
52+
id: string
53+
/** `.node` file output location */
54+
output: string
55+
}) => typeof mapping
56+
/**
57+
* - Use `dlopen` instead of `require`/`import`
58+
* - This must be set to true if using a different file extension that `.node`
59+
*/
60+
dlopen?: boolean
61+
/**
62+
* If the target is `esm`, so we can't use `require` (and `.node` is not supported in `import` anyway), we will need to use `createRequire` instead
63+
* @default 'cjs'
64+
*/
65+
target?: 'cjs' | 'esm'
66+
}
67+
```
68+
69+
## 工作原理
70+
71+
### esm 与 cjs
72+
73+
esm 标准导入模块的方式,是使用写在 js 文件顶层作用域 import 语句,并且模块 id 必须是一个固定的字符串常量。这使得所有行为都是可预测的。这对于 bundler 十分的友好,甚至可以非常轻松的实现 tree-shake 功能。其次导出的模块成员必须是具名的,默认为名字为 default。
74+
75+
cjs 标准导入模块的方式,是使用 require 函数导入模块,require 函数与普通的 js 函数没有什么区别,这就决定了 require 函数可以写在任意的作用域且导入模块 id 可以是任何的 js 变量、字符串常量、表达式。这与 import 语句有着本质的区别,也就是说 cjs(require) 的模块规范要比 esm(import) 模块灵活的多,甚至导出模块成员可以是匿名的;这样的行为差异决定了很多时候一个 cjs 模块是无法有效的转换为 esm 模块的,它们的运行行为差异非常大。
76+
77+
esm 与 cjs 之间的相互导入/导出的转换是所有 bundler/transpiler 都会面临的一个问题,如 Vite、Webpack、esbuild、swc、tsc 等等。关于模块转换有个专有名词叫 interop。
78+
79+
> 我之前在 B 站讲过一个视频 👉🏻 [细讲⚡️vite-plugin-commonjs(上)](https://www.bilibili.com/video/BV1gm4y1e7zK/?vd_source=44b643ef038990a11abb9118def2ef80)
80+
81+
### 作用域 与 同步/异步
82+
83+
Vite(Rollup) 对于构建顶层作用域中使用 import 语句导入的模块是没有作用域概念的,代码的 bundle 规则是基于 export 导出成员的名字,它要求模块作者遵循 esm 规范。这样做理论上不会产生什么副作用,因为模块所有导出成员均在**顶层作用域**且为**同步行为**,所以这就确定了一切行为都是可预测的。
84+
85+
当然如果你使用 import() 函数导入模块,它更像一个普通的 js 函数,并且将不会有顶层作用域限制。导入语句可以写在**任意作用域**且模块导入永远为**异步行为**。Vite(Rollup) 默认是不会将 import() 函数导入的模块合并到 bundle.js 中,如果强行合并到 bundle.js 中可能会导致一些不可预测的副作用,例如用户在被 import() 导入的模块中的顶层作用域立即执行了一些逻辑,这可能会与用户想象中的行为不一致,因为模块会被立即执行!导致执行时机被提前,而不是 bundle.js 由上至下至执行到 import() 函数时候才被加载并执行 👉🏻 [output.inlineDynamicImports](https://github.com/rollup/rollup/blob/v4.17.1/docs/configuration-options/index.md#outputinlinedynamicimports)
86+
87+
### require 与 import/import()
88+
89+
我们知道了模块导入的作用域,同步/异步的概念后,这将决定了 Vite(Rollup) 能否能顺利的将 require 与 import/import() 转换成功,也即 interop 问题。
90+
91+
<table>
92+
<thead>
93+
<th>Statement</th>
94+
<th>Sync</th>
95+
<th>Async</th>
96+
<th>Scope</th>
97+
</thead>
98+
<tbody>
99+
<tr>
100+
<td>import</td>
101+
<td>✅</td>
102+
<td>❌</td>
103+
<td>Global</td>
104+
</tr>
105+
<tr>
106+
<td>import()</td>
107+
<td>❌</td>
108+
<td>✅</td>
109+
<td>Global / Anywhere</td>
110+
</tr>
111+
<tr>
112+
<td>require()</td>
113+
<td>✅</td>
114+
<td>✅ (Wrap with Promise.resolve())</td>
115+
<td>Global / Anywhere</td>
116+
</tr>
117+
</tbody>
118+
</table>
119+
120+
### 模块互操作(interop)
121+
122+
import 语句与 import() 函数很容易转换成 require 且无任何的副作用。
123+
124+
```js
125+
// Global scope
126+
127+
import foo from './foo'
128+
↓↓↓↓ ✅
129+
const { default: foo } = require('./foo')
130+
131+
import * as foo from './foo'
132+
↓↓↓↓ ✅
133+
const foo = require('./foo')
134+
135+
import { bar, baz } from './foo'
136+
↓↓↓↓ ✅
137+
const { bar, baz } = require('./foo')
138+
139+
function func(name) {
140+
// Function scope
141+
142+
const foo = import(`./${name}`)
143+
↓↓↓↓ ✅
144+
const foo = Promise.resolve(require(`./${name}`))
145+
}
146+
```
147+
148+
require 函数换成 import 语句可能会失败!
149+
150+
```js
151+
// Global scope
152+
153+
const foo = require('./foo') // 需要配合 Object.defineProperty(exports, "__esModule", { value: true });
154+
↓↓↓↓ 🚧
155+
import foo from './foo'
156+
157+
const foo = require(`./${name}`)
158+
↓↓↓↓ ❌
159+
import foo from `./${name}` // 不支持使用变量
160+
161+
function func(name) {
162+
// Function scope
163+
164+
const foo = require(`./${name}`)
165+
↓↓↓↓ ❌
166+
const foo = import(`./${name}`) // `require` 是同步 API
167+
}
168+
```
169+
170+
### 中心化模块管理
171+
172+
Webpack/esbuild 的 bundle 方案支持使用 require 函数导入模块并且可以**无视作用域**。他们能作到这点的主要原因都是采用了模块集中管理的策略;在 Webpack 中所有模块都被挂在到一个名为 `__webpack_modules__` 的变量上并且使用统一 `__webpack_require__` 加载函数加载,在 esbuild 中所有模块均被包裹在一个 `__commonJS` 模块管理函数中,并返回该模块的加载函数。
173+
174+
### Vite 的预处理
175+
176+
Vite 中有个概念是 [Pre-Bundling](https://vitejs.dev/guide/dep-pre-bundling.html#dependency-pre-bundling),它有一个很重要的作用就是将 cjs 模块提前构建成一个 bundle.js 然后通过 esm 的形式导出模块,这很好的解决了 interop 的问题。试想一下面有一段包含条件导入模块的代码段,让我们看看预构建是如何处理它的。
177+
178+
```js
179+
// add.js
180+
exports.add = (a, b) => a + b
181+
182+
// minus.js
183+
exports.minus = (a, b) => a - b
184+
185+
// math.js
186+
exports.calc = (a, b, operate) => {
187+
const calc = operate === '+' // condition require
188+
? require('./add').add(a, b)
189+
: require('./minus').minus(a, b)
190+
return calc(a, b)
191+
}
192+
```
193+
194+
**Output a `bundle.js` file with Vite's Pre-Bundling**
195+
196+
```js
197+
var __commonJS = (cb, mod = { exports: {} }) => function __require2() {
198+
const cjs_wrapper = Object.values(cb)[0] // wrapper function
199+
cjs_wrapper(mod.exports, mod); // inject exports, module
200+
return mod.exports;
201+
};
202+
203+
// add.js
204+
var require_add = __commonJS({
205+
"add.js"(exports, module) {
206+
exports.add = (a, b) => a + b
207+
}
208+
})
209+
210+
// minus.js
211+
var require_minus = __commonJS({
212+
"minus.js"(exports, module) {
213+
exports.minus = (a, b) => a - b
214+
}
215+
})
216+
217+
// math.js
218+
var require_math = __commonJS({
219+
"math.js"(exports, module) {
220+
exports.calc = (a, b, operate) => {
221+
const calc = operate === '+' // condition require
222+
? require_add().add(a, b)
223+
: require_minus().minus(a, b)
224+
return calc(a, b)
225+
}
226+
}
227+
});
228+
229+
// Finally export a esmodule ✅
230+
export default require_math(); // { calc: Function }
231+
```
232+
233+
### Vite 与 Webpack 产物
234+
235+
假设我们有如下两个文件分别为 add.js 与 index.js:
236+
237+
**add.js**
238+
239+
```js
240+
export function add(a, b) {
241+
return a + b
242+
}
243+
```
244+
245+
**index.js**
246+
247+
```js
248+
import { add } from './add'
249+
250+
const sum = add(1, 2)
251+
```
252+
253+
Vite(Rollup) 默认只支持 esm 格式的文件,也就是模块引用需要使用 import 语句。这个设定很好,因为它是 ECMAScript 的模块标准;同样这会带来一些天然的好处比如很容易支持 tree-shake 这是因为 import/export 语句规定所有导入/导出模块必须写在 js 文件顶层作用域,也即它们的行为可预测的。Rollup 的构建产物给人最直观的感觉就是**所见即所得**,非常符合代码书写的顺序与直觉。
254+
255+
**Bundled with Vite**
256+
257+
```js
258+
// add.js
259+
function add(a, b) {
260+
return a + b
261+
}
262+
263+
// index.js
264+
const sum = add(1, 2)
265+
```
266+
267+
Webpack 的构建产物是以遵循 cjs 规范的方式组织代码,所以有 **模块中心(modules)、模块导出挂载点(module.exports)、模块加载函数(require)** 等概念。而这些概念在 Rollup 中通通没有;这是两者之间最大的区别,也即两者对模块导入/导出处理的不同。
268+
269+
**Bundled with Webpack**
270+
271+
```js
272+
var __webpack_modules__ = {
273+
// index.js
274+
0: (module, exports, __webpack_require__) => {
275+
const { add } = __webpack_require__(1)
276+
const sum = add(1 + 2)
277+
},
278+
// add.js
279+
1: (module, exports, __webpack_require__) => {
280+
function add(a, b) {
281+
return a + b
282+
}
283+
module.exports = { add };
284+
},
285+
...
286+
}
287+
288+
module.exports = __webpack_require__(0)
289+
```
290+
291+
Webpack 首先会为所有会为所有模块提供一个 cjs 外壳代码,并且注入 模块挂载点(module, exports),模块引入函数(require)。这与 node.js 的模块加载行为完全一致,可以说 Webpack 十分适合构建 Node/Electron 应用,这个场景天然优于使用 Vite(Rollup) 构建。
292+
293+
### 重头戏 C/C++ 模块
294+
295+
C/C++ 扩展是为了 Node/Electron 准备的高性能方案。社区对于构建 C/C++ 模块大部分情况下会使用 node-pre-gyp,node-gyp-build 等工具构建成为 .node 文件,然后放到固定的目录结构中,并且提供一个 bindings 工具用于加载 .node 文件,通常它们可能是这样的:
296+
297+
```js
298+
// node_modules/sqlite3/build/Release/node_sqlite3.node
299+
require('bindings')('node_sqlite3.node');
300+
// node_modules/better-sqlite3/build/Release/better_sqlite3.node
301+
require('bindings')('better_sqlite3.node');
302+
```
303+
304+
C/C++ 构建的 .node 文件本质上是一个 cjs 模块,无论怎样它都不支持使用 import 语句或者 import() 函数加载,所以我们只能使用 require 函数加载它。<!-- 也就是说我们最好以 cjs 格式构建 bundle.js,但是如果我们使用 esm 格式构建 bundle.js 我们需要使用 createRequire 函数创建一个 require 函数并且加载 .node 文件。-->使用 Webpack 工具构建在兼容性上具有天然的优势,因为它们都基于 cjs 格式。且 Webpack 还具有丰富的插件系统。如果使用 Webpack 将 C/C++ 模块构建成一个 bundle.js 并且以一个 esm 模块格式导出,这既能享受 Webpack 强大成熟的生态系统又能做到兼容 Vite(Rollup)。你可以想到这个方法和上面讲到的 预构建(Pre-Bundling) 思路如出一辙,事实却是如此!
305+
306+
## 为什么是 Webpack
307+
308+
<!--
309+
首先我们从上面的说明中关于 C/C++ 模块的支持能总结出两个点:
310+
311+
1. 必须是 bundle 方案,并且以 cjs 格式构建
312+
2. 模块需要被一个模块中心管理,这样才能避免 作用域以及同步/异步 带来的副作用问题
313+
-->
314+
315+
你可能已经意识到 Vite 的 Pre-Bundling 的行为与 Webpack 的行为趋同,为什么我们不直接使用 Pre-Bundling 构建 C/C++ 模块,而且它也是基于 cjs 格式的 bundle 方案。主要是 Webpack 的生态优势,这能让我们少做很多事情且低风险。

0 commit comments

Comments
 (0)