你还不会写 Vite 插件吗

现在 vite 工具越来越流行,我们能不能做一些有意义的事情,比如写一个 vite 插件,你觉得怎么样?
正好趁着 vite 插件生态还不成熟,做一个让自己满意、让领导开心、让社区受益的插件,一起携手共进。
如果你对 vite 感兴趣,可以查看专栏:"Vite 从入门到精通"
通过本文你可以学到:

如何创建 vite 插件模板
vite 插件各钩子的作用
vite 插件的钩子执行顺序
如何编写自己的插件

了解 vite 插件
1. 什么是 vite 插件
vite 实际上是一个基于原生 ES Module 的新型前端开发工具。
vite 插件可以很好地扩展 vite 本身无法实现的功能,例如文件图片压缩、CommonJS 支持、打包进度条等。
2. 为什么要写 vite 插件
相信在座各位对 webpack 的相关配置和常用插件都已经很熟悉了;
而 vite 作为新兴的前端构建工具,还很年轻,有很大的扩展空间,何不趁现在一起携手,做一些对你、对我、对大家都更有意义的事呢?
快速体验
要写插件,首先要创建项目。以下 vite 插件通用模板可以直接 clone 使用,以后写插件时直接复用;
插件通用模板 GitHub:体验入口
插件 GitHub:体验入口

推荐包管理器优先级:pnpm > yarn > npm > cnpm

话不多说,直接开干~
创建 vite 插件通用模板
1. 初始化
1.1 创建文件夹并初始化:按提示操作即可
mkdir vite-plugin-progress && cd vite-plugin-progress && pnpm init

1.2 安装 TypeScript
pnpm i typescript @types/node -D

1.3 配置 tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "dist",
"lib": ["ESNext"],
"sourceMap": false,
"noEmitOnError": true,
"noImplicitAny": false
},
"include": [
"src/*",
"*.d.ts"
],
"exclude": [
"node_modules",
"examples",
"dist"
]
}

1.4 安装 vite
// enter package.json
{
...
"devDependencies": {
"vite": "*"
}
...
}

2. 配置 eslint 和 prettier(可选)


安装 eslint
pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev



配置 .eslintrc:配置链接


安装 prettier(可选)
pnpm i prettier eslint-config-prettier eslint-plugin-prettier --save-dev



配置 .prettierrc:配置链接


3. 添加 src/index.ts 入口
import type { PluginOption } from 'vite';

export default function vitePluginTemplate(): PluginOption {
return {
// plugin name
name: 'vite-plugin-template',

// pre will be executed before post
enforce: 'pre', // post

// Indicate that they are only called in 'build' or 'serve' mode
apply: 'build', // apply can also be a function

config(config, { command }) {
console.log('here is the config hook');
},

configResolved(resolvedConfig) {
console.log('here is the configResolved hook');
},

configureServer(server) {
console.log('here is the configureServer hook');
},

transformIndexHtml(html) {
console.log('here is the transformIndexHtml hook');
},
}
}

下面会详细讲解 vite 插件的各钩子函数~
到这里,基本模板已经搭建好了,但接下来要想一想,这个插件该怎么运行呢?
那我们就需要创建一些示例来运行这段代码;
4. 创建 examples 目录
这里创建了三组项目示例,可以直接复制使用,不再逐一介绍。

vite-react
vite-vue2
vite-vue3

如果插件需要在更多示例中运行,自行创建项目即可;
然后需要配置 examples 下的项目,与根目录的插件做联合调试(以 examples/vite-vue3 为例)。
5. 配置 examples/vite-vue3 项目


修改 examples/vite-vue3/package.json
{
...
"devDependencies": {
...
"vite": "link:../../node_modules/vite",
"vite-plugin-template": "link:../../"
}
}



以上配置的含义:


使 examples/vite-vue3 项目中的 vite 版本与根目录 vite-plugin-template 中的版本保持一致;


同时将 examples/vite-vue3 项目中的 vite-plugin-template 指向当前根目录开发的插件;




引入插件:examples/vite-vue3/vite.config.ts
import template from 'vite-plugin-template';

export default defineConfig({
...
plugins: [vue(), template()],
...
});



安装依赖:cd examples/vite-vue3 && pnpm install
cd examples/vite-vue3 && pnpm install




注意:examples/vite-vue2 和 examples/vite-react 的配置与此一致

思考:
到这里再想一想,examples/vite-vue3 的项目已经配置好了,但该怎么运行呢?
直接进入 examples/vite-vue3 目录运行 pnpm run build 或 pnpm run dev?
这样显然无法成功运行,因为根目录的 src/index.ts 不能直接运行,需要先将 .ts 文件转译为 .js 文件;
那该怎么处理呢?
这就需要借助一个轻量且无需配置的构建工具 tsup。
6. 安装 tsup 并配置运行命令
tsup 是一个基于 esbuild 的轻量级零配置构建工具;
同时它可以直接将 .ts、.tsx 转译为 esm、cjs、iife 等不同格式;


