使用 Vite 开发和构建 Node.js 应用程序
语境
作为一款现代化的 Web 应用构建工具,Vite 已被众多开发者用于创建 Web 应用(例如 React 和 Vue)。凭借其易用性和高性能,许多 Web 框架甚至为其编写了官方插件(例如 Solid 和 Astro),使其近年来成功挑战了 Webpack 的地位。然而,Vite 的功能已远不止于 Web 层工具。其周边生态系统蓬勃发展,催生了一系列外围工具的开发。
动机
为什么Vite适合开发Node.js应用程序?
首先,即使不使用 Vite,也可能需要 vitest(用于单元测试)、tsx/ts-node(用于运行源代码进行调试)以及 tsup/esbuild(用于将代码打包成最终可执行的 js 文件)等工具。因此,如果使用 Vite,这些任务都可以在同一个生态系统中完成。
- vitest:一款支持 ESM 和 TypeScript 的单元测试工具。
- vite-node:一个用于运行 TypeScript 代码的工具,支持各种 Vite 功能,例如
?raw。 - Vite:将 Node.js 应用程序打包成最终要执行的 JavaScript 文件,并可以选择选择性地打包依赖项。
用法
Vitest 和 vite-node 开箱即用,因此本文重点介绍 Vite 的构建方面。
首先,安装依赖项。
pnpm i -D vite vite-node vitest
维特斯
创建一个单元测试文件,例如:src/__tests__/index.test.ts
import { it } from 'vitest'
it('hello world', () => {
expect(1 + 1).eq(2)
})
使用以下命令运行 vitest
pnpm vitest src/__tests__/index.test.ts
维特节点
它可以替代 node 命令来运行任何文件,并且比标准的 node 命令提供更多功能,包括:
- 支持 ts/tsx 文件和运行 esm/cjs 模块。
- ESM 中的 CJS polyfill,允许直接使用
__dirname,等等。 - 支持监视模式执行。
- 支持 Vite 自身的功能,例如
?raw。 - 支持使用 Vite 插件。
例如,创建一个文件src/main.ts
import { readFile } from 'fs/promises'
console.log(await readFile(__filename, 'utf-8'))
然后使用 vite-node 运行它
pnpm vite-node src/main.ts
维特
要使用 Vite 构建 Node.js 应用程序,需要修改一些配置和插件来解决一些关键问题:
- 实现 ESM 代码的 CJS polyfill,包括
__dirname、__filename、require和self。 - 正确打包,
devDependencies同时排除node和dependencies。 - 提供开箱即用的默认配置。
让我们逐一解决这些问题。
在构建过程中填充 CJS 功能
__dirname在 Node.js 中,经常使用全局变量。遗憾的是, ESM 不支持这些变量。Node.js 中推荐的做法如下:
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
您可以使用 Vite 插件在已构建代码的开头添加一些额外的代码,从而自动创建这些变量。
以下是具体实现方法:
- 安装此
magic-string程序可在保持源映射不变的情况下修改代码。
pnpm i -D magic-string
- 然后,在钩子函数中添加 polyfill 代码
renderChunk。
import MagicString from 'magic-string'
import { Plugin } from 'vite'
function shims(): Plugin {
return {
name: 'node-shims',
renderChunk(code, chunk) {
if (!chunk.fileName.endsWith('.js')) {
return null;
}
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({ hires: true }),
};
},
apply: 'build',
};
}
通过将此插件集成到您的 Vite 配置中,您将在构建过程中自动将这些 shim 注入到您的 JavaScript 文件中,从而在 ESM 环境中模拟 Node.js 的 CommonJS 环境。此解决方案简化了现有 Node.js 代码库向 ESM 模块系统的适配过程。
正确打包依赖项
在 Node.js 应用中,通常会包含诸如 `<module>` 之类的模块fs,而 Vite 默认也会尝试打包这些模块。因此,必须将它们视为外部依赖项。幸运的是,已经有一个名为rollup-plugin-node-externals的插件可以排除 Node.js 以及在 `<dependencies>`dependencies字段中声明的依赖项package.json。但是,为了使其与 Vite 无缝协作,还需要进行一些小的兼容性调整。
- 安装依赖项:
pnpm i -D rollup-plugin-node-externals
- 将其封装以使其与 Vite 兼容:
import { nodeExternals } from 'rollup-plugin-node-externals'
import { Plugin } from 'vite'
function externals(): Plugin {
return {
...nodeExternals({
// Options here if needed
}),
name: 'node-externals',
enforce: 'pre', // The key is to run it before Vite's default dependency resolution plugin
apply: 'build',
}
}
添加默认配置
鉴于 Node.js 项目的频繁出现,最好不要为每个项目重复配置。因此,我们采用约定优于配置的方法,并辅以自定义选项。由此,可以按如下方式创建用于通用配置的共享 Vite 插件的简单实现:
import path from 'path'
import { Plugin } from 'vite'
function config(options?: { entry?: string }): Plugin {
const entry = options?.entry ?? 'src/main.ts'
return {
name: 'node-config',
config() {
return {
build: {
lib: {
entry: path.resolve(entry),
formats: ['es'],
fileName: (format) => `${path.basename(entry, path.extname(entry))}.${format}.js`,
},
rollupOptions: {
external: ['dependencies-to-exclude']
// Additional Rollup options here
},
},
resolve: {
// Change default resolution to node rather than browser
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node'],
},
}
},
apply: 'build',
}
}
此设置提供专为 Node.js 项目量身定制的默认配置,从而简化开发流程。通过指定入口文件以及模块解析和构建输出的必要调整,此插件可为各种 Node.js 应用程序提供简便的构建设置。
插件组合
最后,我们将这些插件合并为一个,创建一个新的插件设置:
import { Plugin } from 'vite'
export function node(): Plugin[] {
return [shims(), externals(), config()]
}
vite.config.ts然后,在你的文件中使用它:
import { defineConfig } from 'vite'
import { node } from './path/to/your/plugin'
export default defineConfig({
plugins: [node()],
})
现在,您可以使用以下命令通过 Vite 构建 Node.js 应用程序:
pnpm vite build
尽情享受Vite提供的所有服务!
已发布了一个 Vite 插件@liuli-util/vite-plugin-node,开箱即可使用。
局限性
好的,这里仍然存在一些问题,包括:
-
Vite 官方并不支持构建 Node 应用程序,它的主要目标也不是这个。至少可以说,vitest/nuxt 在节点级别上都依赖于 Vite。 -
vite-plugin-node 仍然存在许多问题,例如无法自动进行 polyfill- 实施的。__dirname等。 -
与esbuild相比,Vite的性能仍然差一个数量级。——目前大多数库的性能问题并不严重,人眼几乎察觉不到。 -
使用 zx 构建失败,原因是 chalk 配置不正确。——具体来说,未能识别node字段imports。维护者缺乏兴趣表明应该考虑切换到 ansi-colors,正如本问题中所讨论的那样。 -
使用 koa-bodyparser 构建后出现问题——由于缺乏适当的ESM支持。等待合并此拉取请求。
没有哪个选择是完美的,但全力投入 Vite 是经过深思熟虑的决定。
未来目标
- [x] 支持多个入口点。
- [x] 类型定义生成。
选择专注于 Vite 意味着要权衡其当前的局限性和潜力,并根据其提供的整体优势做出战略决策,包括其不断增长的生态系统以及简化 Node.js 应用程序开发工作流程的能力。
文章来源:https://dev.to/rxliuli/developing-and-building-nodejs-applications-with-vite-311n
