Vite 插件系统实战:plugins 配置、enforce 排序、apply 条件应用与插件生态
本文围绕 Vite 官方文档 Using Plugins 展开,系统讲解如何在 vite.config.js 中接入、排序和条件启用插件,并结合 Vite 源码 印证 plugins 数组的扁平化、falsy 过滤、enforce 分桶与 apply 判定等底层机制。读完你可以独立完成插件选型与配置,理解一个插件最终在 Vite 管线中的执行位置,并判断 Rollup/Rolldown 插件的兼容性边界。
插件是什么:Rollup/Rolldown 插件接口的超集
Vite 的插件体系基于 Rollup 设计良好的插件接口,并在此之上扩展了若干 Vite 特有的选项。这意味着:
- 用户可以复用成熟的 Rollup 插件生态;
- 同时插件还能扩展 dev server 与 SSR 功能(如自定义中间件、HMR 处理逻辑等)这些 Rollup 构建器本身没有的概念。
需要注意版本演进:官方 Plugin API 文档 在当前仓库中表述为 “Vite plugins extends Rolldown's plugin interface with a few extra Vite-specific options”——即当前版本 Vite 的插件接口建立在 Rolldown 之上,而兼容层保证大量既有 Rollup 插件仍可直接工作。无论底层是 Rollup 还是 Rolldown,对使用者来说都是“写一次插件,dev 和 build 通用”。
添加插件:安装、注册与 preset 机制
基本用法
使用一个插件需要两步:把它加入项目的 devDependencies,再写进 vite.config.js 的 plugins 数组。以官方的 @vitejs/plugin-legacy(为生产构建提供旧浏览器兼容支持)为例:
$ npm add -D @vitejs/plugin-legacy
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
})
@vitejs/plugin-legacy 就位于本仓库 packages/plugin-legacy 目录,可作为官方插件包结构的参考实现。
preset:一个元素里装多个插件
plugins 数组也接受 preset——即“一个元素包含多个插件”的形式,适合由多个插件协作实现的复杂功能(典型场景是框架集成)。这个数组会在内部被展平:
// framework-plugin:返回插件数组即构成 preset
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'
export default function framework(config) {
return [frameworkRefresh(config), frameworkDevtools(config)]
}
import { defineConfig } from 'vite'
import framework from 'vite-plugin-framework'
export default defineConfig({
plugins: [framework()],
})
源码印证:配置解析时,plugins 先经过 asyncFlatten 展平(因此 preset 既可以是数组,也可以是返回 Promise 的异步工厂),再逐个过滤,见 config.ts:
const rawPlugins = (await asyncFlatten(config.plugins || [])).filter(
filterPlugin,
)
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawPlugins)
asyncFlatten 的实现位于 utils.ts,并有对应单测覆盖嵌套数组与 Promise 场景,见 utils.spec.ts。
falsy 插件会被忽略
plugins 数组中的 falsy 值(null、undefined、false 等)会被直接忽略,这提供了一种“按条件启停插件”的轻量写法:
plugins: [
useCssPlugin && cssPlugin(),
typescript(),
]
同样可以在源码里验证:filterPlugin 的第一步就是对 falsy 插件返回 false(见 config.ts),后续所有管线组装处也普遍使用 filter(Boolean) 收尾,例如 resolvePlugins。
找到合适的插件:先查内置能力,再查生态
在去 npm 上搜索插件之前,官方文档给出了明确建议:先查 Features Guide。Vite 的目标是为常见 Web 开发模式提供开箱即用的支持——很多在 Rollup 项目里需要插件才能解决的问题,在 Vite 中已经是内置能力(如 CSS 预处理、JSON/import.meta.glob/import.meta.url、Web Worker、HTML 作为入口等)。
确认内置能力不覆盖需求后,按这个顺序找:
- 官方插件:见 Plugins 文档。当前仓库列出的官方插件包括:
- @vitejs/plugin-vue:Vue 3 SFC 支持;
- @vitejs/plugin-vue-jsx:Vue 3 JSX 支持;
- @vitejs/plugin-react:基于 Oxc Transformer 的 React Fast Refresh;
- @vitejs/plugin-react-swc:开发期用 SWC 替代 Oxc,利于大型项目的冷启动与 HMR;
- @vitejs/plugin-rsc:通过 Environment API 支持 React Server Components;
- @vitejs/plugin-legacy:旧浏览器兼容。
- 社区插件:发布到 npm 的社区插件收录在 Vite Plugin Registry(外部站点,此处不展开链接);本仓库文档 docs/plugins/index.md 同时提示可参考 Rolldown 内置插件。
enforce:强制插件在管线中的位置
为了兼容部分 Rollup 插件,有时需要强制指定插件的执行位置。enforce 修饰符有三个取值:
| 取值 | 含义 |
|---|---|
pre |
在 Vite 核心插件之前调用 |
| 缺省(default) | 在 Vite 核心插件之后调用 |
post |
在 Vite 构建插件之后调用 |
官方文档提醒:对 Vite 插件作者而言,enforce 应当是插件的实现细节而非暴露给用户的配置。用法示例——把 @rollup/plugin-image 提到 Vite 核心插件之前:
import image from '@rollup/plugin-image'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...image(),
enforce: 'pre',
},
],
})
源码印证:排序逻辑分两层。
第一层是分桶,sortUserPlugins 把展平后的用户插件按 enforce 分成三组:
plugins.flat().forEach((p) => {
if (p.enforce === 'pre') prePlugins.push(p)
else if (p.enforce === 'post') postPlugins.push(p)
else normalPlugins.push(p)
})
第二层是拼装,resolvePlugins 将用户插件插入到核心插件序列的固定位置:...prePlugins 插在 alias 解析之后、核心插件(CSS、JSON、WASM、Worker、Asset 等)之前;...normalPlugins 插在核心插件之后、buildPlugins.pre 之前;...postPlugins 再插在 buildPlugins.pre(如 dynamicImportVars、importGlob)之后、buildPlugins.post 之前。最终管线还以三个仅服务 dev server 的内部插件收尾:clientInjectionsPlugin、cssAnalysisPlugin、importAnalysisPlugin。
由此可以整理出 Plugin API 文档 给出的完整顺序:
- Alias
enforce: 'pre'用户插件- Vite 核心插件
- 无
enforce的用户插件 - Vite 构建插件
enforce: 'post'用户插件- Vite 构建后插件(minify、manifest、reporting)
需要区分两个概念:enforce 决定插件整体在管线中的位置,而单个 hook 之间还有独立的 hook 级 order 属性('pre' / 'post')。getSortedPluginsByHook 会针对每个 hook 再次按 hook.order 排序,二者互不替代。
apply:只在 serve 或 build 阶段生效
默认情况下,插件在 serve 和 build 两个阶段都会被调用。若某个插件只应在单一阶段生效,使用 apply 属性限定为 'build' 或 'serve':
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
{
...typescript2(),
apply: 'build',
},
],
})
除了字符串,apply 还可以是函数,用于更精细的控制(例如“只在非 SSR 的 build 时应用”):
apply(config, { command }) {
// apply only on build but not for SSR
return command === 'build' && !config.build.ssr
}
源码印证:判定的真实实现是 filterPlugin,逻辑与文档完全一致:
const filterPlugin = (p: Plugin | FalsyPlugin): p is Plugin => {
if (!p) {
return false
} else if (!p.apply) {
return true
} else if (typeof p.apply === 'function') {
return p.apply({ ...config, mode }, configEnv)
} else {
return p.apply === command
}
}
也就是说,字符串形式就是与当前命令(serve / build)做相等比较;函数形式则会把当前(合并后的)配置对象和 config env 传给你自行决策。过滤发生在 config 钩子执行之前,被过滤掉的插件不会收到任何 hook 调用。
构建插件:接口兼容性与实践约束
如果要写自己的插件,完整文档在 Plugins API Guide。这里从“使用插件”的视角梳理几个影响选型的要点:
Rollup/Rolldown 插件的兼容性判据
相当多的 Rollup/Rolldown 插件(如 @rollup/plugin-alias、@rollup/plugin-json)可以直接当 Vite 插件用,因为 Vite dev server 内部创建的 plugin container 以与 Rolldown 相同的方式调用构建钩子。但并非全部兼容,官方给出的判据是:
- 不使用
moduleParsed钩子(Vite dev 期为避免全量 AST 解析而不调用它); - 不依赖 bundler 专属选项(如
transform.inject); - bundle 阶段钩子与 output 阶段钩子之间没有强耦合。
如果一个插件只服务于构建阶段,也可以把它放到 build.rolldownOptions.plugins 下,效果等价于同时声明 enforce: 'post' 与 apply: 'build' 的 Vite 插件。
路径规范化与 include/exclude 过滤
Vite 解析 id 时会把路径规范化为 POSIX 分隔符(Windows 下保留卷号),而 Rollup 默认保留原始分隔符。因此编写插件、对 resolved id 做路径比较时,应先用 vite 模块导出的 normalizePath 规范化路径:
import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'
官方同时建议插件统一使用 @rollup/pluginutils 的 createFilter 做 include/exclude 过滤(该函数由 vite 再导出,Vite 核心内部同样使用这一模式),以保证 include/exclude 与其他工具的语义一致。
Vite 特有钩子的存在边界
判断一个插件属于“通用兼容型”还是“Vite 专属型”,可以直接看它是否使用了 config、configResolved、configureServer、configurePreviewServer、transformIndexHtml、handleHotUpdate、closeServer 等 Vite 特有钩子——这些钩子会被 Rollup 忽略,详见 Plugin API 文档的 Vite Specific Hooks 一节。其中与“使用插件”最相关的两个:
configureServer:最常用的场景是向 dev server 的 connect 中间件栈注入自定义中间件;它返回一个函数时,该函数会在内部中间件安装之后执行,用于把自定义中间件插到内部中间件之后。注意该钩子在生产构建时不会被调用,依赖它的其他钩子需要做好缺省保护。config:插件可以在配置被 resolve 前修改配置(推荐返回局部配置对象做深合并)。注意用户插件在该钩子执行前已完成解析,因此在config钩子里再注入其他插件不会生效。
小结
对照官方文档与本仓库源码,Vite 插件机制的核心要点可以收敛为五条:
- 注册:
npm add -D安装后写入plugins数组;preset(返回数组的插件工厂)由 asyncFlatten 自动展平,falsy 元素被 filterPlugin 忽略; - 选型:先查 Features Guide 的内置能力,再查官方插件列表与社区 Registry;
- 排序:
enforce: 'pre' | 'post'决定插件在核心/构建插件序列中的相对位置,分桶逻辑见 sortUserPlugins,最终管线见 resolvePlugins; - 阶段:
apply: 'build' | 'serve'或函数形式控制插件生效阶段,判定代码同上filterPlugin; - 扩展:需要 Vite 专属能力(中间件、HMR、HTML 转换)时转向 Plugin API 文档 编写插件,并注意 Rollup/Rolldown 插件的兼容性判据与路径规范化要求。
以上结论均可在当前仓库对应文档与源码路径中复核,适用前提为当前仓库所处的 Vite 版本(基于 Rolldown 的 Vite 8 系列)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00