安装 tsup
pnpm i tsup -D



在根目录的 package.json 中配置
{
...
"scripts": {
"dev": "pnpm run build -- --watch --ignore-watch examples",
"build": "tsup src/index.ts --dts --format cjs,esm",
"example:react": "cd examples/vite-react && pnpm run build",
"example:vue2": "cd examples/vite-vue2 && pnpm run build",
"example:vue3": "cd examples/vite-vue3 && pnpm run build"
},
...
}



7. 开发环境运行


开发环境运行:实时监控文件修改并重新打包(热更新)
pnpm run dev



运行 examples 中的任意项目(以 vite-vue3 为例)
pnpm run example:vue3



注意:

如果插件仅在构建时运行,设置 "example:vue3": "cd examples/vite-vue3 && pnpm run build";
否则运行 pnpm run dev


输出:


到这里就可以边开发边运行了,看到效果是不是很酷~
8. 发布

安装 bumpp 进行版本管理和打标签

pnpm i bumpp -D


配置 package.json

{
...
"scripts": {
...
"prepublishOnly": "pnpm run build",
"release": "npx bumpp --push --tag --commit && pnpm publish",
},
...
}


插件开发完成后执行发布

# first step
pnpm run prepublishOnly

# second step
pnpm run release

到这里,vite 插件模板就写好了,可以直接 clone vite-plugin-template 模板使用;
如果对 vite 的插件钩子和实现一个真实的 vite 插件感兴趣,可以继续往下看;
vite 的插件钩子
1. vite 独有的钩子

enforce:取值可以是 pre 或 post,pre 在 post 之前执行;
apply:取值可以是 build 或 serve 或一个函数,指定仅在 build 或 serve 模式下调用;
config(config, env):可以在 vite 解析配置之前修改 vite 相关配置。该钩子接收原始用户配置 config 和描述配置环境的变量 env;
configResolved(resolvedConfig):在 vite 配置解析完成后调用。用于读取并存储最终解析的配置。当插件需要根据运行的命令做不同处理时非常有用;
configureServer(server):主要用于配置开发服务器,为 dev-server 添加自定义中间件(connect 应用);
transformIndexHtml(html):专门用于转换 index.html 的钩子。接收当前 HTML 字符串和转换上下文;
handleHotUpdate(ctx):执行自定义 HMR 更新,可通过 ws 向客户端发送自定义事件;

2. vite 和 rollup 通用钩子的构建阶段

options(options):服务器启动时调用,获取并操作 Rollup 选项,严格来说在构建阶段之前执行;
buildStart(options):每次构建开始时调用;
resolveId(source, importer, options):对每个传入的模块请求调用,创建自定义确认函数,可用于定位第三方依赖;
load(id):对每个传入的模块请求调用,可自定义加载器,用于返回自定义内容;
transform(code, id):对每个传入的模块请求调用,主要用于转换单个模块;
buildEnd():构建阶段结束时调用,此处的构建结束仅指所有模块转译完成;

3. vite 和 rollup 通用钩子的输出阶段

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 之前立即触发该钩子;
writeBundle(options, bundle):调用 bundle.write 后,所有 chunk 写入文件后,最终调用 writeBundle;
closeBundle():服务器关闭时调用

4. 插件钩子函数的执行顺序(如下图所示)

5. 插件的执行顺序

Alias 处理别名
用户插件设置 enforce: 'pre'
vite 核心插件
用户插件未设置 enforce
vite 构建插件
用户插件设置 enforce: 'post'
vite 构建后置插件(minify、manifest、reporting)

实战一个 vite 插件
下面以 vite 打包进度条插件为例;

插件地址:GitHub 觉得不错的话欢迎 star ⭐️
该插件已被 vite 官方收录到官方文档:链接地址
由于本文重点不在该插件的详细实现过程,因此只贴出源代码供参考。详细介绍将在下一篇文章中讲解,敬请期待!

index.ts

import type { PluginOption } from 'vite';
import colors from 'picocolors';
import progress from 'progress';
import rd from 'rd';
import { isExists, getCacheData, setCacheData } from './cache';

type Omit = Pick>;
type Merge = Omit> & N;

type PluginOptions = Merge<
ProgressBar.ProgressBarOptions,
{
/**
* total number of ticks to complete
* @default 100
*/
total?: number;

/**
* The format of the progress bar
*/
format?: string;
}
>;

