vite插件是基于rollup插件上面拓展而来的,如不了解可以看看如下两篇文章:
rollup插件机制文章的配图是旧版本的,与此时rollup官网上面配图有偏差,但不影响我们理解对应钩子功能。
下面来写一个vite插件,其功能如下:
- 统计项目中用到文件类型(不包括node_modules)以及其数量
- 统计项目中代码的行数
项目版本信息:
- vue: ^3.3.4
- vite: ^4.0.0
- @vitejs/plugin-vue: ^4.0.0
大体结构
// root/src/summary.ts
// 一个可以往控制台输出添加颜色的库
import pc from 'picocolors'
const { green, blue, cyan } = pc
export function vitePluginSummary() {
return {
name: 'vite-plugin-summary'
}
}
// root/vite.config.ts
import { vitePluginSummary } from './src/summary.ts'
export default defineConfig({
plugins: [
vitePluginSummary()
]
})
具体钩子函数
写vite插件就是写对应的钩子函数,针对于本次插件使用的场景,这边使用如下几个钩子函数:
- buildStart
- load
- transform
- closeBundle
buildStart
rollup中打包开始时触发的钩子函数,这里我们在这个钩子函数中初始化变量。
export function vitePluginSummary() {
// Map存储文件后缀名与之对应的文件名称集合Set
const extnameMap = new Map<string, Set<string>>()
// 统计代码行数
let totalCodeLine = 0
return {
// ...省略
buildStart() {
extnameMap.clear()
totalCodeLine = 0
},
}
}
load
rollup中读取具体文件触发的钩子函数,这里面我们可以得到文件路径,通过文件路径获取到文件拓展名,并收集计数。
这个钩子类型在rollup里面叫做First
,在钩子容器中所有的load钩子挨个执行,如果某个钩子返回了不为null的的就停止,否则就一直执行。所以我们这边自定义的load中要返回一个null,不能影响其他load钩子函数的执行。
First
类型的钩子执行原理简要实现:
// 生成钩子函数
function generateHook(timeout, val) {
return () => new Promise(res => {
setTimeout(() => {
console.log(val, 'val')
res(val)
}, timeout)
})
}
const hook1 = generateHook(1000, null)
const hook2 = generateHook(1000, 'not null')
const hook3 = generateHook(1000, 'hook2中返回值不为null,所以该钩子不会执行')
// first类型钩子
async function triggerFirstHook(hookArr) {
for (let i = 0; i < hookArr.length; i++) {
let res = await hookArr[i]()
// 返回结果不为null 就停止
if (res !== null) {
break
}
}
}
triggerFirstHook([hook1, hook2, hook3])
/**
* ==> 输出
* null val
* not null val
*/
load中代码如下:
// 使用 extname 获取字符串中的文件拓展名
import { extname } from 'path'
export function vitePluginSummary() {
// ... 省略部分代码
/** 解释下参数 importee
* 例如:import xx from './src/moduleA'
* importee 就是 './src/moduleA'
*/
load(importee: string) {
// 去除node_modules中的内容
// x00这个好像是vite内置添加的内容 都不用考虑
if (/node_modules|\x00/.test(importee)) return null
/**
* 文件名称中可能有请求参数,例如:/root/src/index.vue?type=script&setup=true
* 去除文件名称中的参数得到:/root/src/index.vue
* */
const importeeRemoveQuery = importee.replace(/?.*$/, '')
/**
* 经过extname处理过的文件后缀名可能会有请求参数,例如:.vue?type=script&xxxx
* 将 .vue?type=script&xxxx => .vue
*/
const currentExtname = extname(importeeRemoveQuery).replace(/?.*$/, '')
// 根据后缀名称存储对应的文件名称
extnameMap.set(
currentExtname,
extnameMap.has(currentExtname)
? extnameMap.get(currentExtname)!.add(importeeRemoveQuery)
: new Set<string>().add(importeeRemoveQuery)
)
return null
}
}
来解释下上面的一行代码:
/**
* 文件名称中可能有请求参数,例如:/root/src/index.vue?type=script&setup=true
* 去除文件名称中的参数得到:/root/src/index.vue
* */
const importeeRemoveQuery = importee.replace(/?.*$/, '')
这行代码中去除了importee中后面的请求信息,我们通过一个案例看看这样做的原因。
一个文件如root/src/App.vue
,它的源码如下:
<script setup lang="ts">
const personObj = ref({
name: 'yellres'
})
</script>
<template>
<div>{{ personObj.name }}</div>
</template>
<style scoped></style>
这个文件打包时候,会先触发load
钩子,然后触发transform
钩子,由于这是个vue文件,会触发@vitejs/plugin-vue
中的transform
钩子,@vitejs/plugin-vue
会将App.vue转换为如下内容:
/* unplugin-vue-components disabled */
import _sfc_main from "root/src/App.vue?vue&type=script&setup=true&lang.ts";
export * from "root/src/App.vue?vue&type=script&setup=true&lang.ts";
export default _sfc_main;
得到模块内容后会去解析模块内容,此时触发了moduleParsed
钩子,这其中moduleParse
又会去解析import路径,在没有命中缓存情况依次触发resolveId
,load
,transform
钩子。
如上面代码所示,rollup
此时会去读取root/src/App.vue?vue&type=script&setup=true&lang.ts
中的内容,此时的load
钩子又被触发了。
而root/src/App.vue?vue&type=script&setup=true&lang.ts
和root/src/App.vue
是同一个文件,它们的后缀名只要被统计一次即可。所以在load
钩子触发的时候,默认去掉了文件地址的参数。
transform
rollup
执行完load
钩子后,下一步就会把load
钩子中获取到的内容给transform
钩子处理。
该钩子类型是Sequential
,所有钩子挨个执行,上一个钩子的返回值作为参数传入下个钩子中。
Sequential
类型的钩子执行原理简要实现:
async function triggerSequentialHook(hookArr) {
let result = null
for (let i = 0; i < hookArr.length; i++) {
if (i === 0) {
result = await hookArr[i]()
} else {
result = await hookArr[i](result)
}
}
}
这个钩子中可以取得文件的源代码,我们统计项目的代码行数就可以在这边获取。
transform
代码如下:
export function vitePluginSummary() {
return {
// ... 省略
transform(code: string, id: string) {
const cachedExtArr = [...extnameMap.values()].reduce((pre, cur) => {
pre.push(...cur)
return pre
}, [])
if (cachedExtArr.includes(id)) {
totalCodeLine += (code.split('\n') || []).length
}
return code
},
enforce: 'pre'
}
}
最后要给加 enforce: 'pre'
, 我们的transform
钩子要在其他transform
函数钩子执行前执行,防止内容被其他tranform
钩子修改。
closeBundle
rollup中结束打包后触发的钩子,这边我们来输出最终的结果。
import { extname } from 'path'
import pc from 'picocolors'
const { green, blue, cyan } = pc
export function vitePluginSummary() {
// ... 省略
closeBundle() {
for (const [key, extSet] of extnameMap.entries()) {
console.log(`${green(key)}'s num is ${blue(extSet.size)}`)
}
console.log(`the total code line is ${cyan(totalCodeLine)}`)
}
}
最终代码如下:
import { extname } from 'path'
import pc from 'picocolors'
const { green, blue, cyan } = pc
export function vitePluginSummary() {
// Map存储文件后缀名与之对应的文件名称集合Set
const extnameMap = new Map<string, Set<string>>()
// 统计代码行数
let totalCodeLine = 0
return {
name: 'vite-plugin-summary',
buildStart() {
extnameMap.clear()
totalCodeLine = 0
},
load(importee: string) {
if (/node_modules|\x00/.test(importee)) return null
/**
* 文件名称中可能有请求参数,例如:/root/src/index.vue?type=script&setup=true
* 去除文件名称中的参数得到:/root/src/index.vue
* */
const importeeRemoveQuery = importee.replace(/?.*$/, '')
// const importeeRemoveQuery = importee
/**
* 经过extname处理过的文件后缀名可能会有请求参数,例如:.vue?type=script&xxxx
* 将 .vue?type=script&xxxx => .vue
*/
const currentExtname = extname(importeeRemoveQuery).replace(/?.*$/, '')
extnameMap.set(
currentExtname,
extnameMap.has(currentExtname)
? extnameMap.get(currentExtname)!.add(importeeRemoveQuery)
: new Set<string>().add(importeeRemoveQuery)
)
return null
},
transform(code: string, id: string) {
const cachedExtArr = [...extnameMap.values()].reduce((pre, cur) => {
pre.push(...cur)
return pre
}, [])
if (cachedExtArr.includes(id)) {
totalCodeLine += (code.split('\n') || []).length
}
return code
},
closeBundle() {
for (const [key, extSet] of extnameMap.entries()) {
console.log(`${green(key)}'s num is ${blue(extSet.size)}`)
}
console.log(`the total code line is ${cyan(totalCodeLine)}`)
},
// transform 必须要设置pre
enforce: 'pre'
}
}
执行结果如下:
本文只是提供想写个vite
插件加深下对vite
插件的理解,其中统计代码行数,以及文件数目只是适用于个人项目,由于不同项目复杂度不同,不是一个完备的解决方案。
本人才疏学浅,上面内容如果有错误,欢迎指出~~☺️
原文链接:https://juejin.cn/post/7325269225430204431 作者:Yellres