深入解析Vite插件开发框架与实战
@meng-xi/vite-plugin不仅提供了 6 个开箱即用的构建插件,更是一套完整的 Vite 插件开发框架。本文将深入剖析其核心功能模块的实现原理、设计亮点、使用方法及与同类方案的对比优势,帮助开发者全面理解并高效使用这个工具库。
目录
一、引言
在现代前端工程化体系中,Vite 已成为主流构建工具。然而,实际项目中经常遇到构建进度不可见、静态资源复制繁琐、路由配置手动维护、版本号管理混乱、白屏体验差等痛点。市面上的 Vite 插件往往只解决单一问题,且缺乏统一的开发规范。
@meng-xi/vite-plugin 的设计理念是:提供一套完整的解决方案,而非零散的工具集合。它包含两层能力:
- 6 个内置插件 — 覆盖构建进度、文件复制、路由生成、版本管理、图标注入、全局 Loading 等高频场景
- 插件开发框架 — 导出
BasePlugin、Validator、Logger、createPluginFactory等核心组件,让开发者能以统一规范快速构建自定义插件
二、功能概述
2.1 库的定位与架构
@meng-xi/vite-plugin
│
├── 插件开发框架(基础设施层)
│ ├── BasePlugin 抽象基类:生命周期、配置合并、错误处理
│ ├── Validator 配置验证器:链式 API、批量错误收集
│ ├── Logger 日志管理器:单例 + 代理、统一前缀
│ ├── createPluginFactory 工厂函数:类 → Vite Plugin 转换
│ └── common 通用工具:fs 操作、格式化、深度合并
│
└── 内置插件(应用层)
├── buildProgress 终端构建进度条
├── copyFile 智能文件复制(增量)
├── generateRouter uni-app 路由自动生成
├── generateVersion 多格式版本号生成
├── injectIco HTML 图标注入
└── injectLoading 全局 Loading 状态管理
模块导出设计:通过 package.json 的 exports 字段提供 5 个入口点,支持按需导入:
{
"exports": {
".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" },
"./common": { "import": "./dist/common/index.mjs", ... },
"./factory": { "import": "./dist/factory/index.mjs", ... },
"./logger": { "import": "./dist/logger/index.mjs", ... },
"./plugins": { "import": "./dist/plugins/index.mjs", ... }
}
}
每个入口同时提供 ESM(.mjs)、CJS(.cjs)和类型声明(.d.ts),兼容所有构建工具和 Node.js 版本。
2.2 核心功能模块一览
| 模块 | 功能 | 关键特性 |
|---|---|---|
| BasePlugin | 插件基类 | 三层配置合并、钩子自动组合、安全执行、生命周期管理 |
| Validator | 配置验证 | 链式 API、批量错误收集、自定义规则、默认值设置 |
| Logger | 日志管理 | 单例模式、插件级代理、统一前缀、ANSI 颜色 |
| createPluginFactory | 工厂函数 | 三重泛型、选项标准化器、pluginInstance 暴露 |
| common | 通用工具 | 增量文件复制、并发控制、深度合并、格式化工具 |
| buildProgress | 构建进度 | 三种格式、进度只进不退、TTY 感知、自定义主题 |
| copyFile | 文件复制 | 增量判断、并发复制、递归支持、详细统计 |
| generateRouter | 路由生成 | pages.json 解析、子包支持、文件监听、用户修改保留 |
| generateVersion | 版本号 | 6 种格式、文件/全局变量双输出、自定义模板 |
| injectIco | 图标注入 | HtmlTagDescriptor API、字符串简写、图标文件复制 |
| injectLoading | Loading 管理 | 双阶段注入、请求拦截、防闪烁、指针事件控制、安全验证 |
2.3 与同类库的对比优势
| 对比维度 | @meng-xi/vite-plugin | vite-plugin-progress | vite-plugin-copy | vite-plugin-html |
|---|---|---|---|---|
| 功能覆盖 | 6 个插件 + 开发框架 | 仅进度条 | 仅文件复制 | 仅 HTML 注入 |
| 插件开发框架 | 完整(BasePlugin + Validator + Logger) | 无 | 无 | 无 |
| 错误处理 | 三级策略(throw/log/ignore) | 无统一策略 | 无统一策略 | 无统一策略 |
| 配置验证 | 内置 Validator,批量错误收集 | 无 | 无 | 无 |
| 日志管理 | 单例 + 代理,统一前缀 | 各自为政 | 各自为政 | 各自为政 |
| 类型安全 | 全链路 TypeScript | 部分 | 部分 | 部分 |
| 增量复制 | mtimeMs + size 双判断 | — | 部分支持 | — |
| Loading 防闪烁 | delayShow + minDisplayTime + debounceHide | — | — | — |
| Loading 指针控制 | enablePointerEvents / disablePointerEvents | — | — | — |
| 路由用户修改保留 | preserveRouteChanges | — | — | — |
| 按需导入 | 5 个子路径入口 | 单入口 | 单入口 | 单入口 |
核心优势总结:
- 框架级一致性 — 所有内置插件共享相同的生命周期、错误处理、日志和验证机制,而非各自为政
- 可扩展性 — 开发者可基于
BasePlugin构建自定义插件,享受与内置插件完全相同的基础设施 - 防御性编程 —
safeExecute+errorStrategy确保单个插件的异常不会意外中断整个构建流程 - 生产级细节 — 增量复制、防闪烁、TTY 感知、SSR 安全检测、XSS 防护等,都是实际生产环境中的刚需
三、插件开发框架
3.1 BasePlugin — 插件基类
BasePlugin 是整个库的核心,所有内置插件和自定义插件都继承自它。它提供了完整的生命周期管理、配置合并、错误处理和钩子自动组合机制。
3.1.1 生命周期流程
┌──────────────────────────────────────────────────────────────┐
│ BasePlugin 生命周期 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ① constructor │
│ ├── mergeOptions() 三层配置深度合并 │
│ │ deepMerge(baseDefaults, pluginDefaults, userOptions) │
│ ├── initLogger() 创建插件日志代理 │
│ │ Logger.create() → createPluginLogger() │
│ ├── new Validator() 初始化配置验证器 │
│ └── validateOptions() 执行配置验证(safeExecuteSync)│
│ │
│ ② toPlugin() → Vite Plugin 对象 │
│ ├── addPluginHooks() 子类注册业务钩子 │
│ ├── 自动组合 configResolved │
│ │ 基类.onConfigResolved() → 子类钩子 │
│ └── 自动组合 closeBundle │
│ 子类钩子 → 基类.destroy() │
│ │
│ ③ destroy() │
│ ├── 子类重写:清理 watcher、定时器等 │
│ └── 基类:Logger.unregister() 注销日志 │
│ │
└──────────────────────────────────────────────────────────────┘
3.1.2 三层配置合并
protected mergeOptions(options: T): Required<T> {
const baseDefaults: BasePluginOptions = {
enabled: true,
verbose: true,
errorStrategy: 'throw'
}
const pluginDefaults = this.getDefaultOptions()
return deepMerge(baseDefaults, pluginDefaults, options) as Required<T>
}
合并优先级:baseDefaults < pluginDefaults < userOptions。deepMerge 的关键行为是 undefined 不覆盖已有值(保护默认值),null 会覆盖(允许显式置空),嵌套对象递归合并,数组直接覆盖。
3.1.3 钩子自动组合
toPlugin() 方法最精巧的设计是自动组合 configResolved 和 closeBundle 钩子:
public toPlugin(): Plugin {
const plugin: Plugin = {
name: this.getPluginName(),
enforce: this.getEnforce()
}
this.addPluginHooks(plugin)
// 自动组合 configResolved:基类先存储 viteConfig,子类才能使用
const subclassConfigResolved = plugin.configResolved
plugin.configResolved = (config) => {
if (this.options.enabled) {
this.onConfigResolved(config)
if (typeof subclassConfigResolved === 'function') {
subclassConfigResolved(config)
}
}
}
// 自动组合 closeBundle:子类先清理资源,基类再注销日志
const instance = this
const subclassCloseBundle = plugin.closeBundle
plugin.closeBundle = function() {
if (typeof subclassCloseBundle === 'function') {
subclassCloseBundle.call(this)
}
instance.destroy()
}
return plugin
}
设计意图:
- configResolved — 基类先存储
viteConfig,子类钩子才能通过this.viteConfig访问 - closeBundle — 子类先清理资源(如关闭 watcher、停止定时器),基类再注销日志配置
子类无需手动注册这两个钩子,只需重写 onConfigResolved() 和 destroy() 方法即可。
3.1.4 错误处理策略
interface BasePluginOptions {
errorStrategy?: 'throw' | 'log' | 'ignore'
}
操作执行
│
├── 成功 → 返回结果
│
└── 异常 → handleError()
│
├── 'throw'(默认)
│ logger.error() → throw error → 中断构建
│
└── 'log' | 'ignore'
logger.error() → return undefined → 继续执行
所有内置插件的异步操作都通过 safeExecute 包裹:
plugin.writeBundle = async () => {
await this.safeExecute(() => this.copyFiles(), '复制文件')
}
这确保了即使复制文件失败,也不会导致整个构建进程崩溃(当 errorStrategy 为 'log' 时)。
3.2 Validator — 配置验证器
Validator 提供流畅的链式 API,是所有插件配置验证的基础:
this.validator
.field('sourceDir')
.required()
.string()
.custom(val => val.trim() !== '', 'sourceDir 不能为空字符串')
.field('overwrite')
.boolean()
.default(true)
.field('incremental')
.boolean()
.default(true)
.validate()
核心设计:验证错误不立即抛出,而是收集到 errors 数组中,最终在 validate() 时一次性抛出所有错误。这让用户能一次看到所有配置问题,而非逐个修复。
支持的验证方法:
| 方法 | 说明 |
|---|---|
field(name) |
指定验证字段 |
required() |
标记为必填 |
string() / boolean() / number() / array() / object() |
类型验证 |
default(value) |
设置默认值(仅当值为 undefined/null 时生效) |
custom(fn, msg) |
自定义验证规则 |
validate() |
执行验证,失败时抛出包含所有错误的异常 |
Validator 实例可独立使用:injectIco 插件中为嵌套的 copyOptions 创建了独立的 Validator 实例进行验证,体现了 Validator 的复用能力。
3.3 Logger — 日志管理器
Logger 采用 单例 + 代理 双层架构:
Logger(全局单例)
│
├── pluginConfigs: Map<string, boolean> ← 各插件日志开关
│
├── 静态方法
│ ├── Logger.create({ name, enabled }) ← 注册插件 + 返回单例
│ ├── Logger.unregister(name) ← 注销插件日志配置
│ └── Logger.destroy() ← 销毁单例(测试用)
│
└── createPluginLogger(name) → PluginLogger ← 创建插件级代理
├── info(message, data?)
├── success(message, data?)
├── warn(message, data?)
└── error(message, data?)
日志输出格式:
ℹ️ [@meng-xi/vite-plugin:build-progress] 转换模块 67%
✅ [@meng-xi/vite-plugin:copy-file] 复制文件成功:从 src/assets 到 dist/assets
⚠️ [@meng-xi/vite-plugin:generate-router] pages.json 中没有有效的页面配置
❌ [@meng-xi/vite-plugin:inject-ico] 图标文件不存在: /assets/favicon.ico
设计要点:
- 统一前缀
[@meng-xi/vite-plugin:插件名],多插件并行时快速定位来源 - 每个插件通过
verbose选项独立控制日志开关 - 插件销毁时自动
Logger.unregister(),防止内存泄漏 - 支持 ANSI 颜色码,终端输出直观区分日志级别
data参数支持附加数据输出,用于展示详细统计信息
3.4 createPluginFactory — 工厂函数
function createPluginFactory<T extends BasePluginOptions, P extends BasePlugin<T>, R = T>(PluginClass: new (options: T, loggerConfig?: LoggerOptions) => P, normalizer?: OptionsNormalizer<T, R>): PluginFactory<T, R>
三重泛型设计:
T— 插件配置类型(如InjectIcoOptions)P— 插件实例类型(如InjectIcoPlugin)R— 原始配置类型(如string | InjectIcoOptions,支持简写)
OptionsNormalizer 的妙用 — 支持字符串简写:
// injectIco 支持字符串简写:injectIco('/assets')
export const injectIco = createPluginFactory<InjectIcoOptions, InjectIcoPlugin, string | InjectIcoOptions>(InjectIcoPlugin, options => (typeof options === 'string' ? { base: options } : options || {}))
pluginInstance 暴露机制:
return (options?: R) => {
const normalizedOptions = (normalizer ? normalizer(options) : options) as T
const plugin = new PluginClass(normalizedOptions)
const vitePlugin = plugin.toPlugin() as PluginWithInstance<T>
vitePlugin.pluginInstance = plugin // 挂载原始实例
return vitePlugin
}
返回的 Vite Plugin 对象上附加了 pluginInstance 属性,允许外部访问插件内部状态(如 options、logger 等),实现了封装与可观测性的平衡。
3.5 common — 通用工具模块
common 模块是插件开发框架的基础工具层,提供 4 个子模块:
| 子模块 | 功能 | 关键导出 |
|---|---|---|
fs |
文件系统操作 | checkSourceExists、copySourceToTarget、shouldUpdateFile、runWithConcurrency、readFileContent、writeFileContent、fileExists、readFileSync(已废弃) |
format |
格式化工具 | generateRandomHash、getDateFormatParams、parseTemplate、stripJsonComments、formatDate、padNumber、toCamelCase、toPascalCase |
object |
对象工具 | deepMerge |
validation |
验证工具 | Validator |
核心工具详解:
增量文件判断 — shouldUpdateFile 通过比较源文件与目标文件的 mtimeMs(修改时间戳)和 size(文件大小)判断是否需要更新:
async function shouldUpdateFile(sourceFile: string, targetFile: string): Promise<boolean> {
try {
const [sourceStats, targetStats] = await Promise.all([fs.promises.stat(sourceFile), fs.promises.stat(targetFile)])
return sourceStats.mtimeMs > targetStats.mtimeMs || sourceStats.size !== targetStats.size
} catch {
return true // 目标文件不存在,需要复制
}
}
文件存在检查 — fileExists 用于判断文件是否存在,在增量复制中替代 try-catch 模式:
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.promises.access(filePath, fs.constants.F_OK)
return true
} catch {
return false
}
}
并发控制 — runWithConcurrency 使用 Worker Pool 模式,N 个 Worker 共享递增索引并行消费任务队列,结果按原始顺序存储:
async function runWithConcurrency<T, R>(items: T[], handler: (item: T) => Promise<R>, concurrency: number): Promise<R[]> {
const results: R[] = []
let index = 0
async function runNext(): Promise<void> {
while (index < items.length) {
const currentIndex = index++
results[currentIndex] = await handler(items[currentIndex])
}
}
const workers = Array(Math.min(concurrency, items.length))
.fill(null)
.map(() => runNext())
await Promise.all(workers)
return results
}
日期格式化 — formatDate 和 getDateFormatParams 提供灵活的日期格式化能力:
const params = getDateFormatParams(new Date())
// { YYYY: '2026', MM: '05', DD: '23', HH: '15', mm: '30', ss: '00', SSS: '123', timestamp: '1748001000000' }
formatDate(new Date(), '{YYYY}-{MM}-{DD}') // '2026-05-23'
四、内置插件详解
4.1 buildProgress — 构建进度可视化
在 Vite 构建过程中实时显示终端进度条,支持三种格式和自定义主题。
实现原理
进度计算 — 基于构建生命周期阶段和模块转换比例计算进度:
| 阶段 | 进度范围 | 说明 |
|---|---|---|
config |
5% | 配置解析完成 |
resolve |
10% | 模块依赖解析 |
transform |
15% - 85% | 按模块转换比例线性增长 |
bundle |
+10% | 打包阶段(仅生产构建) |
write |
+5% | 写入文件阶段 |
done |
100% | 构建完成 |
进度只进不退 — 使用 lastPercentage 记录历史最高进度,Math.max(calculated, lastPercentage) 确保进度不会回退。
TTY 感知 — 通过 process.stdout.isTTY 检测终端类型,非 TTY 环境(如 CI/CD)自动降级为日志输出。
Spinner 动画 — 以 80ms 间隔定时刷新进度显示,实现流畅的旋转动画效果。构建完成时自动停止并恢复光标显示。
模块排除 — node_modules 和 .virtual 模块自动排除,不计入进度统计。
配置选项
buildProgress({
width: 30, // 进度条宽度(字符数)
format: 'bar', // 'bar' | 'spinner' | 'minimal'
completeChar: '█', // 已完成填充字符
incompleteChar: '░', // 未完成填充字符
clearOnComplete: true, // 构建完成后是否清除进度条
showModuleName: true, // 是否显示当前处理模块名
theme: {
// 自定义颜色主题
completeColor: t => `\x1b[32m${t}\x1b[39m`,
incompleteColor: t => `\x1b[90m${t}\x1b[39m`,
percentageColor: t => `\x1b[1m${t}\x1b[22m`,
phaseColor: t => `\x1b[36m${t}\x1b[39m`,
moduleColor: t => `\x1b[90m${t}\x1b[39m`
}
})
适用场景
- 开发/生产构建的进度可视化
- CI/CD 流水线中的构建监控(自动降级为日志)
- 大型项目的构建时间评估
4.2 copyFile — 智能文件复制
在 Vite 构建完成后将指定目录的文件复制到目标位置,支持增量复制避免不必要的 IO 开销。
实现原理
增量判断逻辑 — 通过比较源文件与目标文件的 mtimeMs(修改时间戳)和 size(文件大小)判断是否需要更新:
async function shouldUpdateFile(sourceFile: string, targetFile: string): Promise<boolean> {
try {
const [sourceStats, targetStats] = await Promise.all([fs.promises.stat(sourceFile), fs.promises.stat(targetFile)])
return sourceStats.mtimeMs > targetStats.mtimeMs || sourceStats.size !== targetStats.size
} catch {
return true // 目标文件不存在,需要复制
}
}
并发复制 — 使用 Worker Pool 模式(runWithConcurrency),N 个 Worker 共享递增索引并行消费任务队列,结果按原始顺序存储。默认并发限制为 10。
复制流程:
- 一次性获取所有文件条目(含类型信息,避免重复
stat调用) - 预先并行创建所有目标目录(
Promise.all+ensureTargetDir) - 并发复制文件(
runWithConcurrency,默认 10 并发) - 统计并返回复制结果(copiedFiles / skippedFiles / copiedDirs / executionTime)
安全执行 — 复制操作通过 safeExecute 包裹,确保异常不会导致构建崩溃:
protected addPluginHooks(plugin: Plugin): void {
plugin.writeBundle = async () => {
await this.safeExecute(() => this.copyFiles(), '复制文件')
}
}
enforce 设置 — 插件设置 enforce: 'post',确保在其他插件处理完构建产物后再执行文件复制。
配置选项
copyFile({
sourceDir: 'src/assets', // 源目录路径(必填)
targetDir: 'dist/assets', // 目标目录路径(必填)
overwrite: true, // 是否覆盖现有文件
recursive: true, // 是否递归复制子目录
incremental: true // 是否启用增量复制
})
适用场景
- 静态资源(图片、字体等)从源目录复制到构建输出目录
- 多环境配置文件的按需复制
- 大型项目中避免全量复制带来的性能开销
4.3 generateRouter — 路由配置自动生成
读取 uni-app 项目的 pages.json,自动生成路由配置文件,支持文件监听和用户修改保留。
实现原理
解析流程:
pages.json → stripJsonComments → JSON.parse → 解析主包页面 → 解析子包页面
│
└── 解析 tabBar 页面
路由名称策略 — 支持 4 种命名方式:
| 策略 | 示例路径 | 生成名称 |
|---|---|---|
path |
/pages/index |
pages_index |
camelCase |
/pages/user-list |
pagesUserList |
pascalCase |
/pages/user-list |
PagesUserList |
custom |
自定义函数 | 由 customNameGenerator 决定 |
元信息映射 — 通过 metaMapping 配置将 pages.json 中的字段映射到路由 meta:
metaMapping: {
navigationBarTitleText: 'title', // style.navigationBarTitleText → meta.title
requireAuth: 'requireAuth' // style.requireAuth → meta.requireAuth
}
用户修改保留 — 这是 generateRouter 最独特的设计亮点。当 preserveRouteChanges 为 true 时,重新生成路由配置会保留用户对已有路由的手动修改:
private mergeRoutes(newRoutes: RouteConfig[], existingRoutesMap: Map<string, RouteConfig>): RouteConfig[] {
return newRoutes.map(newRoute => {
const existingRoute = existingRoutesMap.get(newRoute.path)
if (!existingRoute) return newRoute
const mergedMeta: RouteMeta = {}
if (newRoute.meta) Object.assign(mergedMeta, newRoute.meta)
if (existingRoute.meta) Object.assign(mergedMeta, existingRoute.meta)
return {
...existingRoute,
path: newRoute.path,
meta: Object.keys(mergedMeta).length > 0 ? mergedMeta : undefined
}
})
}
合并策略:
path始终使用新生成的(由 pages.json 决定,是路由标识符)name等其他字段保留用户修改(...existingRoute优先)meta先用新生成的作为基础,再用用户现有的覆盖(用户修改优先)
文件监听 — 开发模式下自动监听 pages.json 变化并重新生成路由配置,监听器在 destroy() 时自动关闭。
输出格式 — 生成的路由配置文件支持 TypeScript 和 JavaScript 两种格式,TypeScript 格式自动包含类型定义(RouteMeta、RouteConfig 接口)。
配置选项
generateRouter({
pagesJsonPath: 'src/pages.json', // pages.json 文件路径
outputPath: 'src/router.config.ts', // 输出文件路径
outputFormat: 'ts', // 'ts' | 'js'
nameStrategy: 'camelCase', // 'path' | 'camelCase' | 'pascalCase' | 'custom'
includeSubPackages: true, // 是否包含子包页面
watch: true, // 开发模式下是否监听文件变化
exportTypes: true, // 是否导出类型定义
preserveRouteChanges: true, // 是否保留用户修改
metaMapping: {
// 元信息字段映射
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
适用场景
- uni-app 项目的路由配置自动化管理
- 多人协作中避免手动维护路由配置导致的冲突
- 需要在路由 meta 中添加自定义字段(如权限控制)的场景
4.4 generateVersion — 多格式版本号生成
在 Vite 构建过程中自动生成版本号,支持多种格式和输出方式。
实现原理
6 种版本号格式:
| 格式 | 示例输出 | 说明 |
|---|---|---|
timestamp |
20260203153000 |
时间戳格式 |
date |
2026.02.03 |
日期格式 |
datetime |
2026.02.03.153000 |
日期时间格式 |
semver |
1.0.0 |
语义化版本(基于 semverBase) |
hash |
a1b2c3d4 |
随机哈希(可配置长度 1-32) |
custom |
自定义 | 基于模板解析,支持占位符 |
自定义模板 — 支持丰富的占位符:
generateVersion({
format: 'custom',
customFormat: '{YYYY}.{MM}.{DD}-{hash}', // 输出: 2026.02.03-a1b2c3d4
hashLength: 8
})
可用占位符:{YYYY}、{YY}、{MM}、{DD}、{HH}、{mm}、{ss}、{SSS}、{timestamp}、{hash}、{major}、{minor}、{patch}。
双输出模式:
| outputType | 行为 |
|---|---|
file |
写入版本文件(默认 version.json)到构建输出目录 |
define |
通过 Vite 的 define 注入全局变量(默认 __APP_VERSION__) |
both |
同时执行文件写入和全局变量注入 |
版本信息对象 — 文件输出模式生成的 version.json 包含丰富的构建信息:
{
"version": "20260203153000",
"buildTime": "2026-02-03T15:30:00.000Z",
"timestamp": 1738567800000,
"format": "timestamp"
}
可通过 extra 字段追加自定义信息(如环境、作者等)。
前缀/后缀 — 支持 prefix 和 suffix 配置,如 prefix: 'v' 可生成 v1.0.0。
配置选项
generateVersion({
format: 'timestamp', // 版本号格式
semverBase: '1.0.0', // semver 格式的基础版本
outputType: 'file', // 'file' | 'define' | 'both'
outputFile: 'version.json', // 输出文件名
defineName: '__APP_VERSION__', // 全局变量名
hashLength: 8, // hash 格式的长度
prefix: '', // 版本号前缀
suffix: '', // 版本号后缀
extra: {
// 额外信息
environment: 'production'
}
})
适用场景
- 构建产物版本追溯
- 前端版本检测与热更新提示
- 自动化部署流程中的版本标记
4.5 injectIco — HTML 图标注入
在构建过程中将图标链接注入到 HTML 文件的 <head> 标签中,并可选复制图标文件到目标目录。
实现原理
双模式注入 — 根据配置自动选择注入方式:
- 自定义 link 标签模式 — 当配置了
link字段时,使用字符串替换方式将完整的<link>标签注入到</head>前 - HtmlTagDescriptor 模式 — 当配置了
icons数组时,使用 Vite 原生HtmlTagDescriptorAPI 注入图标标签
plugin.transformIndexHtml = {
order: 'pre',
handler: (html: string) => {
// 如果使用自定义 link 标签,使用字符串替换方式
if (this.options.link) {
return this.injectCustomLinkTag(html)
}
// 否则使用 Vite 原生 HtmlTagDescriptor API
const tags = this.getIconTagDescriptors()
if (tags.length > 0) {
return { html, tags }
}
return html
}
}
字符串简写 — 通过 OptionsNormalizer 支持 injectIco('/assets') 的简写形式,自动转换为 { base: '/assets' }:
export const injectIco = createPluginFactory<InjectIcoOptions, InjectIcoPlugin, string | InjectIcoOptions>(InjectIcoPlugin, options => (typeof options === 'string' ? { base: options } : options || {}))
图标文件复制 — 可选配置 copyOptions,在 writeBundle 阶段将图标文件复制到目标目录,复用 common 模块的 copySourceToTarget 实现增量复制。
配置选项
injectIco({
base: '/', // 图标基础路径
url: '/favicon.ico', // 图标 URL
link: '<link rel="icon" ...>', // 自定义 link 标签
icons: [
// 图标数组(使用 HtmlTagDescriptor API)
{ rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
{ rel: 'icon', href: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' }
],
copyOptions: {
// 图标文件复制配置
sourceDir: 'src/assets/icons',
targetDir: 'dist/assets/icons'
}
})
// 字符串简写
injectIco('/assets')
适用场景
- 多尺寸 favicon 注入
- PWA 图标配置
- 不同环境使用不同图标的场景
4.6 injectLoading — 全局 Loading 状态管理
将全局 Loading 状态管理代码注入到 HTML 中,提供创建、显示、隐藏和销毁 loading 的方法,支持自动拦截 fetch/XHR 请求实现 loading 的自动管理。
实现原理
双阶段注入策略 — 根据 defaultVisible 配置决定注入方式:
defaultVisible = true(白屏 Loading)
┌─────────────────────────────────────────────┐
│ <html> │
│ <head> │
│ <!-- 阶段一:CSS + HTML 静态注入 --> │
│ <style data-loading-style │
│ data-loading-id="...">...</style> │
│ <div id="__loading-root__">...</div> │
│ → HTML 解析即显示,零 JS 依赖 │
│ </head> │
│ <body> │
│ <!-- 阶段二:JS 管理器注入 --> │
│ <script> Loading Manager IIFE </script> │
│ → 运行时 API、请求拦截、事件监听 │
│ </body> │
│ </html> │
└─────────────────────────────────────────────┘
defaultVisible = false(按需 Loading)
┌─────────────────────────────────────────────┐
│ <html> │
│ <head> │
│ <!-- 无静态注入 --> │
│ </head> │
│ <body> │
│ <!-- 完整注入:CSS + HTML + JS --> │
│ <script> │
│ 动态创建 <style> 和 <div> │
│ + Loading Manager IIFE │
│ </script> │
│ </body> │
└─────────────────────────────────────────────┘
白屏 Loading 的关键:当 defaultVisible = true 时,CSS 和 HTML 以静态标签形式直接注入到 <head> 中,浏览器解析到 <head> 时 loading 即可见,无需等待 JS 执行,实现真正的白屏 Loading 效果。
资源缓存 — CSS 和 HTML 只生成一次并缓存(_cachedAssets),供 head 和 body 注入共享,避免重复计算。
注入顺序 — transformIndexHtml 设置 order: 'post',确保在其他 HTML 转换插件之后执行,避免被覆盖。
注入降级策略 — 依次尝试 </body> → </html> → 追加到末尾,确保在各种 HTML 结构下都能成功注入。
请求拦截 — 自动拦截 fetch/XHR 请求,实现请求开始时自动显示 Loading,请求结束时自动隐藏:
// fetch 拦截
if (autoBind === 'fetch' || autoBind === 'all') {
_originalFetch = window.fetch;
window.fetch = function(input, init) {
var url = typeof input === 'string' ? input : (input instanceof URL ? input.href : ...);
var method = (init && init.method) || (input && input.method) || 'GET';
manager._requestStart(url, method);
return _originalFetch.apply(this, arguments).then(
function(response) { manager._requestEnd(url, method); return response; },
function(error) { manager._requestEnd(url, method); throw error; }
);
};
}
// XHR 拦截 — 使用 addEventListener('loadend') 监听请求结束
XMLHttpRequest.prototype.open = function(method, url) {
this.__loadingUrl = url || '';
this.__loadingMethod = method || 'GET';
return _originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
var self = this;
manager._requestStart(self.__loadingUrl, self.__loadingMethod);
self.addEventListener('loadend', function() {
manager._requestEnd(self.__loadingUrl, self.__loadingMethod);
});
return _originalXHRSend.apply(this, arguments);
};
并发请求管理 — 内部维护 pendingCount 计数器,首个请求开始时显示 Loading,最后一个请求结束时隐藏,正确处理并发场景。
请求过滤 — 通过 requestFilter 配置排除特定请求:
requestFilter: {
excludeUrls: [/\/api\/health/], // 排除特定 URL(正则)
includeUrls: [/\/api\/data/], // 仅包含特定 URL
excludeMethods: ['OPTIONS'], // 排除特定 HTTP 方法
excludeUrlPrefixes: ['/static/'] // 排除特定 URL 前缀
}
防闪烁机制 — 三重防护避免 Loading 频繁显示/隐藏导致的视觉闪烁:
请求开始
│
┌────────┴────────┐
│ delayShow 启用? │
└────────┬────────┘
是 │ │ 否
┌─────┘ └──→ 立即显示
│ 延迟 duration ms
│
├── 延迟期间请求结束 → 取消显示
└── 延迟到期 → 显示 Loading
│
┌─────────┴─────────┐
│ minDisplayTime 启用?│
└─────────┬─────────┘
是 │ │ 否
┌─────┘ └──→ 请求结束即隐藏
│ 保证至少显示 duration ms
│
├── 最小显示时间内请求结束 → 延迟隐藏
└── 最小显示时间到期 → 准备隐藏
│
┌─────────────┴─────────────┐
│ debounceHide 启用? │
└─────────────┬─────────────┘
是 │ │ 否
┌─────┘ └──→ 立即隐藏
│ 防抖 duration ms
│
├── 防抖期间新请求 → 取消隐藏
└── 防抖到期 → 隐藏 Loading
元素就绪重试 — _doShow 方法在 loading DOM 元素未就绪时会自动重试(最多 20 次,每次间隔 50ms),确保在动态注入场景下也能正常显示:
function _doShow(text) {
if (_destroyed) return
_findEl()
if (!_loadingEl) {
if (++_showRetryCount > _maxShowRetries) return
_retryTimer = setTimeout(function () {
_retryTimer = null
_doShow(text)
}, 50)
return
}
// ... 正常显示逻辑
}
请求拦截恢复 — destroy() 方法会恢复原始的 fetch 和 XMLHttpRequest 方法,避免内存泄漏和副作用残留:
function _restoreInterceptors() {
if (_originalFetch && typeof window !== 'undefined' && window.fetch) {
window.fetch = _originalFetch
_originalFetch = null
}
if (_originalXHROpen && typeof window !== 'undefined' && window.XMLHttpRequest) {
XMLHttpRequest.prototype.open = _originalXHROpen
XMLHttpRequest.prototype.send = _originalXHRSend
_originalXHROpen = null
_originalXHRSend = null
}
}
SSR 安全检测 — 生成的 JS 代码开头包含 SSR 环境检测:
if (typeof window === 'undefined' || typeof document === 'undefined') return
CSS 性能优化 — 遮罩层样式包含 CSS 性能提示:
.__loading-overlay__ {
contain: content; /* CSS Containment,优化渲染性能 */
will-change: opacity; /* 提示浏览器优化 opacity 动画 */
}
.__loading-overlay__.__loading-hidden__ .__loading-spinner__,
.__loading-overlay__.__loading-hidden__ .__loading-dot__,
.__loading-overlay__.__loading-hidden__ .__loading-spinner__::after {
animation-play-state: paused; /* 隐藏时暂停动画,减少 CPU 开销 */
}
安全验证 — 0.0.9 版本新增多项安全验证:
| 验证器 | 防护目标 |
|---|---|
validateCustomTemplate |
阻止 customTemplate 中包含 <script> 标签(XSS 防护) |
validateCallbacks |
阻止回调字符串中包含 <script> 标签(XSS 防护) |
validateGlobalName |
阻止使用 __proto__、constructor 等危险属性(原型污染) |
validateDefaultText |
空字符串时发出警告 |
validateAutoHideOn |
defaultVisible 为 false 时 autoHideOn 无效,发出警告 |
生命周期回调 — 支持在 Loading 显示/隐藏/销毁时执行自定义逻辑,回调以函数体字符串形式提供(因为需要注入到客户端代码中):
callbacks: {
onBeforeShow: 'console.log("about to show")', // 返回 false 可阻止显示
onShow: 'console.log("shown")',
onBeforeHide: 'console.log("about to hide")', // 返回 false 可阻止隐藏
onHide: 'console.log("hidden")',
onDestroy: 'console.log("destroyed")'
}
4 种 Spinner 类型:
| 类型 | 说明 |
|---|---|
spinner |
旋转圆环(默认) |
dots |
跳动圆点 |
pulse |
脉冲效果 |
bar |
进度条动画 |
autoHideOn 自动隐藏 — 当 defaultVisible = true 时,支持三种自动隐藏时机:
| 值 | 行为 |
|---|---|
DOMContentLoaded |
DOM 加载完成后自动隐藏(默认) |
load |
页面完全加载后自动隐藏 |
manual |
手动调用 hide() 隐藏 |
配置选项
injectLoading({
position: 'center', // 'center' | 'top' | 'bottom'
defaultText: '加载中...', // 默认文本
spinnerType: 'spinner', // 'spinner' | 'dots' | 'pulse' | 'bar'
defaultVisible: false, // 是否默认可见(白屏 Loading)
autoHideOn: 'DOMContentLoaded', // 自动隐藏时机
autoBind: 'none', // 'fetch' | 'xhr' | 'all' | 'none'
globalName: '__LOADING_MANAGER__', // 全局变量名
customTemplate: '<div class="my-loader"><span data-loading-text></span></div>', // 自定义 HTML 模板
style: {
overlayColor: 'rgba(255, 255, 255, 0.7)',
spinnerColor: '#4361ee',
spinnerSize: '40px',
textColor: '#333',
textSize: '14px',
customClass: '', // 自定义 CSS 类名
customStyle: '', // 自定义内联样式字符串
zIndex: 9999,
pointerEvents: true, // 是否阻止底层交互(默认 true = 阻止)
backdropBlur: false, // 是否启用背景模糊
backdropBlurAmount: 4 // 模糊程度
},
transition: {
enabled: true,
duration: 200,
easing: 'ease-out'
},
minDisplayTime: { enabled: true, duration: 300 },
delayShow: { enabled: true, duration: 200 },
debounceHide: { enabled: false, duration: 100 },
requestFilter: {
excludeUrls: [/\/api\/health/],
excludeMethods: ['OPTIONS'],
excludeUrlPrefixes: ['/static/']
},
callbacks: {
onBeforeShow: 'return true',
onShow: '',
onBeforeHide: 'return true',
onHide: '',
onDestroy: ''
}
})
运行时 API
注入后通过全局变量(默认 window.__LOADING_MANAGER__)提供以下方法:
| 方法 | 说明 |
|---|---|
show(text?) |
显示 Loading(受 delayShow 控制) |
hide() |
隐藏 Loading(受 minDisplayTime 和 debounceHide 控制) |
forceHide() |
强制隐藏 Loading(忽略所有延迟和防抖) |
toggle(text?) |
切换 Loading 的显示/隐藏状态 |
enablePointerEvents() |
启用遮罩层指针事件(阻止底层交互) |
disablePointerEvents() |
禁用遮罩层指针事件(允许交互穿透) |
togglePointerEvents() |
切换遮罩层指针事件状态 |
updateText(text) |
更新 Loading 文本 |
isVisible() |
获取当前可见状态 |
isPointerEventsEnabled() |
获取当前遮罩层指针事件是否启用 |
getPendingCount() |
获取当前进行中的请求数 |
destroy() |
销毁管理器(移除 DOM、恢复拦截器、执行回调) |
适用场景
- 白屏 Loading:
defaultVisible: true+autoHideOn: 'DOMContentLoaded' - 请求自动 Loading:
autoBind: 'all'+requestFilter排除不需要的请求 - 手动控制 Loading:
autoBind: 'none',通过全局 API 手动调用 - 防闪烁场景:启用
delayShow+minDisplayTime+debounceHide - 运行时切换交互:
enablePointerEvents()/disablePointerEvents()动态控制遮罩层交互
五、使用指南
5.1 安装与快速上手
# 安装
npm install @meng-xi/vite-plugin
# 或
pnpm add @meng-xi/vite-plugin
// vite.config.ts
import { defineConfig } from 'vite'
import { buildProgress, copyFile, generateRouter, generateVersion, injectIco, injectLoading } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
buildProgress(),
copyFile({
sourceDir: 'src/assets',
targetDir: 'dist/assets'
}),
generateRouter(),
generateVersion({
format: 'datetime',
outputType: 'both'
}),
injectIco('/assets'),
injectLoading({
defaultVisible: true,
autoHideOn: 'DOMContentLoaded',
autoBind: 'all'
})
]
})
5.2 按需导入
// 仅导入插件
import { buildProgress, copyFile } from '@meng-xi/vite-plugin/plugins'
// 仅导入开发框架
import { BasePlugin, createPluginFactory } from '@meng-xi/vite-plugin/factory'
// 仅导入日志模块
import { Logger } from '@meng-xi/vite-plugin/logger'
// 仅导入通用工具
import { deepMerge, copySourceToTarget } from '@meng-xi/vite-plugin/common'
5.3 自定义插件开发
基于 BasePlugin 开发自定义插件只需 4 步:
import { BasePlugin, createPluginFactory } from '@meng-xi/vite-plugin/factory'
import type { Plugin } from 'vite'
// 1. 定义配置类型
interface MyPluginOptions {
message: string
count?: number
}
// 2. 继承 BasePlugin
class MyPlugin extends BasePlugin<MyPluginOptions> {
protected getPluginName(): string {
return 'my-plugin'
}
protected getDefaultOptions(): Partial<MyPluginOptions> {
return { count: 10 }
}
protected validateOptions(): void {
this.validator.field('message').required().string().field('count').number().validate()
}
// 3. 注册 Vite 钩子
protected addPluginHooks(plugin: Plugin): void {
plugin.buildStart = () => {
this.logger.info(this.options.message)
}
}
// 4. 可选:清理资源
protected destroy(): void {
super.destroy()
// 清理 watcher、定时器等
}
}
// 导出工厂函数
export const myPlugin = createPluginFactory(MyPlugin)
5.4 运行时交互:pluginInstance
const plugin = injectLoading({
defaultVisible: true,
autoBind: 'all'
})
// 访问插件内部状态
console.log(plugin.pluginInstance.options) // 完整配置
console.log(plugin.pluginInstance.logger) // 日志代理
六、常见应用场景
场景一:uni-app 项目工程化
export default defineConfig({
plugins: [
buildProgress({ format: 'bar' }),
generateRouter({
pagesJsonPath: 'src/pages.json',
preserveRouteChanges: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
}),
generateVersion({
format: 'datetime',
outputType: 'both',
defineName: '__APP_VERSION__'
}),
injectLoading({
defaultVisible: true,
autoHideOn: 'DOMContentLoaded',
autoBind: 'all',
requestFilter: {
excludeUrlPrefixes: ['/static/']
}
})
]
})
场景二:Web 应用白屏优化
export default defineConfig({
plugins: [
buildProgress(),
injectLoading({
defaultVisible: true, // 白屏 Loading
autoHideOn: 'load', // 页面完全加载后隐藏
spinnerType: 'pulse',
style: {
overlayColor: 'rgba(255, 255, 255, 0.9)',
backdropBlur: true,
backdropBlurAmount: 8,
pointerEvents: true // 阻止白屏期间的交互
},
delayShow: { enabled: false }, // 白屏模式无需延迟
minDisplayTime: { enabled: true, duration: 500 }
}),
injectIco({
base: '/',
icons: [
{ rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }
]
})
]
})
场景三:CI/CD 构建监控
export default defineConfig({
plugins: [
buildProgress({
format: 'minimal',
clearOnComplete: false, // 保留最终进度信息
showModuleName: false
}),
generateVersion({
format: 'custom',
customFormat: '{YYYY}.{MM}.{DD}.{hash}',
hashLength: 6,
outputType: 'both',
extra: {
environment: process.env.NODE_ENV,
commitHash: process.env.CI_COMMIT_SHA
}
}),
copyFile({
sourceDir: 'public',
targetDir: 'dist',
incremental: true,
errorStrategy: 'log' // CI 中不中断构建
})
]
})
场景四:运行时动态控制交互
// vite.config.ts
export default defineConfig({
plugins: [
injectLoading({
autoBind: 'all',
style: {
pointerEvents: true // 默认阻止交互
},
debounceHide: { enabled: true, duration: 100 }
})
]
})
// 应用代码中
const loading = window.__LOADING_MANAGER__
// 显示 loading 并阻止交互
loading.show('提交中...')
// 特定场景允许交互穿透(如仅展示加载状态,不阻止操作)
loading.disablePointerEvents()
// 恢复交互阻止
loading.enablePointerEvents()
// 切换交互状态
loading.togglePointerEvents()
// 检查交互是否被阻止
if (loading.isPointerEventsEnabled()) {
console.log('交互已被阻止')
}
七、注意事项与最佳实践
通用注意事项
- peerDependencies 要求 — 本库要求 Vite 版本
^5.0.0 || ^6.0.0 || ^7.0.0,请确保项目 Vite 版本兼容 - errorStrategy 选择 — 生产环境建议使用默认的
'throw'策略以及时发现问题;CI/CD 等容错场景可使用'log' - verbose 控制 — 生产构建可设置
verbose: false减少日志输出
buildProgress 注意事项
- 非 TTY 环境降级 — CI/CD 中进度条自动降级为日志输出,无需额外配置
- 模块排除 —
node_modules和.virtual模块自动排除,不计入进度统计
copyFile 注意事项
- 增量复制前提 — 增量复制依赖文件的
mtimeMs和size,某些构建工具可能会重置文件时间戳,导致增量判断失效 - 并发控制 — 默认并发限制为 10,大量小文件场景可适当提高
generateRouter 注意事项
- pages.json 格式 — 支持 JSON 注释(自动调用
stripJsonComments),但需确保 JSON 结构合法 - preserveRouteChanges — 开启后,删除 pages.json 中的页面不会自动删除对应路由配置中的用户修改字段,需手动清理
- customNameGenerator — 使用
nameStrategy: 'custom'时必须提供customNameGenerator函数
generateVersion 注意事项
- define 模式 — 使用
outputType: 'define'时,代码中通过__APP_VERSION__访问版本号,TypeScript 项目需自行声明类型 - custom 格式 — 使用
format: 'custom'时必须提供customFormat模板
injectIco 注意事项
- 注入顺序 —
transformIndexHtml设置为order: 'pre',确保在其他 HTML 转换之前执行 - link 与 icons 互斥 — 配置了
link字段时会使用字符串替换方式,不会处理icons数组
injectLoading 注意事项
- 白屏 Loading 条件 —
defaultVisible: true时要求 HTML 模板包含<head>和</head>标签,否则白屏 Loading 无法生效 - 请求拦截副作用 —
autoBind会修改全局fetch和XMLHttpRequest,务必在不需要时调用destroy()恢复 - 回调函数格式 —
callbacks中的回调以函数体字符串形式提供,不是函数引用,因为需要注入到客户端代码中 - SSR 兼容 — 生成的代码包含 SSR 环境检测,在服务端渲染时自动跳过
- XHR 拦截方式 — XHR 使用
addEventListener('loadend')监听请求结束,确保只触发一次_requestEnd - pointerEvents 默认值 — 默认为
true(阻止交互),如需允许交互穿透需显式设为false - customTemplate 安全 — 自定义模板不允许包含
<script>标签,回调逻辑应通过callbacks配置 - globalName 安全 — 全局变量名必须是合法的 JavaScript 标识符,且不能是
__proto__、constructor、prototype等内置属性 - autoHideOn 生效条件 —
autoHideOn仅在defaultVisible为true时生效,defaultVisible为false时该配置将被忽略(会有警告提示)
八、总结
@meng-xi/vite-plugin 的核心价值在于它不仅是一组工具插件,更是一套完整的插件开发体系:
框架层面:
BasePlugin提供了标准化的生命周期管理、三层配置合并、安全执行和钩子自动组合Validator的批量错误收集机制让配置问题一目了然Logger的单例 + 代理架构确保多插件日志输出的一致性和可控性createPluginFactory的三重泛型设计和 OptionsNormalizer 让插件 API 既类型安全又灵活易用
插件层面:
buildProgress的进度只进不退和 TTY 感知体现了对终端体验的细致考虑copyFile的增量判断(mtimeMs + size)和并发复制(Worker Pool)兼顾了正确性和性能generateRouter的用户修改保留(preserveRouteChanges)解决了代码生成的经典痛点generateVersion的 6 种格式和双输出模式覆盖了版本管理的各种需求injectIco的字符串简写和双模式注入降低了使用门槛injectLoading的双阶段注入、请求拦截恢复、防闪烁三重机制、指针事件运行时控制、XSS 防护和 CSS 性能优化体现了生产级的工程素养
这套设计使得 @meng-xi/vite-plugin 既能开箱即用解决常见问题,又能作为基础设施支撑自定义插件的开发,实现了工具库与开发框架的统一。
本文基于 @meng-xi/vite-plugin@0.0.9 版本撰写,如有更新请以最新文档为准。
更多推荐

所有评论(0)