2022 年本人投入了 Vite 的怀抱,开始参与到 Vite 社区中,陆续开发了一些插件。
Vite 秉承了开箱即用,简化配置的思路,确实显著提升了前端开发体验。
但是在类库模式的构建上却有所欠缺,只能处理单个输入和单输入出的情况,构建场景单一,Vite 社区上目前也没有可直接使用的工具,所以才有了开发一个统一构建插件的想法。
目前 vite-plugin-build 插件已可以直接使用,也录入了 Vite 官方 awesome-vite,希望也刚好能满足一些人的需要。
什么是统一构建?
因为没有特别好的叫法,本人暂且把这叫做统一构建,本人把统一构建归纳为如下构建:
-
Bundle 构建
即
Vite
(也是 Rollup)的库打包模式,单输入文件,单输出 bundle 文件,如果没有设置外部依赖(external)所有涉及的依赖包都会打包到一个 bundle 文件中。优点:支持 umd 格式,浏览器中可作为外部依赖,不受业务代码 bundle 影响,可利用浏览器缓存机制,提高加载性能。
缺点:不支持 Tree Shaking 没有使用到的代码也会加载进来,由于打包到一个 bundle 文件,本地源码可读性差。
-
文件夹构建(文件到文件转换器,file-to-file transformer)
文件夹所有的符合格式的文件(
['ts', 'tsx', 'js', 'jsx', 'vue', 'svelte']
)会转换为对应同名的.js
文件,只支持commonjs
和es
格式。转换的时候,所有的
import
依赖包不会打包进来,根据需要转换的格式转换为commonjs
的require
或者es
的import
语法。优点:es 模式可支持 Tree Shaking,本地源码可读性高。
缺点:代码在 Webpack、Vite 这些构建工具中会和业务代码一起打包到 bundle 文件中,很难利用跨站点缓存优势。
-
生成 TypeScript 声明文件
支持原生 TypeScript、Vue TypeScript 和 Svelte TypeScript 声明文件的生成(如果有其他类型的框架也可以在此拓展)。
vite-plugin-build
通过 Vite 配合使用 @vitejs/plugin-react
、@vitejs/plugin-vue
、@sveltejs/vite-plugin-svelte
可支持上面的三种构建方式。
为什么要开发一个 Vite 统一构建插件?
理由一,Vite 构建场景单一,不支持如下场景:
- 多输入多输出(多输入多 bundle)
- 转换文件夹(文件转文件的转换方式,不打包成一个 bundle 文件)
- 生成 TypeScript 声明文件
理由二,Vite 社区缺乏可直接代替的工具。
Vite Github 上官方插件库使用的是 unbuild,是一款统一构建的工具,虽然挺方便的,但是 unbuild 更倾向于处理纯 JavaScript 或者 TypeScript 的代码,对于 React、Vue、Svelte 等浏览器 UI 类型相关的打包缺乏相关的转换处理。
理由三,Vite 体系一条龙服务。
Vite 统一构建插件,可在一些场景下让 Vite、Vitest 形成以个闭环体系,无需用到其他的构建和单元测试工具,一个 Vite 配置文件闯天下。
萌生想法
Vite
还没有兴起的之前,公司组内业务组件和个人的一些 Github 项目一开始是使用 Rollup
进行构建,Rollup
并不能开箱即用,还需要各种插件配置,相对较繁琐。
后来使用 Vite
的库模式来代替原生的 Rollup
可以减少不少的插件配置,不过整个文件夹的所有文件单独转换为 commonjs 和 es 的格式,还是需要通过 Vite
提供的 build
API 实现,如下是通过指定文件夹下的所有目标文件,然后遍历运行 build
方式实现(还需要多一个 Vite 配置文件来配置):
const fs = require('fs');
const path = require('path');
const spawn = require('cross-spawn');
const srcDir = path.resolve(__dirname, '../src');
// 所有 src 文件夹包括子文件夹的 js、ts、jsx、tsx 文件路径数组
const srcFilePaths = getTargetDirFilePaths(srcDir);
srcFilePaths.forEach((file) => {
const fileRelativePath = path.relative(srcDir, file);
spawn(
'npm',
['run', 'vite', '--', 'build', '--mode', fileRelativePath, '--outDir', 'es', '--config', './vite.file.config.ts'],
{
stdio: 'inherit',
},
);
});
同时还需要配置 npm run tsc
生成声明文件,pacakge.json
scripts
字段比较繁琐:
{
"scripts": {
"tsc:es": "tsc --declarationDir es",
"tsc:lib": "tsc --declarationDir lib",
"tsc": "npm run tsc:lib && npm run tsc:es",
"vite": "vite",
"build:lib": "vite build",
"build:file": "node ./scripts/buildFiles.js",
"build": "npm run tsc && npm run build:file && npm run build:lib"
}
}
新的项目,就直接复制修改一下,虽然也能达到构建的目的,但是就是不够方便,本人懒,所以还是想有没有更简单点的方式?如下方使用一个 script
就可以解决?
{
"scripts": {
"build": "vite build"
}
}
实现思路
首先完全通过 Vite 配置文件是无法实现统一构建的功能,即通过正常运行一次 Vite 构建服务无法实现统一构建的功能。
还是得多次运行 Vite 构建服务来实现此功能(表面上使用者无感知)。
实现的关键点
Bundle 构建
Vite 库模式就是 bundle 构建模式,不过只能设置一个入口文件,一个 bundle 输出文件。
实际场景可能需要多个入口文件,多个 bundle 输出文件,这个功能不复杂,通过遍历多个 vite build
构建即可实现,代码大致如下:
import { build } from 'vite';
const buildPs = lastBuildOptions.map((buildOption) => {
return build({
...viteConfig, // 透传 vite.config.ts 或者 vite.config.js 的用户配置,插件需要过滤自身(vite-plugin-build)
mode: 'production',
configFile: false,
logLevel: 'error',
build: buildOption,
});
});
await Promise.all(buildPs);
文件夹构建
实现思路也不复杂,如下:
- 获取文件夹中所有符合的文件路径
- 遍历所有文件路径,运行
vite build
构建 - 由于 Vue 和 Svelte 的 import 是需要带后缀名的,需要额外移除文件内容中的
.vue
和.svelte
后缀名。
简单代码实现大致如下:
import { build } from 'vite';
import fg from 'fast-glob';
const {
inputFolder = 'src',
extensions = ['ts', 'tsx', 'js', 'jsx', 'vue', 'svelte'],
ignoreInputs,
...restOptions,
} = options;
// 获取默认为项目根目录下 src 文件夹包括子文件夹的所有 js、ts、jsx、tsx、vue、sevele 文件路径,除开 .test.*、.spec.* 和 .d.ts 三种后缀名的文件
// 返回格式为 ['src/**/*.ts', 'src/**/*.tsx']
const srcFilePaths = fg.sync([`${inputFolder}/**/*.{${extensions.join(',')}}`], {
ignore: ignoreInputs || [`**/*.spec.*`, '**/*.test.*', '**/*.d.ts', '**/__tests__/**'],
});
const buildPromiseAll = srcFilePaths.map((fileRelativePath) => build({
...viteConfig, // 透传 vite.config.ts 或者 vite.config.js 的用户配置,插件需要过滤自身(vite-plugin-build)
mode: 'production',
configFile: false,
logLevel: 'error',
build: {
lib: {
entry: fileRelativePath,
...
},
...restOptions
},
}));
await Promise.all(buildPromiseAll);
await removeSuffix(); // 移除生成文件内容中的 `.vue` 和 `.svelte` 后缀名
TypeScript 声明文件生成
前端 UI 框架的多种多样,像 Vue、Svlete 这类有自身自定义的语法,TypeScript 的语法需要特殊支持,生成声明文件自然也需要特殊处理。
原生 TypeScript
原生的 TypeScript 官方直接提供 tsc
工具可直接使用,通过直接运行 tsc
bin 文件,并传递对应的配置即可实现。
简单的代码实现如下:
import spawn from 'cross-spawn';
const { rootDir, outputDir } = options;
const tscPath = path.resolve(require.resolve('typescript').split('node_modules')[0], 'node_modules/.bin/tsc');
spawn.sync(
tscPath,
['--rootDir', rootDir, '--declaration', '--emitDeclarationOnly', '--declarationDir', outputDir],
{
stdio: 'ignore',
},
);
Vue TypeScript
Vue 3 出来后,对 TypeScript 的支持就比较完善了,Vue 社区的 vue-tsc
可以用来替代 tsc
,用法保持和 tsc
一致。
有一点不一样的是 vue-tsc
生成的声明文件是 .vue.d.ts
后缀名的,所以需要重命名为 .d.ts
后缀名。
简单的代码实现如下:
import spawn from 'cross-spawn';
const { rootDir, outputDir } = options;
const vueTscPath = path.resolve(
require.resolve('vue-tsc/out/proxy').split('node_modules')[0],
'node_modules/.bin/vue-tsc',
);
spawn.sync(
vueTscPath,
['--rootDir', rootDir, '--declaration', '--emitDeclarationOnly', '--declarationDir', outputDir],
{
stdio: 'ignore',
},
);
if (isVue) {
renameVueTdsFileName(); // 重命名 .vue.d.ts 为 .d.ts
}
Svelte TypeScript
Svelte 社区没有像 Vue 那么强大,没有类似 vue-tsc
这样的工具,最终找到 svelte-type-generator 可以实现,参考 svelte-type-generator
的代码实现了,生成声明文件的功能(暂时不支持 tsc
cli 的功能)。
const { compile } = require('svelte-tsc');
const { rootDir, outputDir } = options;
compile({
rootDir,
declaration: true,
emitDeclarationOnly: true,
declarationDir: outputDir,
});
if (isVue) {
renameSvelteTdsFileName(); // 重命名 .svelte.d.ts 为 .svelte.d.ts
}
打印构建信息
由于是触发运行多个 vite build
所以如果直接输出默认的构建信息,那么会显得混乱,无法像运行一个 vite build
输出构建信息。
所以得拦截和隐藏原有的构建信息,自定义输出新的构建信息。
-
拦截和隐藏原有的构建信息,通过重写
console.log
和console.warn
可以达到目的,代码如下:export const restoreConsole = { ...console }; export class InterceptConsole { public log: typeof console.log; public warn: typeof console.warn; public clear: typeof console.clear; constructor() { const { log, warn } = console; this.log = log; this.warn = warn; } silent() { console.log = () => {}; console.warn = () => {}; } restore() { console.log = this.log; console.warn = this.warn; } }
-
自定义输出新的构建信息
功能参考 Vite 的内置 reporter 插件,通过对应的钩子函数,实现构建信息的输出。
最终效果
Github 例子
vanilla
vanilla-ts
react
react-ts
vue
vue-ts
svelte
svelte-ts
所有例子,运行如下命令即可
$ npm install
$ npm run build
Codesandbox 在线例子
- vanilla-ts
- react-ts
- vue-ts
- svelte-ts
后续
如果感兴趣的也可以加入一起建设,目前 svelte-tsc
实现和 vue-tsc
一样的用法,是有点挑战性的。
- 生成声明文件支持配置 tsconfig 配置文件路径
svelte-tsc
支持 bin 文件,功能参照vue-tsc
,支持所有tsc
的 cli 选项。