export default function viteProgressBar(options?: PluginOptions): PluginOption {

const { cacheTransformCount, cacheChunkCount } = getCacheData()

let bar: progress;
const stream = options?.stream || process.stderr;
let outDir: string;
let transformCount = 0
let chunkCount = 0
let transformed = 0
let fileCount = 0
let lastPercent = 0
let percent = 0

return {
name: 'vite-plugin-progress',

enforce: 'pre',

apply: 'build',

config(config, { command }) {
if (command === 'build') {
config.logLevel = 'silent';
outDir = config.build?.outDir || 'dist';

options = {
width: 40,

...options
};
options.total = options?.total || 100;

const transforming = isExists ? `${colors.magenta('Transforms:')} :transformCur/:transformTotal | ` : ''
const chunks = isExists ? `${colors.magenta('Chunks:')} :chunkCur/:chunkTotal | ` : ''
const barText = `${colors.cyan(`[:bar]`)}`

const barFormat =
options.format ||
`${colors.green('Bouilding')} ${barText} :percent | ${transforming}${chunks}Time: :elapseds`

delete options.format;
bar = new progress(barFormat, options as ProgressBar.ProgressBarOptions);



// not cache: Loop files in src directory
if (!isExists) {
const readDir = rd.readSync('src');
readDir.forEach((item) => reg.test(item) && fileCount++);
}
}
},

transform(code, id) {
transformCount++

// not cache
if(!isExists) {
const reg = /node_modules/gi;

if (!reg.test(id) && percent < 0.25) {
transformed++
percent = +(transformed / (fileCount * 2)).toFixed(2)
percent < 0.8 && (lastPercent = percent)
}

if (percent >= 0.25 && lastPercent <= 0.65) {
lastPercent = +(lastPercent + 0.001).toFixed(4)
}
}

// go cache
if (isExists) runCachedData()

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})

return {
code,
map: null
};
},

renderChunk() {
chunkCount++

if (lastPercent <= 0.95)
isExists ? runCachedData() : (lastPercent = +(lastPercent + 0.005).toFixed(4))

bar.update(lastPercent, {
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: chunkCount,
})

return null
},

closeBundle() {
// close progress
bar.update(1)
bar.terminate()

// set cache data
setCacheData({
cacheTransformCount: transformCount,
cacheChunkCount: chunkCount,
})

// out successful message
stream.write(
`${colors.cyan(colors.bold(`Build successful. Please see ${outDir} directory`))}`
);
stream.write(' ');
stream.write(' ');
}
};

/**
* run cache data of progress
*/
function runCachedData() {

if (transformCount === 1) {
stream.write(' ');

bar.tick({
transformTotal: cacheTransformCount,
transformCur: transformCount,
chunkTotal: cacheChunkCount,
chunkCur: 0,
})
}

transformed++
percent = lastPercent = +(transformed / (cacheTransformCount + cacheChunkCount)).toFixed(2)
}
}



cache.ts

import fs from 'fs';
import path from 'path';

const dirPath = path.join(process.cwd(), 'node_modules', '.progress');
const filePath = path.join(dirPath, 'index.json');

export interface ICacheData {
/**
* Transform all count
*/
cacheTransformCount: number;

/**
* chunk all count
*/
cacheChunkCount: number
}

/**
*It has been cached
* @return boolean
*/
export const isExists = fs.existsSync(filePath) || false;

/**
* Get cached data
* @returns ICacheData
*/
export const getCacheData = (): ICacheData => {
if (!isExists) return {
cacheTransformCount: 0,
cacheChunkCount: 0
};

return JSON.parse(fs.readFileSync(filePath, 'utf8'));
};

/**
* Set the data to be cached
* @returns
*/
export const setCacheData = (data: ICacheData) => {
!isExists && fs.mkdirSync(dirPath);
fs.writeFileSync(filePath, JSON.stringify(data));
};


最后
本系列将持续更新。关于整个"Vite 从入门到精通"专栏,主要从以下几个方面进行讲解,敬请期待!

各位看到这里了,还不点个赞👍.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs- quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number ,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector -id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font -weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet, .hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background: #fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 分类:前端 标签:前端 Vite 文章收录于专栏:
Vite 从入门到精通

Vite(发音类似 [weɪt],意为轻、轻快)是一个基于原生 ES Module 的 Web 开发前端构建工具。
具有以下特性:
💡 极速的服务启动;
⚡️ 轻量快速的热更新;
🛠️ 丰富的功能;
📦 优化的构建;
🔩 通用的插件;
🔑 完全类型化的 API。

相关文章

探索更多特惠

  1. 短信服务(SMS)和邮件服务

    50,000封邮件包低至1.99美元,120条短信仅需1.00美元起

phone 联系我们
你好,我是AI助理。
可以解答问题、推荐解决方案等。