简介
Vite插件扩展了设计出色的Rollup接口,带有一些Vite独有的配置项。因此,你只需要写一个Vite插件,就可以同时为开发环境和生产环境工作。
配置vite
当以命令方式运行
vite
时(即在根目录下的终端运行,例:vite
,vite build
,vite preview
等命令),Vite会自动解析项目根目录下名为vite.config.js
或vite.config.ts
的配置文件(支持.js或.ts扩展名);
基础配置
最基础的配置文件内容是这样:
javascript
// vite.config.js
export default {
// 配置选项
}
显示定义/(自定义)配置文件,显示的通过--config
命令选项指定一个配置文件,例如:my-config.js
(相对于 process.cwd 路径进行解析);
bash
vite --config my-config.js
注意:即使项目没有在package.json
中开启type:"module"
,Vite也支持在配置文件中使用ESM语法。这种情况下,配置文件会在被加载前自动进行预处理。
配置智能提示
在vite的配置文件
vite.config.js
或vite.config.ts
中配置后,在代码编辑器中实现智能提示;
因为Vite
本身附带TypeScript类型,所以你可以通过IDE 和 jsdoc 的配置来实现智能提示:
js
/** @type {import('vite').UserConfig} */
export default {
// ...
}
另外你可以使用 defineConfig
工具函数,这样不用jsdoc注解也可以获取类型提示:
js
import { defineConfig } from 'vite'
export default defineConfig({
// ...
})
Vite也直接支持 TypeScript配置文件。你可以在 vite.config.ts
中使用上述的 defineConfig
工具函数,或者 typescript 的 satisfies
运算符:
ts
import type { UserConfig } from 'vite'
export default {
// ...
} satisfies UserConfig
情景配置
有些场景,我们需要根据运行的vite命令(mode),或者当前的模式(mode),dev命令运行在development(开发)模式,而 build命令运行在production(生产)模式,或者其他的情况,来做一些独有的配置。这时候,我们可以用情景配置的方式,来做处理就特别合适;
如果配置文件需要基于(serve
或 build
)命令或者不同的模式来决定选项,亦或者是一个SSR构建(isSsrBuild
)、一个正在预览的构建产物(isPreview),则可以选择导出这样一个函数:
配置文件vite.config.js:
js
export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => {
if (command === 'serve') {
return {
// dev 独有配置
}
} else {
// command === 'build'
return {
// build 独有配置
}
}
})
需要注意的是:在Vite的API中,开发环境下command
的值为serve(在CLI中,vite dev 和 vite serve 是vite 的别名), 而在生产环境下 build
(vite build
)。
异步配置
如果配置需要调用一个异步函数,也可以转而导出一个异步函数。这个异步函数也可以通过
defineConfig
传递,以便获取更好的智能提示:
js
export default defineConfig(async ({ command, mode }) => {
const data = await asyncFunction()
return {
// vite 配置
}
})
在配置中使用环境变量
环境变量通常可以从
process.env
获取。我们用webpack的时候,常用它来获取.env中的变量和当前的环境变量信息; 但是注意:Vite默认是不加载.env文件的 ,也就是默认不获取.env文件中的变量信息的,因为这些文件需要在执行完Vite配置后才能确定加载哪一个,举个例子,root
和envDir
选项会影响加载行为。不过当你确定需要时,你可以使用Vite导出loadEnv
函数来加指定的.env
文件。
js
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有
// `VITE_` 前缀。
const env = loadEnv(mode, process.cwd(), '')
return {
// vite 配置
define: {
__APP_ENV__: JSON.stringify(env.APP_ENV),
},
}
})
插件配置使用
vite插件的配置使用:
(1).下载插件添加到项目的
package.json
中的devDependencies
中;(2). 在vite的配置文件中(
vite.config.js
)引入使用(使用数组形式的)plugins
选项配置它们;
js
// vite.config.js
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default defineConfig({
plugins: [vitePlugin(), rollupPlugin()],
})
假值的插件将被忽略,可以用来轻松地启用或停用插件。
plugins
也可以接受将多个插件作为单个元素的预设。这对于使用多个插件实现的复杂特性(如框架集成)很有用。该数组将在内部被扁平化(flatten)。
有的框架会把
vite
的plugins
抽象出去; 例如:![]()
代码示例:
js
// 框架插件
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'
export default function framework(config) {
return [frameworkRefresh(config), frameworkDevTools(config)]
}
js
// vite.config.js
import { defineConfig } from 'vite'
import framework from 'vite-plugin-framework'
export default defineConfig({
plugins: [framework()],
})
插件创作前沿
Rollup是Vite依赖的打包器,
Vite
插件扩展了Rollup精妙的插件接口,并增加了一些Vite特有的选项配置;也就是说,Vite插件兼容Rollup插件的钩子。当然Vite也有自己的专属钩子。
约定
Vite插件不使用Vite特有的钩子,那么可以作为 兼容Rollup的插件来实现,插件使用Rollup插件名称约定。
- Rollup 插件应该有一个带
rollup-plugin-
前缀、语义清晰的名称。 - 在 package.json 中包含
rollup-plugin
和vite-plugin
关键字。
对于Vite专属的插件:
- Vite 插件应该有一个带
vite-plugin-
前缀、语义清晰的名称。 - 在 package.json 中包含
vite-plugin
关键字。 - 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。
如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:
vite-plugin-vue-
前缀作为 Vue 插件vite-plugin-react-
前缀作为 React 插件vite-plugin-svelte-
前缀作为 Svelte 插件
Rollup钩子
由于本文只针对Vite的插件开发,不深入探讨Rollup插件开发的内容, 👉👉Rollup插件开发
Vite独有(专属)的钩子
config
: 在解析 Vite 配置前调用。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的 mode
和 command
。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)。
js
// 返回部分配置(推荐)
const partialConfigPlugin = () => ({
name: 'return-partial',
config: () => ({
resolve: {
alias: {
foo: 'bar',
},
},
}),
})
// 直接改变配置(应仅在合并不起作用时使用)
const mutateConfigPlugin = () => ({
name: 'mutate-config',
config(config, { command }) {
if (command === 'build') {
config.root = 'foo'
}
},
})
注意
用户插件在运行这个钩子之前会被解析,因此在 config
钩子中注入其他插件不会有任何效果。
configResolved
:在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用。
js
const examplePlugin = () => {
let config
return {
name: 'read-config',
configResolved(resolvedConfig) {
// 存储最终解析的配置
config = resolvedConfig
},
// 在其他钩子中使用存储的配置
transform(code, id) {
if (config.command === 'serve') {
// dev: 由开发服务器调用的插件
} else {
// build: 由 Rollup 调用的插件
}
},
}
}
注意,在开发环境下,command
的值为 serve
(在 CLI 中,vite
和 vite dev
是 vite serve
的别名)。
configureServer
: 是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件:
js
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义请求处理...
})
},
})
注入后置中间件
configureServer
钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。如果你想注入一个在内部中间件 之后 运行的中间件,你可以从 configureServer
返回一个函数,将会在内部中间件安装后被调用:
js
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
// 返回一个在内部中间件安装后
// 被调用的后置钩子
return () => {
server.middlewares.use((req, res, next) => {
// 自定义请求处理...
})
}
},
})
存储服务器访问
在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:
js
const myPlugin = () => {
let server
return {
name: 'configure-server',
configureServer(_server) {
server = _server
},
transform(code, id) {
if (server) {
// 使用 server...
}
},
}
}
注意 configureServer
在运行生产版本时不会被调用,所以其他钩子需要防范它缺失。(也就是说打包后没有效果了。)
configurePreviewServer
: 与 configureServer
相同,但用于预览服务器。configurePreviewServer
这个钩子与 configureServer
类似,也是在其他中间件安装前被调用。如果你想要在其他中间件 之后 安装一个插件,你可以从 configurePreviewServer
返回一个函数,它将会在内部中间件被安装之后再调用:
js
const myPlugin = () => ({
name: 'configure-preview-server',
configurePreviewServer(server) {
// 返回一个钩子,会在其他中间件安装完成后调用
return () => {
server.middlewares.use((req, res, next) => {
// 自定义处理请求 ...
})
}
},
})
transformIndexHtml
: 转换 index.html
的专用钩子。
handleHotUpdate
: 执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象:
通用钩子
在开发中,Vite开发服务器会创建一个插件容器来调用Rollup构建钩子,这与Rollup如出一辙。
以下钩子在服务器启动时被调用:
以下钩子会在每个传入模块请求时被调用:
以下钩子在服务器关闭时被调用:
请注意 moduleParsed
钩子在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。
Output Generation Hooks(除了 closeBundle
) 在开发中是 不会 被调用的。你可以认为 Vite 的开发服务器只调用了 rollup.rollup()
而没有调用 bundle.generate()
。
插件顺序
一个 Vite 插件可以额外指定一个 enforce
属性(类似于 webpack 加载器)来调整它的应用顺序。enforce
的值可以是pre
或 post
。解析后的插件将按照以下顺序排列:
- Alias
- 带有
enforce: 'pre'
的用户插件 - Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有
enforce: 'post'
的用户插件 - Vite 后置构建插件(最小化,manifest,报告)
请注意,这与钩子的排序是分开的,钩子的顺序仍然会受到它们的 order
属性的影响,这一点 和 Rollup 钩子的表现一样。
插件开发
vite-plugin-vue-slogn
插件使用到的钩子:configResolved
,buildStart
,closeBundle
在控制台中生成项目banner标语以及打包后统计 打包时间 和 打包后的dist文件的大小;
效果图如下:
插件开发过程
我将vite插件的使用和开发分为2种情况: 本地方式插件 和 npm方式插件;本地方式插件:适合仅项目内使用,不对外暴露插件,集成在项目源代码中使用的;npm方式插件:开源的,可使用npm引入使用;
基于Node: v22.14.0
使用到的npm包:boxen, gradient-string
本地方式
一般在项目的根目录创建
plugins
文件夹,然后在该文件夹下创建.ts
或.js
文件,在该文件中export default 一个函数
,该该函数具有插件的基本结构
。然后在vite配置文件(vite.config.ts
或vite.config.js
)中,引入使用
;
以下是在 vue3+vite+ts 项目中定义:
-
安装插件使用到的相关依赖;
bash# boxen 可以创建终端盒子 npm i boxen # gradient-string 可以在终端输出好看的文字样式 npm i gradient-string # dayjs 日期处理库 npm i dayjs
-
在要使用插件的项目(vue3+vite+ts)
根目录
下创建plugins
文件夹,下创建index.ts
和utils.ts
并写入如下内容:ts// utils.ts import { readdirSync, statSync } from "node:fs" import path from "node:path" /** 格式化字节单位 */ export function formatBytes(bytes: number, decimals: number = 3): string { const units: string[] = ['B', 'KB', 'MB', 'GB'] let unitIndex: number = 0 while (bytes >= 1024 && unitIndex < units.length -1) { bytes /= 1024 unitIndex++ } return `${bytes.toFixed(decimals)} ${units[unitIndex]}` } /** 获取指定文件夹中所有文件的总大小 */ export const getDirectorySize = (dirPath: string) => { let totalSize = 0 const traverse = (currentPath:string) => { const items = readdirSync(currentPath) for(const item of items) { const fullPath = path.join(currentPath, item) const stats = statSync(fullPath) if (stats.isDirectory()) { traverse(fullPath) } else if (stats.isFile()) { totalSize += stats.size } } } traverse(dirPath) return totalSize }
ts// plugins/index.ts import type { ResolvedConfig, Plugin } from 'vite' import { resolve } from 'node:path' import { getDirectorySize, formatBytes } from './utils' import gradient from 'gradient-string' import boxen, { type Options as BoxenOptions } from 'boxen' import dayjs, { type Dayjs } from 'dayjs' import duration from 'dayjs/plugin/duration' dayjs.extend(duration) const gradientMessage = gradient(["cyan", "magenta"]).multiline( `从今以后我只能称呼你为您了,因为,你在我心上。\n疯狂Coding......` ) const boxenOptions: BoxenOptions = { padding: 0.5, borderColor: "cyan", borderStyle: "round" } type VoidFunc = () => void type PluginOptions = VoidFunc | string | Plugin export default function vueSloganPlugin(options?: PluginOptions): Plugin { let config: ResolvedConfig; let startTime: Dayjs; let endTime: Dayjs; let outDir: string; console.log(" 使用插件的时候传过来的参数: ", options); return { name: 'vite:vitePluginCaption', // configResolved: 在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置 configResolved(resolvedConfig: ResolvedConfig){ config = resolvedConfig; outDir = resolvedConfig.build?.outDir ?? "dist"; }, // Rollup 的钩子 buildStart() { console.log(boxen(gradientMessage, boxenOptions)) if (config.command === 'build') { startTime = dayjs(new Date()) } }, // Rollup的钩子 closeBundle() { // 只有在打包,运行 build 命令才生效; if (config.command === 'build') { endTime = dayjs(new Date()) const takeTime = dayjs.duration(endTime.diff(startTime)) const m = takeTime.get('minutes') const s = takeTime.get('second') const S = takeTime.get('milliseconds') try { const distPath = resolve(process.cwd(), outDir || 'dist') const totalBytes = getDirectorySize(distPath) const size = formatBytes(totalBytes) console.log(boxen( gradient(["cyan", "magenta"]).multiline( `🎉🎉 恭喜打包完成,总用时: ${m}分${s}秒${S}毫秒 🎉🎉, 打包后的大小为 ${size}` ), boxenOptions )) } catch (e) { throw e } } } } }
-
在vite的配置文件(
vite.config.ts
)中,使用插件; -
运行,打包项目,看看效果;运行
vite
,npm run dev
,npm run build
,vite build
的时候,在终端控制台,能看到 banner 和 打包用时以及打包后dist的大小输出;
npm方式
我们可以基于构建打包工具:
vite
,[tsup](https://tsup.egoist.dev/)
,[unbuild](https://github.com/unjs/unbuild#readme)
,rollup
等为基础来创建打包插件工程;
以下示例,我们基于tsup
来构建插件工程项目(正常是要配置 eslit prettier),我们这边暂时忽略
:
ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ['src/index.ts'], // 入口
outDir: 'dist', // 打包输出目录
clean: true, // 每次打包前清空目录
format: ['esm'], // 仅生成 ESM
dts: true, // 输出 d.ts 文件
minify: true, // 压缩代码
sourcemap: false
})
json
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"noEmit": true,
// tsc 仅检查,不生成 js 文件
"sourceMap": false,
"strict": true,
"declaration": true,
"declarationDir": "dist",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"removeComments": true,
},
"include": [
"src/**/*"
]
}
package.json
{
"name": "vite-plugin-output",
"version": "1.1.8",
"description": "A vite plug-in that, when building a package, outputs the duration of the package and the size of the file after the package",
"type": "module", // 必须的,这个要删掉
"module": "dist/index.js",// 必须的,和tsup.config.ts中对应,这个要删掉
"types": "dist/index.d.ts",// 必须的,和tsup.config.ts中对应,这个要删掉
"exports": {// 必须的,和tsup.config.ts中对应,这个要删掉
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"assets",
"README.md"
],
"scripts": {
"build": "tsup"
},
"keywords": [
"vite",
"vite plugin",
"output",
"duration",
"size",
"build"
],
"author": "evan <[email protected]>",
"license": "MIT",
"homepage": "https://gitee.com/evan_origin_admin/vite-plugin-output",
"repository": {
"type": "git",
"url": "https://gitee.com/evan_origin_admin/vite-plugin-output"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"tslib": "^2.8.1",
"tsup": "^8.4.0",
"typescript": "^5.8.2",
"vite": "^6.2.2"
},
"dependencies": {
"@types/node": "^22.13.11",
"boxen": "^8.0.1",
"dayjs": "^1.11.13",
"gradient-string": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
ts
// src/index.ts
import type { ResolvedConfig, Plugin } from 'vite'
import { resolve } from 'node:path'
import { getDirectorySize, formatBytes } from './utils'
import gradient from 'gradient-string'
import boxen, { type Options as BoxenOptions } from 'boxen'
import dayjs, { type Dayjs } from 'dayjs'
import duration from 'dayjs/plugin/duration.js'
dayjs.extend(duration)
// const gradientMessage = gradient(["cyan", "magenta"]).multiline(
// `从今以后我只能称呼你为您了,因为,你在我心上。\n疯狂Coding......`
// )
const boxenOptions: BoxenOptions = {
padding: 0.5,
borderColor: "cyan",
borderStyle: "round"
}
// type VoidFunc = () => void
// type PluginOptions = VoidFunc | string | Plugin
export default function vueSloganPlugin(): Plugin {
let config: ResolvedConfig;
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: 'vite-plugin-output',
// configResolved: 在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置
configResolved(resolvedConfig: ResolvedConfig){
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
// Rollup 的钩子
buildStart() {
// console.log(boxen(gradientMessage, boxenOptions))
if (config.command === 'build') {
startTime = dayjs(new Date())
}
},
// Rollup的钩子
closeBundle() {
// 只有在打包,运行 build 命令才生效;
if (config.command === 'build') {
endTime = dayjs(new Date())
const takeTime = dayjs.duration(endTime.diff(startTime))
const m = takeTime.get('minutes')
const s = takeTime.get('second')
const S = takeTime.get('milliseconds')
try {
const distPath = resolve(process.cwd(), outDir || 'dist')
const totalBytes = getDirectorySize(distPath)
const size = formatBytes(totalBytes)
console.log(boxen(
gradient(["cyan", "magenta"]).multiline(
`🎉🎉 恭喜打包完成,总用时: ${m}分${s}秒${S}毫秒 🎉🎉, 打包后的大小为 ${size}`
),
boxenOptions
))
} catch (e) {
throw e
}
}
}
}
}
ts
// src/utils.ts
import { readdirSync, statSync } from "node:fs"
import path from "node:path"
/** 格式化字节单位 */
export function formatBytes(bytes: number, decimals: number = 3): string {
const units: string[] = ['B', 'KB', 'MB', 'GB']
let unitIndex: number = 0
while (bytes >= 1024 && unitIndex < units.length -1) {
bytes /= 1024
unitIndex++
}
return `${bytes.toFixed(decimals)} ${units[unitIndex]}`
}
/** 获取指定文件夹中所有文件的总大小 */
export const getDirectorySize = (dirPath: string) => {
let totalSize = 0
const traverse = (currentPath:string) => {
const items = readdirSync(currentPath)
for(const item of items) {
const fullPath = path.join(currentPath, item)
const stats = statSync(fullPath)
if (stats.isDirectory()) {
traverse(fullPath)
} else if (stats.isFile()) {
totalSize += stats.size
}
}
}
traverse(dirPath)
return totalSize
}
然后在插件工程的根目录下运行 npm link
在要使用到的项目中 npm link vite-plugin-output
, 然后引入使用即可;
可以美化终端输出依赖
boxen: 可以在,在终端中创建盒子; gradient-string: 可以在,在终端中输出彩色文字样式;
picocolors:最小和最快的库,用于终端输出格式与ANSI颜色;
progress:灵活的ascii进度条;
rd: 列出(遍历)目录下的所有文件,包括子目录(支持 TypeScript);
yoctocolors: 最小、最快的命令行文字着色工具包;
chalk: 终端字符串样式;
优秀的vite插件
以下是部分优秀的插件🫡🫡:
vite-plugin-banner
vite-plugin-progress
vite-plugin-slogan
vite-plugin-url-copy
参考
Vite官方中文文档-插件API
Rollup中文文档-插件开发
Rollup插件开发
tsup应用打包构建
unbuild统一的js构建工具