yulu
前端开发工程师
yulu 2022-06-15
Vite 是由原生 ES Module
驱动的新型 Web 前端构建工具;
Vite插件 其实就是在vite的基础上扩展vite的能力,进而能实现我们在构建阶段需要处理的一些定制化的需求,比如解析用户自定义的文件输入,文件图片的压缩,在打包之前转译代码等等。
比如Galio项目中用到的vite插件:
由于vite插件扩展自Rollup接口,只是额外多了一些 vite特有选项 ,为了区分两者,所以制定了以下命名约定。
如果插件不使用 Vite 特有的钩子,可以实现为兼容的 Rollup 插件,则插件命名带有rollup-plugin-` 前缀
对于 Vite 专属的插件:
vite-plugin-
前缀、语义清晰的名称。vite-plugin
关键字。如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:
vite-plugin-vue-
前缀作为 Vue 插件vite-plugin-react-
前缀作为 React 插件vite-plugin-svelte-
前缀作为 Svelte 插件用户会将插件添加到项目的 devDependencies
中并使用数组形式的 plugins
选项配置它们。
import vitePlugin from 'vite-plugin-test'
import rollupPlugin from 'rollup-plugin-test'
export default {
plugins: [vitePlugin(), rollupPlugin()]
}
plugins
也可以接受将多个插件作为单个元素的预设。这对于使用多个插件实现的复杂特性(如框架集成)很有用。该数组将在内部被扁平化(flatten)。
例如 vite-legacy插件中返回的就是一个插件数组:
enforce
:值可以是pre
或 post
, pre
会较于 post
先执行;apply
:值可以是 build
或 serve
亦可以是一个函数,指明它们仅在 build
或 serve
模式时调用;config(config, env)
:可以在 vite 被解析之前修改 vite 的相关配置。钩子接收原始用户配置 config 和一个描述配置环境的变量env(包含正在使用的 mode
和 command
)。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置。configResolved(resolvedConfig)
:在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它很有用。configureServer(server)
:主要用来配置开发服务器,为 dev-server (connect 应用程序) 添加自定义的中间件;transformIndexHtml(html)
:转换 index.html
的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer
实例,在构建期间暴露 Rollup 输出的包。
这个钩子可以是异步的,并且可以返回以下其中之一:
{ tag, attrs, children }
)。每个标签也可以指定它应该被注入到哪里(默认是在 <head>
之前){ html, tags }
的对象handleHotUpdate(ctx)
:执行自定义HMR更新,可以通过ws往客户端发送自定义的事件;options(options)
:在服务器启动时被调用:获取、操纵Rollup选项,严格意义上来讲,它执行于属于构建阶段之前;buildStart(options)
:在每次开始构建时调用;resolveId(source, importer, options)
:在每个传入模块请求时被调用,创建自定义确认函数,可以用来定位第三方依赖;load(id)
:在每个传入模块请求时被调用,可以自定义加载器,可用来返回自定义的内容;transform(code, id)
:在每个传入模块请求时被调用,主要是用来转换单个模块;buildEnd()
:在构建阶段结束后被调用,此处构建结束只是代表所有模块转义完成;outputOptions(options)
:接受输出参数;renderStart(outputOptions, inputOptions)
:每次 bundle.generate 和 bundle.write 调用时都会被触发;augmentChunkHash(chunkInfo)
:用来给 chunk 增加 hash;renderChunk(code, chunk, options)
:转译单个的chunk时触发。rollup 输出每一个chunk文件的时候都会调用;generateBundle(options, bundle, isWrite)
:在调用 bundle.write 之前立即触发这个 hook;writeBundle(options, bundle)
:在调用 bundle.write后,所有的chunk都写入文件后,最后会调用一次 writeBundle;closeBundle()
:在服务器关闭时被调用开发环境:
构建阶段
PS: 在学习、调试或创作插件时,可以在项目中引入 vite-plugin-inspect。 它可以帮助你检查 Vite 插件的中间状态。安装后,你可以访问 localhost:3000/__inspect/
来检查你项目的模块和栈信息。
一个 Vite 插件可以额外指定一个 enforce
属性来调整它的应用顺序。enforce
的值可以是pre
或 post
。解析后的插件将按照以下顺序排列:
enforce: 'pre'
的用户插件enforce: 'post'
的用户插件推荐使用 @rollup/pluginutils 工具包,可以很方便的处理一些常规事情,比如添加文件扩展,创建过滤器等等。
统计组件使用情况的部分代码(待进一步完善) `
import { createFilter } from '@rollup/pluginutils'
import { walk } from 'estree-walker'
const fs = require('fs')
/**
* 几种情况
* <OkkiButton>按钮</OkkiButton>
* import { Button } from '@okki-design/ui'
* import { Button as AButton, Input as OkkiInput } from '@okki-design/ui'
*/
export default function myPlugin(options = {}) {
const source_map = {}
const source_map2 = {}
const components = new Set()
const cwd = process.cwd()
var filter = createFilter(options.include, options.exclude)
return {
name: 'my-example',
apply: 'build',
transform(code, id) {
if (!filter(id)) return
const file_path = id.replace(cwd, '')
const ast = this.parse(code)
const data = {}
let locals = []
walk(ast, {
enter(node, parent) {
if (node.type === 'ImportDeclaration' && node.source.value === '@okki-design/ui') {
for (const item of node.specifiers) {
data[item.local.name] = {
num: 0,
refer: item.imported.name,
}
if (!components[item.local.name]) {
components.add(item.local.name)
}
}
locals = Object.keys(data)
return
}
if (node.type === 'Identifier' && locals.includes(node.name) && parent.type !== 'ImportSpecifier') {
data[node.name].num++
}
if (node.type === 'Literal' && typeof node.value === 'string' && node.value.startsWith('Okki')) {
if (data[node.value] === undefined) {
data[node.value] = {
num: 0,
refer: node.value.replace('Okki', ''),
}
}
data[node.value].num++
}
},
})
try {
Object.keys(data).forEach((key) => {
if (source_map[file_path] === undefined) {
source_map[file_path] = {}
}
source_map[file_path][data[key].refer] = data[key].num
})
} catch {}
try {
Object.keys(data).forEach((key) => {
if (source_map2[data[key].refer] === undefined) {
source_map2[data[key].refer] = {}
}
source_map2[data[key].refer][file_path] = data[key].num
})
} catch {}
},
buildEnd() {
fs.writeFile('data.json', `${JSON.stringify(source_map)}\n`, {}, (error) => {
console.log(error)
})
fs.writeFile('data2.json', `${JSON.stringify(source_map2)}\n`, {}, (error) => {
console.log(error)
})
},
}
}