Vue3 来了,Vue3 + Vite这套组合拳大家也津津乐道。
提起 Vite 2.0,我们熟知就是快,而快基于 esbuild,数据表明要快百倍以上,我自己感受呢,还不止。
考虑 esbuild 比较新,Vite 基于 esbuild 也只用于开发时,构建生产环境还是比较熟悉的 Rollup 模式,所以大家平时也接触不到。
以下是重点,写组件库的家人们,在转为 esm 版本时,esbuild 是可用的。
以我在写的 Vue3 组件库为例子(最小颗粒度说明):
0. 回顾老版本
老版本是参考了当时 Element UI 库的,因为入口文件太多的问题,为了防止超时,以4个一组开子进程跑。构建方案用的 rollup。每跑一次的时间呢,我觉得喝个下午茶都够了,在 Github Action 上面大概是6分钟。
1. 从安装开始
npm i -D esbuild @vue/compiler-sfc
说一下 @vue/compiler-sfc 这个,这就是解析 .vue 文件的模块。
2. 写个 script
const { resolve } = require('path')
const { build } = require('esbuild')
const pkg = require('../package.json')
const tss = require('./ts-files.json')
const deps = Object.keys(pkg.dependencies)
const runBuild = async () => {
const entryPoints = {}
tss.forEach(name => {
entryPoints[name] = resolve(__dirname, `../src/${name}.ts`)
})
await build({
entryPoints: entryPoints,
external: ['vue', '../*', './*', ...deps],
outdir: `es/`,
format: 'esm',
bundle: true,
target: ['es2018']
})
}
runBuild()
其中 tss 就是所有要打包的文件列表,esm 模式需要全部单独打包,我这有100多个,性能差距可以特别明显。
3. 写个 vue 文件处理插件
3.1 了解 esbuild 插件原理
个人感觉 esbuild 插件比起 webpack,rollup 还是简单不少,其中最核心的就是 onResolve 和 onLoad 两个方法:
const vuePlugin = options => {
name: "vue",
setup(build) {
// ....
}
}
其中 name 就是插件的名称,setup 则是写主要处理逻辑的,options 传入一些配置参数的,往下看两个核心方法:
// filter 可以过滤不需要的引用地址,如果下面就是只要 .vue 文件
build.onResolve({ filter: /\.vue$/ }, args => {
return {
path: args.path, // 重定向路径可用,这里不需要,原路径返回
namespace: 'vue', // 告知 vue 插件也就是自身去处理
pluginData: { resolveDir: args.resolveDir } // 插件传递参数
}
})
// 对读取的内容
build.onLoad({ filter: /\.vue$/, namespace: 'vue' }, async args => {
const filePath = path.resolve(args.pluginData.resolveDir, args.path)
// 读取vue文件
const content = await fs.promises.readFile(filePath, 'utf8')
// 处理 .vue文件 具体逻辑可以往下看
...
return {
contents, // 返回处理后的代码
resolveDir,
loader: isTS ? 'ts' : 'js' // 告知 esbuild 用哪个loader 构建返回的代码
}
})
3.2 处理 .vue 文件
举个简单的例子 hello.vue :
<template>
<div>{{ hello }}</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: "hello",
setup() {
const hello = ref('hello world')
return {
hello
}
}
});
</script>
构建后的 hello.js :
import { ref, defineComponent } from 'vue'
var script = defineComponent({
name: "hello",
setup() {
const hello = ref('hello world')
return {
hello
}
}
});
import { resolveComponent, openBlock ... } from "vue"
function render(_ctx, _cache) {
return ...
}
script.render = render
export default script
原来是在 <script> 的默认导出中加个通过 <template> 内容转化后 render 函数。那 .vue 转 .ts 就可以这么做:
// 读取vue文件
const content = await fs.promises.readFile(filePath, 'utf8')
// 通过 @vue/compiler-sfc 解析 vue 文件区分 script 和 template,当然还有样式啥的本文不聊
const sfc = compiler.parse(content)
// 判断是否 lang="ts"
const isTS = sfc.descriptor.script?.lang === 'ts'
// 开始组合成新ts
let contents = ``
// 读取 <script> 标签的内容
if (sfc.descriptor.script) {
// "export default" 改为 "var script = " 方便加入 render 函数
contents += compiler.rewriteDefault(
sfc.descriptor.script.content,
'script'
)
} else {
contents += `var script = {};`
}
// 读取 <template> 标签的内容
if (sfc.descriptor.template) {
// 将我们熟知的模板转为 js 形式
contents += compiler.compileTemplate({
id: md5(filePath),
source: sfc.descriptor.template.content,
filename: filePath,
isProd:
process.env.NODE_ENV === 'production' ||
process.env.BUILD === 'production',
slotted: sfc.descriptor.slotted
}).code
// 把 render 函数加入到 script 中
contents += `
script.render = render;
`
}
// 上面把 export default 替换掉了,下面当然要加回来
contents += `
export { script as default };
`
这里没有写对 script setup 的处理,因为他依赖 @vue/compiler-sfc 一个比较核心的方法 compileScript ,需要多一层转化。
最终完整的插件代码可以 查看Github 。
4. 完整的 build
把上面写的插件引入之前写的 script :
// build.components.js
const { resolve } = require('path')
const { build } = require('esbuild')
const vuePlugin = require('./esbuild-plugin-vue')
const pkg = require('../package.json')
const tss = require('./ts-files.json')
const deps = Object.keys(pkg.dependencies)
const runBuild = async () => {
const entryPoints = {}
tss.forEach(name => {
entryPoints[name] = resolve(__dirname, `../src/${name}.ts`)
})
await build({
plugins: [vuePlugin()],
entryPoints: entryPoints,
external: ['vue', '../*', './*', ...deps],
outdir: `es/`,
format: 'esm',
bundle: true,
target: ['es2018']
})
}
runBuild()
执行一下 node build.components.js,100多个文件2秒完成,欣慰啊。
5. 加入类型声明
esbuild 快的缺点就是构建没有类型声明,既然这么快了,就自己打包类型声明吧。
5.1 安装依赖
pnpm i -D vue-tsc vite
看到这两是不是很熟悉,没错就是使用 Vite 时看到那句熟悉的 vue-tsc --noEmit && vite build。
vue-tsc 在 tsc 基础上加入对 .vue 文件的解析,所以使用方法可以参考 tsc。
5.2 编写 tsconfig.json
再来写一个生成声明文件的 tsconfig.json。
# tsconfig.declaration.json
{
"compilerOptions": {
"target": "es2018",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true, // 如果用到一些第三方库需要设置为 true
"lib": ["esnext", "dom"],
"types": ["vite/client"], // 这里就用到 vite 了,主要是用 vite 的全局声明来帮助 tsc 识别模板
"declaration": true, // 输出声明文件
"emitDeclarationOnly": true, // 只要声明文件
"declarationDir": "./es", // 声明文件输出目录,跟上面构建对应
},
"files": ["./src/index.ts"], // 入口文件,因为我的index.ts有引入所有组件,所以没写一大列
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] // 涵盖解析的范围
}
5.3 package.json 加个 script
{
"scripts": {
"build:declaration": "vue-tsc --project tsconfig.declaration.json",
}
}
执行
npm run build:declaration
声明 Done。