使用 vite 开发和构建 nodejs 应用
本文最后更新于:2024年3月25日 凌晨
场景
vite 作为现代 web 应用的构建工具,许多开发者将之用于开发 web 应用(react、vue),由于其本身的易用性和高性能,许多 web 框架甚至官方编写了插件(solid、astro),可以称得上是近年来 webpack 的成功的挑战者。但实际上,发展至今,vite 已经远不仅仅是一个 web 层的工具,它的周边生态正在蓬勃发展,衍生除了一系列周边的工具,在之前的 vite-不仅仅是一个构建工具 有所提及,这里专门针对开发 nodejs 应用来说明。
动机
vite 为什么适合开发 nodejs 应用?
首先,即便不使用 vite,可能也需要使用 vitest 之类的工具进行单元测试,使用 tsx/ts-node 运行源代码调试,使用 tsup/esbuild 将代码 bundle 为最终要运行的 js。那么,如果使用了 vite,这些事情将在同一个生态中完成。
- vitest: 单元测试工具,支持 esm,支持 ts
- vite-node: ts 代码的运行工具,支持 vite 的各种特性,例如
?raw
之类的 - vite: 将 nodejs 应用打包最终运行的 js,可以选择性的捆绑依赖
使用
vitest 和 vite-node 都是现成的,可以开箱即用,所以下面重点关注 vite 构建相关的事情。
首先安装依赖
1 |
|
vitest
参考:
vite-node
可以使用它替代 node 命令运行任何文件,它只是比 node 命令更加强大,包括
- 支持 ts/tsx 文件,也可以运行 esm/cjs 模块
- 在 esm 中有 cjs 的 polyfill,
__dirname
什么的可以直接使用 - 支持监视模式运行
- 支持 vite 本身的特性,例如
?raw
- 支持使用 vite 插件
例如创建一个 src/main.ts
文件
1 |
|
然后使用 vite-node 运行
1 |
|
解决热更新时 server 没有重启的问题
例如下面这个简单的 http server 示例
1 |
|
当使用 vite-node -w 监视模式运行时就可能出现丑陋的错误,因为 vite hmr 时并未杀死之前启动的服务器。
1 |
|
解决方法很简单,手动监视 hmr 并杀死之前的 server 即可。
1 |
|
vite
vite 想要构建 nodejs 应用,确实需要修改一些配置和插件,主要解决几个问题
- 为 esm 代码实现 cjs polyfill,包括
__dirname/__filename/require/self
- 正确捆绑 devDependencies 的依赖项,但排除 node/dependencies 的依赖
- 提供一个开箱即用的默认配置
然后我们分别解决这几个问题
构建时针对 cjs 特性做 polyfill
在 nodejs 中,可能会使用 __dirname
之类的全局变量,遗憾的是,esm 并不支持它。nodejs 推荐的做法是
1 |
|
可以使用 vite 插件在构建之后的代码最前面添加一些额外的代码,自动创建这些变量。
具体实现
安装 magic-string,用于修改代码保持 sourcemap。
1
pnpm i -D magic-string
然后在钩子
renderChunk
中添加 polyfill 代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import MagicString from 'magic-string'
function shims(): Plugin {
return {
name: 'node-shims',
renderChunk(code, chunk) {
if (!chunk.fileName.endsWith('.js')) {
return
}
// console.log('transform', chunk.fileName)
const s = new MagicString(code)
s.prepend(`
import __path from 'path'
import { fileURLToPath as __fileURLToPath } from 'url'
import { createRequire as __createRequire } from 'module'
const __getFilename = () => __fileURLToPath(import.meta.url)
const __getDirname = () => __path.dirname(__getFilename())
const __dirname = __getDirname()
const __filename = __getFilename()
const self = globalThis
const require = __createRequire(import.meta.url)
`)
return {
code: s.toString(),
map: s.generateMap(),
}
},
apply: 'build',
}
}
正确捆绑依赖项
由于 nodejs 应用中引入 fs 等 nodejs 模块很常见,而 vite 默认也会尝试捆绑它们,所以需要将它们作为外部依赖项。幸运的是已经有插件 rollup-plugin-node-externals,它可以排除 nodejs 和 dependencies 声明的依赖项,但需要针对 vite 做一些小的兼容性修改。
安装依赖
1
pnpm i -D rollup-plugin-node-externals
简单封装一下
1
2
3
4
5
6
7
8
9
10import { nodeExternals } from 'rollup-plugin-node-externals'
function externals(): Plugin {
return {
...nodeExternals(),
name: 'node-externals',
enforce: 'pre', // 关键是要在 vite 默认的依赖解析插件之前运行
apply: 'build',
}
}
添加默认配置
由于吾辈有很多 nodejs 项目,不希望每次都填写配置,而是通过约定 + 支持配置的方式来解决这个问题,所以简单实现一个 vite 共享配置的插件。
1 |
|
合并插件
最终,我们将这些插件合并到一起,产生一个新的插件
1 |
|
然后在 vite.config.ts 中使用
1 |
|
现在,我们可以使用 vite 构建 nodejs 应用了
1 |
|
享受 vite 带来的一切吧!
吾辈发布了一个 vite 插件 @liuli-util/vite-plugin-node,可以开箱即用的使用。
局限性
好吧,这里其实仍然存在一些问题,包括
vite 没有官方支持构建 node 应用,项目的主要目标也不是它– 至少可以说 vitest/nuxt 都在 node 层面依赖着 vitevite-plugin-node 还有许多问题,例如没有自动 polyfill– 已实现__dirname
之类的vite 相比 esbuild 的性能仍然差了一个数量级– 函数库已经大多数应用性能问题不大,人类感知不太能感知到- zx 构建之后无法运行 – chalk 没有正确的配置 – 无法识别
imports
中的node
字段 – 维护者不在乎,需要考虑更换为 ansi-colors,ref: https://github.com/chalk/chalk/issues/535 koa-bodyparser 构建之后存在问题 – 没有正确支持 esm – 等待 https://github.com/koajs/bodyparser/pull/152 合并
没什么选择是尽善尽美的,但吾辈现在选择 all in vite。
未来的目标
- 多入口点支持
- 生成类型定义