当你在 Vite 项目中反复编写相似的构建逻辑时,是否想过拥有一套标准化的插件开发范式?本文将带你深入剖析
@meng-xi/vite-plugin的设计哲学与实现细节,揭示它如何将"造轮子"变成"搭积木"。
📑 目录
- 一、核心价值与应用场景
- 二、架构全景:模块化设计一览
- 三、内置插件:开箱即用的生产力工具
- 四、插件开发框架:BasePlugin 核心机制
- 五、技术实现亮点与创新点分析
- 六、实战案例:从零开发一个自定义插件
- 七、注意事项与最佳实践
- 八、总结与展望
一、核心价值与应用场景
为什么需要 @meng-xi/vite-plugin?
在现代前端工程化体系中,Vite 以其极快的冷启动和高效的 HMR 成为构建工具的首选。然而,随着项目复杂度的增长,开发者常常面临以下痛点:
| 痛点 | 具体表现 |
|---|---|
| 重复构建逻辑 | 每个项目都在写文件复制、版本号生成、图标注入等相似逻辑 |
| 插件开发门槛高 | Vite 插件 API 灵活但缺乏规范,缺少统一的开发范式 |
| 配置校验缺失 | 插件参数错误只能在运行时暴露,调试成本高 |
| 错误处理不一致 | 不同插件的异常处理方式各异,难以统一管控 |
| 日志输出混乱 | 多插件并行时日志交织,难以追踪问题来源 |
@meng-xi/vite-plugin 的核心价值在于 双重定位:
- 开箱即用的插件集 — 提供
copyFile、generateRouter、generateVersion、injectIco四大内置插件,覆盖常见构建场景 - 完整的插件开发框架 — 导出
BasePlugin、Validator、Logger、createPluginFactory等核心组件,让自定义插件开发变得标准化、类型安全
典型应用场景
┌─────────────────────────────────────────────────────┐
│ @meng-xi/vite-plugin │
├─────────────────────┬───────────────────────────────┤
│ 内置插件(直接使用) │ 开发框架(构建自定义插件) │
├─────────────────────┼───────────────────────────────┤
│ • 构建后静态资源复制 │ • 标准化插件生命周期管理 │
│ • uni-app 路由生成 │ • 流畅的配置验证 API │
│ • 自动版本号管理 │ • 统一的错误处理策略 │
│ • HTML 图标注入 │ • 单例日志管理器 │
│ │ • 工厂模式 + 类型推导 │
└─────────────────────┴───────────────────────────────┘
二、架构全景:模块化设计一览
项目采用清晰的分层架构,各模块职责分明:
@meng-xi/vite-plugin
├── common/ # 通用工具层
│ ├── fs/ # 文件系统操作(复制、读写、并发控制)
│ ├── format.ts # 格式化工具(日期、命名转换、模板解析)
│ ├── object.ts # 对象工具(深度合并)
│ └── validation.ts # 配置验证器(流畅 API)
│
├── factory/ # 插件工厂层
│ ├── plugin/ # BasePlugin 抽象基类
│ └── types.ts # 工厂类型定义
│
├── logger/ # 日志管理层
│ └── Logger 单例 # 全局日志管理 + 插件级日志代理
│
└── plugins/ # 内置插件层
├── copyFile/ # 文件复制插件
├── generateRouter/ # 路由生成插件
├── generateVersion/ # 版本号生成插件
└── injectIco/ # 图标注入插件
子路径导出设计支持按需加载,减少打包体积:
// 完整导入
import { copyFile, BasePlugin, Logger } from '@meng-xi/vite-plugin'
// 按模块导入 — Tree-shaking 友好
import { BasePlugin, createPluginFactory } from '@meng-xi/vite-plugin/factory'
import { Logger } from '@meng-xi/vite-plugin/logger'
import { copyFile, generateRouter } from '@meng-xi/vite-plugin/plugins'
import { Validator, readFileContent, writeFileContent } from '@meng-xi/vite-plugin/common'
三、内置插件:开箱即用的生产力工具
3.1 copyFile — 智能文件复制
场景:构建完成后将静态资源(图片、字体、配置文件等)复制到输出目录。
import { defineConfig } from 'vite'
import { copyFile } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
copyFile({
sourceDir: 'src/assets',
targetDir: 'dist/assets',
overwrite: true,
recursive: true,
incremental: true // 增量复制,只复制变更文件
})
]
})
核心配置项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sourceDir | string | — | 源目录路径(必填) |
targetDir | string | — | 目标目录路径(必填) |
overwrite | boolean | true | 是否覆盖已有文件 |
recursive | boolean | true | 是否递归复制子目录 |
incremental | boolean | true | 是否启用增量复制 |
技术亮点:增量复制通过比较源文件与目标文件的 mtimeMs(修改时间戳)和
size(文件大小)来判断是否需要更新,避免全量复制带来的性能开销。同时,底层使用并发控制(runWithConcurrency)限制同时执行的文件 IO 操作数,在保证性能的同时避免资源耗尽。
3.2 generateRouter — uni-app 路由自动生成
场景:从 uni-app 的 pages.json 自动生成类型安全的路由配置文件,告别手动维护路由表。
import { defineConfig } from 'vite'
import { generateRouter } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
generateRouter({
pagesJsonPath: 'src/pages.json',
outputPath: 'src/router.config.ts',
nameStrategy: 'camelCase',
includeSubPackages: true,
watch: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
]
})
核心配置项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pagesJsonPath | string | 'src/pages.json' | pages.json 文件路径 |
outputPath | string | 'src/router.config.ts' | 输出文件路径 |
outputFormat | 'ts' | 'js' | 'ts' | 输出格式 |
nameStrategy | 'path' | 'camelCase' | 'pascalCase' | 'custom' | 'camelCase' | 路由名称策略 |
customNameGenerator | (path: string) => string | — | 自定义名称生成函数 |
includeSubPackages | boolean | true | 是否包含子包路由 |
watch | boolean | true | 是否监听变化自动重新生成 |
metaMapping | Record<string, string> | — | 页面 style 字段到 meta 的映射 |
exportTypes | boolean | true | 是否导出类型定义 |
preserveRouteChanges | boolean | true | 是否保留用户对 routes 的修改 |
生成的路由配置示例:
// src/router.config.ts(自动生成)
export interface RouteMeta {
title?: string
isTab?: boolean
requireAuth?: boolean
[key: string]: unknown
}
export interface RouteConfig {
path: string
name?: string
meta?: RouteMeta
}
export const routes: RouteConfig[] = [
{ path: '/pages/index/index', name: 'pagesIndexIndex', meta: { title: '首页', isTab: true } },
{ path: '/pages/user/profile', name: 'pagesUserProfile', meta: { title: '个人中心', requireAuth: true } }
]
export default routes
技术亮点:
- 智能合并:开启
preserveRouteChanges后,重新生成时会保留用户对routes数组的手动修改(如自定义meta字段),新字段以pages.json为基础,用户修改覆盖其上 - 文件监听:开发模式下自动监听
pages.json变化,实时重新生成路由配置 - JSON 注释兼容:内置
stripJsonComments函数,支持解析含注释的pages.json
3.3 generateVersion — 多格式版本号生成
场景:在构建过程中自动生成版本号,支持输出到文件、注入全局变量或两者兼有。
import { defineConfig } from 'vite'
import { generateVersion } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
// 语义化版本 + 前缀 + 附加信息
generateVersion({
format: 'semver',
semverBase: '2.0.0',
prefix: 'v',
outputType: 'both',
defineName: '__APP_VERSION__',
extra: {
environment: 'production',
author: 'MengXi Studio'
}
}),
// 自定义格式模板
generateVersion({
format: 'custom',
customFormat: '{YYYY}.{MM}.{DD}-{hash}',
hashLength: 6
})
]
})
支持的版本号格式:
| 格式 | 示例输出 | 说明 |
|---|---|---|
timestamp | 20260518153000 | 精确到秒的时间戳 |
date | 2026.05.18 | 日期格式 |
datetime | 2026.05.18.153000 | 日期时间格式 |
semver | 1.0.0 | 语义化版本 |
hash | a1b2c3d4 | 随机哈希 |
custom | 自定义 | 配合 customFormat 模板 |
自定义模板占位符:
{YYYY} 四位年份 {MM} 两位月份 {DD} 两位日期
{HH} 两位小时 {mm} 两位分钟 {ss} 两位秒数
{timestamp} 时间戳 {hash} 随机哈希
{major} 主版本号 {minor} 次版本号 {patch} 补丁版本号
输出方式:
file— 生成version.json文件到构建输出目录define— 通过 Vite 的define注入全局变量(如__APP_VERSION__)both— 同时使用以上两种方式
在代码中使用注入的版本号:
// 通过 define 注入后,可直接访问
console.log(__APP_VERSION__) // "v2.0.0"
console.log(__APP_VERSION_INFO__) // { version: "v2.0.0", buildTime: "...", ... }
3.4 injectIco — HTML 图标注入
场景:将网站图标(favicon)链接自动注入到 HTML 的 <head> 中,同时支持图标文件复制。
import { defineConfig } from 'vite'
import { injectIco } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
// 简写:只指定 base 路径
injectIco('/assets'),
// 完整配置:多图标 + 文件复制
injectIco({
base: '/assets',
icons: [
{ rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
{ rel: 'icon', href: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }
],
copyOptions: {
sourceDir: 'src/assets/icons',
targetDir: 'dist/assets/icons'
}
})
]
})
核心配置项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base | string | '/' | 图标文件基础路径 |
url | string | — | 图标完整 URL(优先于 base) |
link | string | — | 自定义完整 link 标签 HTML(最高优先) |
icons | Icon[] | — | 自定义图标数组 |
copyOptions | object | — | 图标文件复制配置 |
优先级链:link > icons > url > base + favicon.ico
技术亮点:优先使用 Vite 原生 HtmlTagDescriptor API 注入标签(更可靠、更规范),仅在用户提供自定义 link HTML 时才降级为字符串替换方案。同时通过 OptionsNormalizer 支持字符串简写配置。
四、插件开发框架:BasePlugin 核心机制
BasePlugin 是整个框架的灵魂,它将 Vite 插件开发的通用逻辑抽象为标准化的生命周期和开发范式。
4.1 生命周期管理
┌──────────────────────────────────────────────────────────┐
│ BasePlugin 生命周期 │
├──────────────────────────────────────────────────────────┤
│ │
│ ① constructor │
│ ├── mergeOptions() 合并默认配置与用户配置 │
│ ├── initLogger() 初始化插件日志代理 │
│ ├── new Validator() 初始化配置验证器 │
│ └── validateOptions() 执行配置验证 │
│ │
│ ② configResolved(Vite 钩子) │
│ └── onConfigResolved() 存储 Vite 解析后的配置 │
│ │
│ ③ addPluginHooks() 注册业务钩子(子类实现) │
│ │
│ ④ closeBundle(Vite 钩子) │
│ └── destroy() 销毁资源、注销日志 │
│ │
└──────────────────────────────────────────────────────────┘
子类只需关注两个核心方法:
abstract class BasePlugin<T extends BasePluginOptions> {
/** 必须实现:返回插件名称 */
protected abstract getPluginName(): string
/** 必须实现:注册 Vite 插件钩子 */
protected abstract addPluginHooks(plugin: Plugin): void
}
4.2 钩子自动组合
toPlugin() 方法自动组合 configResolved 和 closeBundle 钩子,确保基类逻辑与子类逻辑有序执行:
configResolved 执行顺序:
BasePlugin.onConfigResolved() → 子类注册的 configResolved 钩子
closeBundle 执行顺序:
子类注册的 closeBundle 钩子 → BasePlugin.destroy()
设计意图:配置解析时基类先初始化(存储 viteConfig),子类再使用;销毁时子类先清理(如关闭 watcher),基类再注销日志。顺序不可颠倒。
4.3 配置验证器 Validator
Validator 提供流畅的链式 API,让配置验证声明式、可读性强:
protected validateOptions(): void {
this.validator
.field('sourceDir').required().string()
.custom(val => val.trim() !== '', 'sourceDir 不能为空字符串')
.field('targetDir').required().string()
.field('overwrite').boolean().default(true)
.field('incremental').boolean().default(true)
.validate()
}
支持的验证规则:
| 方法 | 说明 |
|---|---|
.field(name) | 指定当前验证字段 |
.required() | 标记为必填 |
.string() / .boolean() / .number() / .array() / .object() | 类型验证 |
.default(value) | 设置默认值(仅当值为 undefined/null 时生效) |
.custom(fn, msg) | 自定义验证函数 |
.validate() | 执行验证,失败时抛出包含所有错误的异常 |
4.4 错误处理策略
通过 errorStrategy 配置项统一管控错误行为,配合 safeExecute / safeExecuteSync 实现安全执行:
// 三种策略
interface BasePluginOptions {
errorStrategy?: 'throw' | 'log' | 'ignore'
}
| 策略 | 行为 | 适用场景 |
|---|---|---|
throw(默认) | 记录错误日志并抛出异常,中断构建 | 生产环境,确保问题不被忽略 |
log | 记录错误日志但不抛出,继续执行 | 开发环境,非关键操作 |
ignore | 记录错误日志但不抛出,继续执行 | 可降级的操作 |
使用方式:
// 包裹可能出错的操作
const result = await this.safeExecute(async () => {
return await someRiskyOperation()
}, '执行风险操作')
// result 在 throw 策略下:要么是正常返回值,要么已抛出异常
// result 在 log/ignore 策略下:正常返回值或 undefined
4.5 日志系统 Logger
Logger 采用 单例 + 代理 模式,全局唯一实例管理所有插件的日志输出:
Logger(单例)
├── pluginConfigs: Map<string, boolean> // 各插件日志开关
├── create({ name, enabled }) // 注册插件日志配置
├── unregister(name) // 注销插件日志配置
└── createPluginLogger(name) // 创建插件级日志代理
├── info(message, data?)
├── success(message, data?)
├── warn(message, data?)
└── error(message, data?)
日志输出格式(带颜色和图标):
ℹ️ [@meng-xi/vite-plugin:generate-router] 路由配置文件已生成: src/router.config.ts
✅ [@meng-xi/vite-plugin:copy-file] 复制文件成功:从 src/assets 到 dist/assets
⚠️ [@meng-xi/vite-plugin:generate-version] 版本文件路径未配置
❌ [@meng-xi/vite-plugin:inject-ico] 图标文件不存在: /assets/favicon.ico
设计优势:
- 每个插件通过
verbose选项独立控制日志开关 - 插件销毁时自动注销日志配置,防止内存泄漏
- 统一前缀格式
[@meng-xi/vite-plugin:插件名],便于在多插件并行时快速定位来源
4.6 工厂函数 createPluginFactory
createPluginFactory 将插件类转换为符合 Vite 规范的工厂函数,同时支持 选项标准化器:
// 基本使用
const myPlugin = createPluginFactory(MyPlugin)
// 带标准化器 — 支持字符串简写配置
const injectIco = createPluginFactory<InjectIcoOptions, InjectIcoPlugin, string | InjectIcoOptions>(InjectIcoPlugin, options => (typeof options === 'string' ? { base: options } : options || {}))
// 使用时支持简写
injectIco('/assets') // 字符串 → 自动转换为 { base: '/assets' }
injectIco({ base: '/assets' }) // 对象 → 直接使用
返回值增强:工厂函数返回的 Vite 插件对象附带 pluginInstance 属性,可访问插件内部状态:
import type { PluginWithInstance } from '@meng-xi/vite-plugin/factory'
const routerPlugin = generateRouter({ watch: true }) as PluginWithInstance<GenerateRouterOptions>
console.log(routerPlugin.pluginInstance?.options) // 访问合并后的完整配置
五、技术实现亮点与创新点分析
1. 深度合并策略 — deepMerge
// undefined 值不覆盖已有默认值,null 值会覆盖
deepMerge({ a: 1 }, { a: undefined }) // { a: 1 } ← 保留默认值
deepMerge({ a: 1 }, { a: null }) // { a: null } ← 允许显式置空
deepMerge({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: 1, c: 2 } } ← 嵌套合并
deepMerge({ a: [1, 2] }, { a: [3, 4] }) // { a: [3, 4] } ← 数组覆盖
这一设计确保了 BasePlugin.mergeOptions() 的三层合并(基础默认值 → 插件默认值 → 用户配置)行为可预测:用户未提供的字段使用默认值,显式传入 undefined 不会意外覆盖默认值。
2. 并发控制的文件复制 — runWithConcurrency
async function runWithConcurrency<T, R>(items: T[], handler: (item: T) => Promise<R>, concurrency: number): Promise<R[]>
采用 Worker Pool 模式,通过共享索引实现并发限制。相比 Promise.all(无限制并发)和串行执行(效率低),这种方式在 IO 密集场景下实现了性能与资源占用的平衡。
3. 增量复制 — 基于文件元数据的智能判断
async function shouldUpdateFile(sourceFile: string, targetFile: string): Promise<boolean> {
const [sourceStats, targetStats] = await Promise.all([fs.promises.stat(sourceFile), fs.promises.stat(targetFile)])
return sourceStats.mtimeMs > targetStats.mtimeMs || sourceStats.size !== targetStats.size
}
同时比较修改时间和文件大小,避免因时间戳精度问题导致的误判。在大型项目中,增量复制可将重复构建的文件复制耗时从秒级降至毫秒级。
4. 路由配置智能合并 — preserveRouteChanges
generateRouter 插件在重新生成路由时,会解析已有的 router.config.ts,提取用户手动修改的字段,并与新生成的配置合并:
新生成的配置(来自 pages.json) + 用户手动修改 → 合并结果
─────────────────────────────────────────────────────────────
{ path: '/home', meta: { title: '首页' } }
+
{ path: '/home', meta: { title: '首页', custom: true } }
↓
{ path: '/home', meta: { title: '首页', custom: true } }
合并策略:path 始终以 pages.json 为准(它是标识符),meta 先以新生成的为基础,再用用户修改覆盖(用户优先)。
5. 双模式图标注入 — HtmlTagDescriptor 优先
injectIco 插件优先使用 Vite 原生 HtmlTagDescriptor API(更规范、更可靠),仅在用户需要注入自定义 HTML 字符串时降级为 transformIndexHtml 字符串替换。这种渐进降级策略兼顾了规范性和灵活性。
6. 类型安全的完整闭环
从 BasePluginOptions 到具体插件选项(如 CopyFileOptions extends BasePluginOptions),从 PluginFactory<T, R> 到 PluginWithInstance<T>,整个类型链确保了:
- 工厂函数的输入类型与插件类的配置类型一致
pluginInstance的类型与实际插件实例匹配Validator<T>的字段名与配置对象的键名对应
六、实战案例:从零开发一个自定义插件
下面我们开发一个 构建时间统计插件,在构建开始和结束时记录耗时:
import { BasePlugin, createPluginFactory } from '@meng-xi/vite-plugin'
import type { BasePluginOptions, PluginWithInstance } from '@meng-xi/vite-plugin/factory'
import type { Plugin } from 'vite'
// 1. 定义配置接口
interface BuildTimerOptions extends BasePluginOptions {
/** 是否输出详细时间节点 */
detailed?: boolean
/** 自定义日志标签 */
label?: string
}
// 2. 继承 BasePlugin 实现插件类
class BuildTimerPlugin extends BasePlugin<BuildTimerOptions> {
private startTime: number = 0
protected getDefaultOptions(): Partial<BuildTimerOptions> {
return {
detailed: false,
label: 'Build Timer'
}
}
protected validateOptions(): void {
this.validator.field('detailed').boolean().field('label').string().validate()
}
protected getPluginName(): string {
return 'build-timer'
}
protected addPluginHooks(plugin: Plugin): void {
plugin.buildStart = {
order: 'pre', // 确保最先执行
handler: () => {
this.startTime = Date.now()
this.logger.info(`${this.options.label}: 构建开始`)
}
}
plugin.closeBundle = {
order: 'post', // 确保最后执行
handler: () => {
const elapsed = Date.now() - this.startTime
this.logger.success(`${this.options.label}: 构建完成,耗时 ${elapsed}ms`)
if (this.options.detailed) {
this.logger.info(` 开始时间: ${new Date(this.startTime).toLocaleString()}`)
this.logger.info(` 结束时间: ${new Date().toLocaleString()}`)
}
}
}
}
}
// 3. 使用 createPluginFactory 导出工厂函数
export const buildTimer = createPluginFactory(BuildTimerPlugin)
使用方式:
import { defineConfig } from 'vite'
import { buildTimer } from './plugins/build-timer'
export default defineConfig({
plugins: [buildTimer({ detailed: true, label: 'My App' })]
})
控制台输出:
ℹ️ [@meng-xi/vite-plugin:build-timer] My App: 构建开始
✅ [@meng-xi/vite-plugin:build-timer] My App: 构建完成,耗时 3247ms
ℹ️ [@meng-xi/vite-plugin:build-timer] 开始时间: 2026/5/18 15:30:00
ℹ️ [@meng-xi/vite-plugin:build-timer] 结束时间: 2026/5/18 15:30:03
七、注意事项与最佳实践
⚠️ 常见陷阱
1. 不要忘记调用 super.destroy()
// ❌ 错误:资源未清理,日志未注销
protected destroy(): void {
this.stopWatching()
}
// ✅ 正确:先清理子类资源,再调用基类销毁
protected destroy(): void {
this.stopWatching()
super.destroy()
}
2. closeBundle 钩子不要手动注册
BasePlugin.toPlugin() 已自动组合 closeBundle 钩子,子类只需重写 destroy() 方法。手动注册 closeBundle 会导致基类销毁逻辑被覆盖。
3. configResolved 钩子不要手动注册
同理,toPlugin() 已自动组合 configResolved。如需在配置解析后执行逻辑,重写 onConfigResolved() 方法:
// ❌ 错误
protected addPluginHooks(plugin: Plugin): void {
plugin.configResolved = (config) => { /* ... */ }
}
// ✅ 正确
protected onConfigResolved(config: ResolvedConfig): void {
super.onConfigResolved(config) // 存储配置
// 自定义逻辑...
}
4. 异步操作务必使用 safeExecute
// ❌ 错误:异常未被捕获,可能导致构建进程崩溃
plugin.writeBundle = async () => {
await this.copyFiles()
}
// ✅ 正确:异常按 errorStrategy 处理
plugin.writeBundle = async () => {
await this.safeExecute(() => this.copyFiles(), '复制文件')
}
💡 最佳实践
1. 合理设置 errorStrategy
// 生产构建:严格模式,任何错误立即中断
copyFile({ sourceDir: '...', targetDir: '...', errorStrategy: 'throw' })
// 开发辅助:宽松模式,非关键操作失败不影响主流程
generateVersion({ format: 'hash', errorStrategy: 'log' })
2. 利用 verbose 控制日志噪音
// 生产环境关闭非关键插件的日志
generateVersion({ verbose: false })
3. 使用 pluginInstance 进行运行时交互
const versionPlugin = generateVersion({ outputType: 'define' }) as PluginWithInstance<GenerateVersionOptions>
// 在其他构建脚本中访问版本号
console.log(versionPlugin.pluginInstance?.options)
4. 善用 OptionsNormalizer 提供简写 API
// 为你的插件支持字符串简写
export const myPlugin = createPluginFactory(MyPlugin, opt => (typeof opt === 'string' ? { path: opt } : opt || {}))
// 用户可以更简洁地使用
myPlugin('./custom-path')
myPlugin({ path: './custom-path', verbose: true })
5. 增量操作优先
对于文件操作类插件,始终默认开启增量模式(incremental: true),在大型项目中效果显著。
八、总结与展望
@meng-xi/vite-plugin 的设计哲学可以概括为三个关键词:
| 关键词 | 体现 |
|---|---|
| 标准化 | BasePlugin 统一生命周期、Validator 统一配置校验、Logger 统一日志格式 |
| 类型安全 | 完整的 TypeScript 泛型链,从配置到实例到工厂函数全链路类型推导 |
| 开发者友好 | 流畅 API、智能默认值、简写支持、渐进降级、详尽的错误信息 |
如果你正在寻找一套 Vite 插件开发的最佳实践,或者需要开箱即用的构建工具集,@meng-xi/vite-plugin 值得一试。
# 安装
pnpm add @meng-xi/vite-plugin -D
完整文档:mengxi-studio.github.io/vite-plugin…
本文基于 @meng-xi/vite-plugin@0.0.6 版本撰写,如有更新请以最新文档为准。