正如 rollup
中的插件名称约定一样,vite
插件也需要一定的命名规范:
vite-plugin-
前缀package.json
中包含 vite-plugin
关键字Vite
专属插件的详细说明如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:
vite-plugin-vue-
前缀作为 Vue 插件vite-plugin-react-
前缀作为 React 插件vite-plugin-svelte-
前缀作为 Svelte 插件该插件要达到的效果是(以 jQuery
为例):
CDN
加载 jQuery
资源,不用包管理工具<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
这时候全局 window
对象就有 jQuery
或者 $
属性了
vue
组件中导入 jQuery
虽然我们没有安装 jquery
依赖,但可以写 vite
插件实现安装的效果
<script lang="ts">
import jquery from 'jquery'
console.log(jquery)
</script>
语法会报错,但是能成功打印在浏览器控制台上
resolveId
能够接受每个文件的模块引用关系,例如以下:
Tip:
vite
规定当第一个插件中的resolveId
执行之后,那么后面的resolveId
都不会执行(官方源码内置的插件也有resolveId
),所以我们需要加上enforce: 'pre'
告诉vite
此插件的优先级在所有插件前面
// main.ts
import App from './App'
// HelloWorld.vue
import { ref } from 'vue'
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
{
name: 'vite-plugin-xxx',
enforce: 'pre',
resolveId(source) {
console.log(source)
// ./App
// vue
}
}
],
})
resolveId
还可以返回一个值,用于下一级处理,返回的值可以加 \0
表示只被当前插件识别处理,其他插件不会处理
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
{
name: 'vite-plugin-xxx',
enforce: 'pre',
resolveId(source) {
return '\0' + source
}
}
],
})
上面我们讲过 resolveId
可以返回一个字符串,我们可以用 load
来获取:
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
{
name: 'vite-plugin-xxx',
enforce: 'pre',
resolveId(source) {
return '\0' + source
},
load(id) {
console.log(id)
// \0./App
// \0vue
}
}
],
})
load
也可以返回一个字符串,该字符串表示我们该如何处理用户的导入模块
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugin: [
vue(),
{
name: 'vite-plugin-external',
enforce: 'pre',
resolveId(source) {
if(source === 'jquery') {
return '\0' + source
}
},
load(id) {
if(id === '\0jquery') {
// 虚拟模块
// 表示当我们 import $ from 'jquery' 时,实际上的 `jquery` 模块内容是 export default window.jQuery
return `export default window.jQuery`
}
}
}
]
})
效果如下:
- import { Button } from 'antd'
+ import Button from 'antd/es/button'
大致的原理通过 ast
语法树来替换,例如以下代码:
import { Button } from 'ant-design-vue'
转换后的 ast
语法树(重点关注红色框中的内容):
transform
可以对文件的源码进行操作,我们利用此函数将 import { Button } from 'antd'
改为 import Button from 'antd/es/button'
transform
内部包含 parse
转换 ast 函数
export default defineConfig({
plugins: [
{
name: 'vite-plugin-demand-import',
// code 表示源码,id 跟文件名有关
transform(code, id) {
// 使用内置的 parse 转换 ast 语法树
const ast = this.parse(code)
}
}
]
})
treeWalk
函数function treeWalk(
ast: Record<string, any> | Array<Record<string, any>>,
visitor: { [type: string]: (ast: Record<string, any>) => void }
) {
for(const node of Object.values(ast)) {
if(node && typeof node === 'object') {
// ?. 语法
visitor[node.type]?.(node)
treeWalk(node, visitor)
}
}
}
上述函数中出现了 ?.
语法,这表示如果 visitor[node.type]
不是函数,那么就不执行
treeWalk
函数的使用例如我们想要 ast
中的 ImportDeclaration
属性:
treeWalk(ast, {
ImportDeclaration(node) {
// node 就是 ImportDeclaration 中的内容
}
})
transform(code, id) {
const ast = this.parse(code)
// 从 antd 导入的成员集合
const antdImports: string[] = []
// 使用 start 和 end 定位代码块
let start = 0
let end = -1
// 如果文件名以 .vue 结尾
if (id.endsWith('.vue')) {
treeWalk(ast, {
ImportDeclaration(node) {
// .vue 文件中导入了 ant-design-vue 模块
if (node.source.value === 'ant-design-vue') {
// 以下两个 if 代码待会解释
if (!start) start = node.start
if (end) end = node.end
// 循环获得 import 语句内容
for (const spec of node.specifiers) {
// import 语句的设计代码符合 ant-design-vue 构建后的目录结构
antdImports.push(`import ${spec.local.name} from 'ant-design-vue/es/${spec.local.name.toLowerCase()}'`)
}
}
},
})
// 如果数组不为空,那么就将转换后的代码返回
if (antdImports.length) {
return (
code.slice(0, start) + // import antd 前面的部分
antdImports.join('\n') + // 转换后的部分
code.slice(end) // import antd 后面的部分
)
}
}
},
为什么会有两个 if 判断呢?我们导入 antd
成员时,一般这样:
import { Button, Table } from 'ant-design-vue'
但有的用户会这样:
import { Button } from 'ant-design-vue'
import { Table } from 'ant-design-vue'
虽然这样会被某些 eslint
规则报错,但为了完善,还是加上了,另外,此插件不支持在两个 antd
导入模块中导入新的模块,例如下面代码会报错的:
import { Button } from 'ant-design-vue'
import { ref } from 'ref'
import { Table } from 'ant-design-vue'
使用 configResolved
可以看到配置信息,例如打印所有 plugins
的名字:
export default defineConfig({
plugins: [
{
name: 'vite-plugin-test',
configResolved(config) {
console.log(config.plugins.map(p => p.name))
}
}
]
})