第九章:新时代的引擎轰鸣——Vite 全面精通

第九章:新时代的引擎轰鸣——Vite 全面精通

引言: 在第八章,我们站在历史的交汇点,理解了 Webpack 的伟大思想与时代局限。现在,让我们正式踏入由 Vite 开创的新纪元。Vite 并非简单地对 Webpack 进行修补,而是利用浏览器和编译工具链的代际优势,从根本上颠覆了“打包”这一核心环节。本章,我们将从其革命性的工作原理出发,深入配置实战,探索其强大的插件生态,并最终触及 SSR、库模式等高级应用场景,全面掌握这个现代前端开发的首选引擎。


9.1. 范式转移:从“先打包,再启动”到“即时服务”

9.1.1. 亲身体验:“魔法”是如何发生的

在深入理论之前,让我们先用一分钟时间,亲手见证 Vite 的“魔法”。

动手实践:请打开您的终端,执行以下命令来创建一个全新的 Vite 项目。

1
2
# 我们使用 pnpm,速度更快
pnpm create vite

在交互式问答中,你可以为项目命名(如 vite-magic-show),并选择 Vanilla -> TypeScript 模板。这是一个不依赖任何框架的、纯净的 TypeScript 项目。

1
2
3
4
5
6
# 进入项目目录
cd vite-magic-show
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev

请注意这个时间:ready in 312 ms。Vite 几乎是瞬间就启动了服务。现在,用浏览器打开 http://localhost:5173/

接下来,找到并打开 src/main.ts 文件,随意修改其中的内容,例如:

1
2
3
4
// src/main.ts
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<h1>Hello Prorise! Vite is amazing!</h1>
`

当您按下 Ctrl + S 保存的瞬间,浏览器中的页面几乎是 同步更新 的,没有任何可感知的延迟。

这就是 Vite 带来的第一个震撼:告别漫长的启动和更新等待,沉浸在 极致的心流开发体验 之中。

9.1.2. 根本原因剖析:两种模式的对决

这“魔法”的背后,是 Vite 和 Webpack 在底层工作模式上的 范式转移

  • Webpack 的“预打包”模式:
    回顾第八章,Webpack 在启动开发服务器时,必须从入口文件开始,遍历整个项目的依赖,构建一个完整的依赖图,然后将所有模块打包(Bundle)进内存。项目越大,这张图越复杂,启动前的“打包”耗时就越长,从而导致了漫长的“咖啡时间”。

  • Vite 的“按需服务”模式:
    Vite 则彻底颠覆了这个流程。它利用了现代浏览器原生支持的 ES Module (ESM) 语法。

    1. 启动时:Vite 仅启动一个轻量级的 Web 服务器,几乎是零开销。它 不需要进行任何打包操作
    2. 浏览器请求:当浏览器加载 index.html 并解析到 <script type="module" src="/src/main.ts"></script> 时,它会主动向 Vite 服务器发起一个对 /src/main.ts 的 HTTP 请求。
    3. Vite 响应:Vite 服务器接收到请求,对 /src/main.ts 这个 单个文件 进行即时编译(例如将 TypeScript 转换为 JavaScript),然后直接返回给浏览器。
    4. 循环:如果 main.ts 文件内部又有 import 了其他模块,浏览器会继续发起新的 HTTP 请求,Vite 也继续按需编译并返回。

在这个模式下,浏览器自身成为了“打包器”,它负责根据 import 语句来请求和组织模块。Vite 则退居为一个高效的、按需服务的“文件翻译官”。因此,项目的规模与启动时间彻底解耦,实现了恒定的、毫秒级的启动速度。

9.1.3. 热更新的“质变”

这种模式同样让 热模块替换 (HMR) 发生了质变。

  • Webpack HMR: 当你修改一个模块时,Webpack 需要找出这个模块所属的 chunk(代码块),并重新计算和构建这整个 chunk,即使其中很多模块并未改变。
  • Vite HMR: 当你修改一个模块(如 utils.ts)时,Vite 能利用 ESM 清晰的模块边界,精准地知道只有这一个模块失效了。它通过 WebSocket 通知浏览器:“嘿,utils.ts 更新了,请重新请求它。” 浏览器接收到指令后,只会重新请求这一个文件,并凭借其原生的 ESM 处理能力,无缝地替换掉旧模块,而无需刷新整个页面或重新加载大量不相关的代码。

核心总结:Vite 的“快”,根植于它对 原生 ES Module 的极致利用。它将 Webpack 在启动时必须完成的繁重打包工作,巧妙地 分解、延迟 到了开发过程中的每一次浏览器实际请求中,从而实现了开发体验的代际飞跃。


9.2. Vite 的“双核引擎”:深入理解开发与生产模式的差异

Vite 的设计哲学中,最核心的一点就是 区分开发与生产。它认为,开发阶段我们最追求的是 极致的响应速度和调试体验,而生产阶段我们最追求的是 最优的加载性能和兼容性。为了同时达到这两个目标,Vite 创造性地采用了一套“双核引擎”架构。

9.2.1. 开发环境的“王牌”:原生 ESM + esbuild

我们在上一节体验的“即时服务”魔法,其背后的两大功臣,就是 浏览器原生 ESM 支持esbuild

在这个工作流中,esbuild 扮演着至关重要的角色。

  • esbuild 是什么?
    它是一个使用 Go 语言编写的、速度极快的 JavaScript Bundler / Transpiler / Minifier。由于 Go 语言是编译型语言,并且能充分利用多核 CPU 进行并行处理,esbuild 在执行代码转换任务时,比用 JavaScript 编写的传统工具(如 Babel, Terser)快 10-100 倍

  • esbuild 在 Vite 开发环境中的角色:在开发阶段,Vite 并不使用 esbuild 来“打包”,而是主要利用其 闪电般的“转译”能力。当浏览器请求一个 main.ts 文件时,Vite 会在瞬间调用 esbuild 将 TypeScript 代码转换为 JavaScript,然后返回给浏览器。对于 .jsx.tsx 文件也是同理。

这可以看作是 Vite 对 Webpack ts-loader/babel-loader 链式调用的“降维打击”。Webpack 需要通过一系列基于 Node.js 的工具链来处理文件,而 Vite 直接调用了性能不在一个数量级的 esbuild,这是其能够实现“即时编译”的关键。

9.2.2. 生产环境的“守护神”:Rollup

一个核心疑问随之而来:既然开发时可以不打包,为什么执行 pnpm build 进行生产部署时,Vite 还是要打包呢?

这是因为直接将成百上千个未经优化的模块部署到线上,会带来严重的性能问题。Vite 在生产环境构建时,选择了一个久经考验、极其成熟的打包器——Rollup 来完成这项任务。

生产环境仍需打包的四大理由

  1. 网络性能: 即使有 HTTP/2,同时发起成百上千个模块的 HTTP 请求也会在浏览器和服务器端造成巨大的网络开销和拥堵,形成“请求瀑布流”,严重拖慢页面加载速度。打包能将这些请求合并为少数几个。
  2. 更优的 Tree Shaking: Rollup 是最早普及 Tree Shaking 概念的打包器之一,它基于 ESM 的静态分析能实现非常彻底的死代码消除,确保最终产物中不包含任何一行无用代码,这是单纯的浏览器 ESM 加载无法做到的。
  3. 代码分割: Rollup 能进行智能的代码分割,将应用代码拆分成多个按需加载的块(chunks),进一步优化首屏加载性能。
  4. 统一优化与兼容: 打包过程可以统一进行代码压缩、混淆、以及通过插件(如 Babel)处理对旧版浏览器的兼容性问题,确保最终代码在各种环境下的健壮性。

为什么选择 Rollup?
Rollup 由 Vue 的作者尤雨溪(也是 Vite 的作者)长期使用和贡献,它打包输出的 ES Module 格式代码非常“干净”,副作用少,是开发 JS 库的首选。Vite 沿用其成熟的打包能力和庞大的插件生态,来为生产环境的最终产物保驾护航,是一个非常明智的选择。

核心总结: Vite 的“双核”策略,是在开发者体验和用户体验之间做出的完美权衡。

  • 开发时 (esbuild): 牺牲一些最终产物的优化,换取极致的编译速度和即时反馈。
  • 生产时 (Rollup): 花费必要的打包时间,换取用户浏览器中最佳的加载性能和运行表现。

9.3. “秘密武器”:依赖预构建全解析

我们在 9.1 节中建立的核心认知是:Vite 开发服务器通过按需服务源码文件来避免“打包”。然而,这个原则有一个重要的例外:它 不适用node_modules 里的 第三方依赖

Vite 会在首次启动时,花费少量时间对第三方依赖进行“预构建”,这是一个极其聪明的优化策略,旨在解决两大痛点。

9.3.1. 它要解决的两大痛点

1. 模块格式兼容:将 CommonJS (CJS) 统一为 ES Module (ESM)

尽管我们正在全面拥抱 ES Module,但 node_modules 中依然有大量以 CommonJS 格式发布的历史悠久的优秀库(例如 react 的部分依赖)。浏览器本身 完全不认识 CommonJS 的 require()module.exports 语法。如果不进行处理,浏览器在请求这些模块时会直接报错。

解决方案: Vite 使用 esbuild 扫描 node_modules,找到所有 CommonJS 格式的依赖,并将它们强制转换为浏览器友好的 ES Module 格式。

2. 性能瓶颈:避免“请求瀑布流”

一些现代库虽然以 ESM 格式发布,但其内部可能由成百上千个细碎的模块文件组成(一个典型的例子是 lodash-es)。

场景: 假设你在代码中 import { debounce } from 'lodash-es'debounce 函数本身可能又 import 了其他十几个内部工具函数。如果 Vite 对这些内部 import 也进行“按需服务”,浏览器为了加载一个小小的 debounce 功能,就可能需要同时发起数十个甚至上百个 HTTP 请求。这种“请求瀑布流”会严重阻塞浏览器渲染,让开发体验变得卡顿。

解决方案: 预构建再次使用 esbuild,将这些由许多小文件组成的 ESM 依赖 打包 成一个或少数几个大的 ESM 文件。这样,当浏览器请求 lodash-es 时,只需要一次 HTTP 请求就能获取全部所需代码。

9.3.2. 工作原理与缓存揭秘

依赖预构建的流程如下:

  1. 扫描: Vite 启动后,会首先扫描你源码中所有的 importrequire 语句,找出所有指向 node_modules 的“裸模块导入”。
  2. 打包: 它将找到的所有第三方依赖作为入口,调用 esbuild 将它们打包成符合浏览器标准的 ESM 文件。
  3. 缓存: 打包产物被存放在项目根目录的 node_modules/.vite/deps 目录下。每个预构建的依赖都会生成一个 JS 文件,以及一个记录元信息的 _metadata.json 文件。
  4. 重写: Vite 会重写你的 import 路径。例如,你代码中的 import React from 'react' 会被重写为 import React from '/node_modules/.vite/deps/react.js?v=xxxx',从而精确地指向缓存文件。

后续,只要依赖关系没有变化,Vite 每次启动都会直接使用这份缓存,这也是即使是大型项目,Vite 第二次及以后的启动也能“秒开”的核心原因。

9.3.3. 触发时机与强制重建

预构建 不会 在每次启动时都运行。它只在以下情况被触发:

  • 首次启动开发服务器时。
  • package.jsonlock 文件或 vite.config.ts 中的依赖相关配置发生变化后。

在某些特殊情况下,例如你手动修改了 node_modules 里的某个文件进行调试,或者缓存出现了问题,你可能需要 手动强制重建 依赖缓存。这可以通过在启动命令后添加 --force 标志来实现。

1
2
# 强制 Vite 重新进行依赖预构建
pnpm dev --force

你会发现,添加 --force 后,启动时间会回到首次启动时的秒级水平,因为 Vite 重新执行了完整的预构建流程。

核心总结:依赖预构建是 Vite 为解决 node_modules “历史遗留问题”和“性能短板”而祭出的“秘密武器”。它通过一次性的预处理,将混乱、低效的第三方依赖,转化为浏览器能最高效识别的 ESM 格式,从而在不牺牲“按需服务”源码优势的前提下,保证了开发服务器的整体高性能。


9.4. 指挥中心:vite.config.ts 实战精解

Vite 奉行“约定优于配置”的原则,在零配置下就已经非常强大。但对于一个严肃的项目,自定义配置是必不可少的。我们可以在项目根目录下创建一个 vite.config.ts 文件来对 Vite 进行配置。

为什么是 .ts Vite 天然支持 TypeScript 配置文件。强烈推荐使用 .ts 后缀,这样你可以享受到 IDE 提供的类型提示和自动补全,让配置过程更安全、更便捷。

基础配置结构:
文件路径: vite-magic-show/vite.config.ts

1
2
3
4
5
6
import { defineConfig } from 'vite';

// defineConfig 工具函数提供了类型提示
export default defineConfig({
// 在这里添加你的配置
});

9.4.1. 核心配置与环境变量

  • root: 指定项目根目录(index.html 所在的位置),默认为 process.cwd()
  • base: 开发或生产环境下的公共基础路径。例如,如果你的应用部署在 https://prorise.com/app/ 下,那么 base 应设置为 '/app/'
  • mode: 模式,默认为 'development'dev 命令)或 'production'build 命令)。
  • define: 定义全局常量替换。在开发时是全局变量,构建时是静态替换。例如 define: { __APP_VERSION__: JSON.stringify('1.0.0') }
  • envDir: 用于加载 .env 文件的目录,默认为根目录。

实战:配置部署路径和注入构建信息

问题场景: 我们的项目需要部署到服务器的 /my-app/ 子目录下。同时,我们希望在应用中可以展示当前的版本号和构建时间。

解决方案: 使用 base 和 define 选项。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineConfig } from 'vite';

export default defineConfig({
// 1. 设置部署的基础路径
base: '/my-app/',

// 2. 定义全局常量
define: {
// __APP_VERSION__ 是一个自定义的常量,在代码中可以直接使用
__APP_VERSION__: JSON.stringify('1.0.5'),
// 注入构建时的时间戳
__BUILD_DATE__: `"${new Date().toLocaleString()}"`
}
});

在代码中使用:
文件路径: src/main.ts

1
2
3
4
5
6
console.log(`App Version: ${__APP_VERSION__}`);
console.log(`Build Date: ${__BUILD_DATE__}`);

// 运行时会输出:
// App Version: 1.0.5
// Build Date: "8/25/2025, 10:30:00 AM" (示例)

实战:管理环境变量
在真实项目中,API 地址、密钥等信息在开发和生产环境通常是不同的。Vite 通过 .env 文件对此提供了顶级支持。

  1. 在项目根目录创建两个文件:
    文件路径: .env.development

    1
    2
    # 开发环境 API 地址
    VITE_API_URL=http://localhost:8080/api/v1

    文件路径: .env.production

    1
    2
    # 生产环境 API 地址
    VITE_API_URL=https://api.prorise.com/api/v1

    重要约定:为了安全,Vite 只会暴露以 VITE_ 为前缀的变量给客户端代码。

  2. 在你的源码中访问这些变量:
    文件路径: src/api.ts

    1
    2
    3
    4
    5
    const API_URL = import.meta.env.VITE_API_URL;

    export function fetchUserData() {
    return fetch(`${API_URL}/users`);
    }

    当你运行 pnpm dev 时,API_URL 的值是 http://localhost:8080/api/v1;当你运行 pnpm build 时,打包出的代码中 API_URL 的值会自动变为 https://api.prorise.com/api/v1


9.4.2. 开发服务器 (server)

这是用于配置 Vite Dev Server 的选项。

  • host: 指定服务器主机名,默认为 'localhost'
  • port: 指定服务器端口,默认为 5173
  • open: 在服务器启动时自动在浏览器中打开应用。
  • proxy: (极其常用) 配置自定义请求代理,解决开发时的跨域问题。

实战:配置 API 代理解决跨域
问题场景: 你的前端应用运行在 http://localhost:5173,但需要调用的后端 API 运行在 http://localhost:8080。由于浏览器的同源策略,直接请求会触发 CORS 跨域错误。

解决方案: 在 vite.config.ts 中配置 proxy,让 Vite Dev Server 帮你转发请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'vite';

// defineConfig 工具函数提供了类型提示
export default defineConfig({
server: {
proxy: {
// 字符串简写写法
// '/foo': 'http://localhost: 4567',
'/api': {
target: "http://localhost:8080",
changeOrigin: true,
// 因为前端请求地址加了/api 前缀用于区分或触发代理,而后端实际接口路径没有这个前缀,通过 rewrite 去掉前缀才能让后端正确接收请求 。
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});

配置完成后,你在代码中发起的 fetch('/api/users') 请求,实际上会被 Vite 服务器转发到 http://localhost:8080/users,从而完美绕开了浏览器的跨域限制。

9.4.3. 构建配置 (build)

这些选项用于控制生产环境的构建过程 (pnpm build)。

  • outDir: 指定输出路径,默认为 dist
  • assetsDir: 指定生成静态资源的存放路径,默认为 assets
  • sourcemap: 是否生成 source map 文件。true'inline'。开启后便于线上代码调试。
  • minify: 指定使用哪种混淆器,默认为 'esbuild'(速度极快)。设为 false 可禁用混淆。
  • rollupOptions: 传递给 Rollup 的高级选项,用于更精细的构建控制。

实战:优化生产环境的打包输出
问题场景: 我们希望将最终打包的文件输出到 prod-dist 目录,并为了方便管理,将 JS、CSS 和图片等资源分别存放到不同的子文件夹中。同时,为了线上调试,我们希望生成独立的 source map 文件。

解决方案: 配置 build 选项,特别是 rollupOptions。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineConfig } from 'vite';

export default defineConfig({
build: {
// 1. 指定输出目录
outDir: 'prod-dist',
// 2. 生成独立的 source map 文件
sourcemap: true,
// 3. 使用 rollupOptions 自定义输出文件结构
rollupOptions: {
output: {
// [name]:文件名(不含扩展名)
// [hash]:基于文件内容的哈希值
// [ext]:文件扩展名
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
}
}
}
});

执行 pnpm build 后,你的 prod-dist 目录结构会像这样:

1
2
3
4
5
6
7
8
9
10
11
prod-dist/
├── static/
│ ├── js/
│ │ ├── index-a1b2c3d4.js
│ │ └── vendor-e5f6g7h8.js
│ ├── css/
│ │ └── index-i9j0k1l2.css
│ └── img/
│ └── logo-m3n4o5p6.png
├── index.html
└── index-a1b2c3d4.js.map // Source map 文件

9.4.4. 解析 (resolve)

用于配置模块的解析行为。

  • alias: (极其常用) 配置路径别名。

实战:配置 @ 路径别名
问题场景: 在深层嵌套的组件中,你可能需要写这样的导入语句:import { Button } from '../../../components/Button',这种相对路径既不美观也难以维护。

解决方案: 配置 alias,让 @ 符号直接指向 src 目录。

准备工作: 为了在 vite.config.ts 中正确使用 Node.js 的内置模块(如 path)并获得类型提示,你需要安装它的类型定义文件。这是一个开发依赖,因为它只在开发阶段需要。

请在你的项目终端中运行以下命令:

1
2
3
4
5
6
7
8
# 使用 pnpm
pnpm add -D @types/node

# 或使用 npm
npm install -D @types/node

# 或使用 yarn
yarn add -D @types/node

安装完成后,现在可以修改配置文件了。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'vite';
import path from 'path'; // 现在 TypeScript 可以正确识别 'path' 模块了

export default defineConfig({
// ... 其他配置
resolve: {
alias: {
// 设置 '@' 指向 'src' 目录
'@': path.resolve(__dirname, './src'),
}
}
});

配置完成后,你就可以在任何地方使用清爽的绝对路径导入了:

1
2
3
4
5
6
7
// 之前: import { Button } from '../../../components/Button';
// 现在:
import { Button } from '@/components/Button';

// 之前: import { fetchUserData } from '../../api';
// 现在:
import { fetchUserData } from '@/api';

这极大地提升了代码的可读性和项目维护性。

核心总结: vite.config.ts 是你驾驭 Vite 的“方向盘”和“仪表盘”。虽然 Vite 开箱即用,但熟练掌握 server.proxyresolve.alias 这两大配置,并善用 .env 文件管理环境变量,是区分“入门”与“熟练”的关键分水岭,也是构建任何真实项目的基础。


9.5. 一等公民:Vite 中的 CSS 工程化方案

在前端工程化中,对 CSS 的管理始终是一个复杂但至关重要的环节。Webpack 通过一套复杂的 Loader 链条(style-loader, css-loader, postcss-loader, sass-loader 等)来处理样式,而 Vite 则将 CSS 视为项目中的“一等公民”,提供了开箱即用的、功能强大的工程化方案。

9.5.1. 开箱即用的体验

让我们回想一下在 Webpack 中处理 SCSS 文件需要做的配置:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js (示例)
module.exports = {
//...
module: {
rules: [
{
test: /\.scss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
],
},
};

你需要安装三个独立的 loader,并正确配置它们的顺序。

而在 Vite 中,你所需要做的仅仅是:

  1. 安装相应的预处理器:
    1
    pnpm add -D sass
  2. 在你的组件中直接引入 .scss 文件:
    文件路径: src/main.ts
    1
    import './styles/main.scss';

然后,它就直接工作了。 Vite 会自动检测文件扩展名,并调用已安装的相应预处理器进行处理,无需任何额外配置。这种零配置的体验,极大地降低了项目初始化的复杂度。

9.5.2. 实战技巧

Vite 不仅提供了基础支持,还内置了多种能显著提升开发效率的实战技巧。

1. @import 路径别名解析

9.4.4 节中,我们已经配置了 @ 指向 src 目录的路径别名。这个别名同样可以在 CSS 文件中无缝使用。

文件路径: src/components/Header.scss

1
2
3
4
5
6
7
// 无需再使用 ../../ 这样的相对路径
@import '@/styles/variables.scss';

.header {
color: $primary-color;
background-color: $background-color;
}

这使得样式文件的维护性和可移植性大大增强。

2. 将 CSS 作为字符串导入 (?inline)

在某些特殊场景下,你可能需要获取 CSS 的内容作为字符串,而不是直接将其注入到页面中(例如,在 Web Components 的 Shadow DOM 中动态创建 <style> 标签)。Vite 通过 ?inline 后缀提供了这个能力。

文件路径: src/components/MyComponent.ts

1
2
3
4
5
6
7
8
9
import cssString from './MyComponent.scss?inline';

// cssString 的值就是 MyComponent.scss 文件的内容
// => ".my-component { color: red; }"

// 你可以将其用于任何需要 CSS 文本的场景
const styleElement = document.createElement('style');
styleElement.innerHTML = cssString;
shadowRoot.appendChild(styleElement);

3. CSS Modules:实现组件级样式隔离

问题场景: 在大型项目中,不同组件的 CSS 类名很容易发生冲突。你在 Header.css 中定义的 .title 样式,可能会意外地污染到 Footer.css 中的 .title 样式。

解决方案: 使用 CSS Modules。Vite 对此提供了顶级的支持。只需将你的样式文件命名为 *.module.css (或 .module.scss 等)。

文件路径: src/components/Button.module.scss

1
2
3
4
5
6
7
8
9
// 定义一个局部作用域的类
.button {
background-color: blue;
border-radius: 8px;

&:hover {
background-color: darkblue;
}
}

在组件中这样使用它:
文件路径: src/components/Button.ts

1
2
3
4
5
6
7
8
9
10
import styles from './Button.module.scss';

export function createButton(text: string) {
const btn = document.createElement('button');
// styles 对象包含了所有类名,其值是经过哈希处理后的唯一字符串
// 例如 styles.button 的值可能是 "Button_button_a1B2c"
btn.className = styles.button;
btn.innerText = text;
return btn;
}

Vite 会将 .button 这个类名编译成一个唯一的、带有哈希值的字符串(如 Button_button_a1B2c),从而保证这个样式 只对当前组件生效,彻底杜绝了全局样式污染的问题。

4. 全局注入 SCSS/Less 变量

问题场景: 你的项目中有一个 _variables.scss 文件,定义了所有的主题颜色、字体大小等。你希望在 所有.scss 文件中都能直接使用这些变量,而不想在每个文件的开头都手动写一遍 @import '@/styles/_variables.scss';

解决方案: 使用 css.preprocessorOptions 配置。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
// ... 其他配置
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
}
},
css: {
preprocessorOptions: {
scss: {
// 全局注入变量文件
additionalData: `@import "@/styles/_variables.scss";`
}
}
}
});

完成这个配置后,Vite 在编译任何 .scss 文件之前,都会自动在文件内容的 最前面 注入 @import "@/styles/_variables.scss"; 这行代码。现在,你可以在项目的任何一个 .scss 文件中直接使用 $primary-color 等全局变量了,极大地提升了主题管理的便利性。


9.5.3. PostCSS:CSS 的“Babel”

在我们深入 Vite 更多高级 CSS 功能之前,必须先认识一个在幕后默默工作的“大功臣”——PostCSS。当你看到像 Tailwind CSS 或 DaisyUI 这样神奇的 CSS 框架时,驱动它们的核心技术之一就是 PostCSS。

为了理解 PostCSS,最好的方式是将它与我们已经熟悉的 Babel 进行类比:

  • Babel:接收我们写的 JavaScript 代码,将其解析成一种通用的数据结构(AST),然后通过各种插件(如 preset-env)对这个结构进行修改(比如将箭头函数转为普通函数),最后再把这个结构转换回浏览器兼容的 JavaScript 代码。
  • PostCSS:做着完全一样的事情,但 处理的对象是 CSS。它接收我们写的 CSS 代码,将其解析成 AST(抽象语法树),然后通过各种插件(如 autoprefixer)对这个结构进行修改(比如添加浏览器厂商前缀),最后再转换回标准的 CSS 代码。

核心认知:PostCSS 本身 几乎不做任何事。它不是一个像 Sass 或 Less 那样的 CSS 预处理器,它不会给你提供变量、嵌套等新语法。PostCSS 是一个平台,一个让你可以用 JavaScript 插件来转换和增强 CSS 的平台。

PostCSS 的威力体现在其强大的插件生态上,最著名的两个例子是:

  1. autoprefixer: 这是最经典的 PostCSS 插件。它会自动读取 Can I Use 网站的数据,为你的 CSS 规则(如 display: grid)自动添加所需的浏览器厂商前缀(如 -ms-grid),让你从兼容性的琐事中解放出来。
  2. tailwindcss: Tailwind CSS 框架本身就是一个 PostCSS 插件。它会在构建时,扫描你的 html.js/.ts 文件,找到所有原子化类名(如 text-center, p-4),然后动态地生成你所需要的全部 CSS。

在 Vite 中使用 PostCSS
Vite 对 PostCSS 提供了开箱即用的支持。你无需进行任何配置,Vite 就会自动处理项目中的 PostCSS 配置。

实战:为项目添加 autoprefixer

第一步:安装依赖

1
pnpm add -D postcss autoprefixer

第二步:创建 PostCSS 配置文件
在项目根目录创建 postcss.config.js 文件。Vite 会自动识别并加载它。

文件路径: postcss.config.js

1
2
3
4
5
module.exports = {
plugins: {
'autoprefixer': {}, // 直接启用 autoprefixer 插件
},
};

完成了! 就是这么简单。现在,当 Vite 处理你的 CSS 文件时,会自动通过 PostCSS 和 autoprefixer,为你的样式添加必要的浏览器前缀,确保最佳的兼容性。


9.6. Vite 插件开发与应用

引言: 如果说 vite.config.ts 是 Vite 的“指挥中心”,那么插件系统就是它的“灵魂”与无限潜能的源泉。它允许我们深入构建流程的每一个环节,实现代码转换、服务扩展、产物优化等任何可以想象到的功能。本节将系统地讲解 Vite 插件的机制、应用与开发,内容覆盖从使用社区优秀插件到从零编写企业级自定义插件的全过程。


9.6.1. 核心机制:在实战中理解插件如何工作

理论是枯燥的,让我们从一个最简单的“痛点”出发,用实战来开启 Vite 插件的大门。

痛点场景:在开发过程中,我们想知道 Vite 究竟处理了我们项目中的哪些文件,希望能 在每次文件被转换时,在终端打印出它的路径

这是一个 Vite 自身配置项无法满足的需求,却是插件的完美用武之地。

第一步:创建你的第一个插件

  1. 在项目根目录创建一个 plugins 文件夹,用于存放我们自定义的插件。
  2. plugins 文件夹中,创建一个 vite-plugin-inspect.ts 文件。

现在,写入我们插件的核心代码:

文件路径: plugins/vite-plugin-inspect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type { Plugin } from 'vite';

export default function inspectPlugin(): Plugin {
return {
// 插件名称,用于调试和错误信息输出
name: 'vite-plugin-inspect',

// transform 是一个钩子,在每个模块请求被转换时调用
transform(code, id) {
// code 是文件源代码,id 是文件的绝对路径
console.log('Vite 正在处理:', id);

// 我们这里只打印,不修改代码,所以返回 null
return null;
}
};
}

第二步:在 vite.config.ts 中使用它

1
2
3
4
5
6
7
8
import { defineConfig } from 'vite';
import inspectPlugin from './plugins/vite-plugin-inspect'; // 1. 导入我们的插件

export default defineConfig({
plugins: [
inspectPlugin(), // 2. 将插件实例放入 plugins 数组
],
});

第三步:见证实战效果

现在,启动你的开发服务器:

1
pnpm dev

当你刷新浏览器页面时,观察你的终端,会看到 Vite 打印出了它处理的每一个文件!

1
2
3
4
5
Vite 正在处理: D:/web/vite-magic-show/src/main.ts
Vite 正在处理: D:/web/vite-magic-show/src/style.css
Vite 正在处理: D:/web/vite-magic-show/src/counter.ts
Vite 正在处理: D:/web/vite-magic-show/node_modules/.pnpm/vite@7.1.3/node_modules/vite/dist/client/client.mjs
Vite 正在处理: D:/web/vite-magic-show/node_modules/.pnpm/vite@7.1.3/node_modules/vite/dist/client/env.mjs

通过这个简单的实践,我们已经可以开始剖析插件的核心机制了。

插件的本质

正如你所见,一个 Vite 插件就是一个返回特定对象的 JavaScript 函数。这个对象必须有一个 name 属性用于识别,其核心则是一系列被称为 钩子 (Hooks) 的函数(如我们刚才用的 transform)。Vite 在构建流程的特定阶段会自动调用这些钩子。


API 架构:兼容 Rollup 并扩展

我们刚才使用的 transform 钩子,实际上是继承自 Rollup 的。Vite 的插件 API 是 Rollup 插件接口的一个 超集。这意味着,绝大多数 Rollup 插件都能直接在 Vite 中使用,让 Vite 共享了一个庞大而成熟的生态。

同时,Vite 增加了许多 独有的钩子 来控制开发服务器。让我们来为刚才的插件增加一个 Vite 独有钩子 configureServer,它只在 pnpm dev 启动时运行一次。

文件路径: plugins/vite-plugin-inspect.ts (升级版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import type { Plugin, ViteDevServer } from 'vite';

export default function inspectPlugin(): Plugin {
return {
name: 'vite-plugin-inspect',

// configureServer 是 Vite 独有的钩子
configureServer(server: ViteDevServer) {
// server 参数是 Vite 开发服务器的实例
console.log(
'\n ✨ 欢迎使用 Prorise 插件,开发服务器已启动! ✨\n'
);
},

transform(code, id) {
console.log('Vite 正在处理:', id);
return null;
}
};
}

现在再次运行 pnpm dev,你会在启动信息后看到我们自定义的欢迎语。这个 configureServer 钩子就是 Vite 强大开发体验的扩展点之一。


执行顺序与 enforce 属性

当使用多个插件时,它们的执行顺序由 enforce 属性决定,分为三个阶段:

  1. pre: 在 Vite 核心插件 之前 执行。
  2. default: 在 Vite 核心插件 之后 执行。(默认值)
  3. post: 在 Vite 构建插件 之后 执行。

例如,如果你希望你的日志插件总是在所有其他插件转换代码 之前 运行,你可以这样配置:

1
2
3
4
5
6
7
// vite.config.ts
export default defineConfig({
plugins: [
inspectPlugin({ enforce: 'pre' }), // 将插件的执行时机提前
// ... other plugins
],
});

9.6.2. 核心钩子“配方”:深入 Vite 构建的生命周期

在正式开始前,我们先为插件开发 准备好类型环境,这是所有后续实战的基础。

核心上下文:Vite 的配置文件及所有插件都运行在 Node.js 环境 中。因此我们可以使用 fs, path 等内置模块。为了让 TypeScript “认识”它们,我们需要安装其类型定义包。

环境准备:安装 Node.js 类型定义

1
pnpm add -D @types/node

安装后,TypeScript 就不会再对 Node.js 的相关 API 报错了。

本节,我们将通过下表中一系列源于真实开发痛点的“插件配方”,来学习最有代表性的钩子函数。

痛点场景解决方案核心钩子
在代码中需要动态的 构建时信息(版本号、Git 提交哈希等)。编写一个插件,通过 define 选项将信息 注入为全局常量config
前后端分离开发,后端接口未就绪,前端开发被阻塞。编写一个插件,在 Dev Server 中 创建动态 Mock APIconfigureServer
希望直接 import 文件系统中不存在 的、由程序动态生成的模块。编写一个插件,凭空 创造一个“虚拟模块”resolveId, load
希望 import 一种 自定义文件类型(如 .txt),并自动将其转换为所需格式。编写一个插件,转换特定文件内容,例如将纯文本转为 HTML 字符串。transform
构建完成后,需要执行一些 自动化收尾工作,如压缩产物、上传 CDN。编写一个插件,在构建结束后 执行自定义脚本closeBundle

配方一:注入动态构建信息 (config)

Vite 启动时:当 Vite 读取 vite.config.js 文件并初始化配置时,会调用插件的 config 钩子。

痛点: 我们希望在应用中展示版本号、构建时间或最新的 Git 提交信息,以便于测试和追溯问题,但这些信息是动态的,手动维护非常麻烦。

解决方案: 利用 config 钩子,统一收集所有需要的构建时信息,并通过 Vite 的 define 选项,安全、干净地注入到一个全局常量中。

插件实现: plugins/vite-plugin-app-info.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import type { Plugin } from 'vite';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { execSync } from 'child_process'; // 用于执行 shell 命令

// 获取 Git 提交的短哈希值
function getGitCommitHash() {
try {
return execSync('git rev-parse --short HEAD').toString().trim();
} catch (e) {
console.warn('获取 Git Commit 哈希失败:', e);
return 'N/A';
}
}

export default function appInfoPlugin(): Plugin {
return {
name: 'vite-plugin-app-info',
config() {
const CWD = process.cwd();
// 读取 package.json
const pkg = JSON.parse(readFileSync(resolve(CWD, 'package.json'), 'utf-8'));

const appInfo = {
version: pkg.version,
commitHash: getGitCommitHash(),
buildTime: new Date().toISOString(),
};

// 通过 define 选项注入全局变量
return {
define: { __APP_INFO__: JSON.stringify(appInfo) },
};
},
};
}

效果验证:

  1. src/vite-env.d.ts 中声明全局类型。
  2. src/main.ts 中使用 __APP_INFO__ 变量并将其显示在页面上。每次构建,这些信息都会自动更新。

配方二:创建动态 Mock 服务 (configureServer)

Vite 启动开发服务器时(即运行 vitevite dev 命令时)

痛点: 后端接口未就绪或不稳定,导致前端开发被阻塞。

解决方案: 利用 configureServer 钩子向 Vite 开发服务器中注入一个自定义中间件,该中间件 动态加载 mock/ 目录下的所有模块化配置文件,并结合 mockjs 生成动态数据,完美实现 热更新

插件实现: plugins/vite-plugin-api-mock.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import type { Plugin } from 'vite';
import { globSync } from 'glob'; // glob 用于查找匹配特定规则的文件路径
import Mock from 'mockjs';

export default function apiMockPlugin(): Plugin {
return {
name: 'vite-plugin-api-mock',
apply: 'serve', // 仅在开发模式下生效
async configureServer(server) {
const mockFiles = globSync('mock/**/*.ts', { cwd: process.cwd() });

// 使用 Vite 的 ssrLoadModule 加载模块,这是实现热更新的关键!
// 它能加载最新的模块代码(包括 TS),且会自动处理编译。
for (const file of mockFiles) {
const mockModule = await server.ssrLoadModule(`/${file}`);
const mockConfig = mockModule.default;

// 简单演示:在服务器中间件中注册 mock 路由
server.middlewares.use((req, res, next) => {
if (req.url === mockConfig.url) {
const responseData = Mock.mock(mockConfig.response(req));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(responseData));
return;
}
next();
});
}
}
};
}

效果验证:

  1. 安装 mockjsglob
1
pnpm add -D mockjs glob
  1. 创建 mock/user.ts 文件并定义 mock 数据结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 我们导出一个符合特定结构的对象
export default {
// 匹配的请求 URL
url: '/api/user-info',
// 匹配的请求方法,默认为 'get'
method: 'get',
// 模拟接口延迟,单位是毫秒
delay: 200,
// 响应函数,返回的数据可以是静态的,也可以使用 mockjs 语法
response: () => {
return {
code: 200,
message: 'Success',
data: {
id: '@id',
name: '@cname', // 使用 mockjs 的占位符生成中文名
email: '@email',
age: '@integer(18, 60)',
role: '@pick(["admin", "editor", "guest"])', // 从列表中随机选取一个
},
};
}
};
  1. src/main.tsfetch('/api/user-info')。运行 pnpm dev,你会看到请求返回了随机生成的数据,并且修改 mock 文件无需重启服务即可生效。
1
2
3
4
fetch('/api/user-info')
.then(res => res.json())
.then(data => console.log('Dynamic mocked data:', data));


配方三:创建虚拟模块 (resolveId & load)

引言: 在日常开发中,你可能已经见过一些神奇的 import,例如 import routes from 'virtual:routes'。这些在文件系统中找不到的模块,就是“虚拟模块”。虽然从零编写虚拟模块插件不是日常任务,但理解其原理,能帮助你揭开许多现代框架和库(如文件式路由、图标库)背后‘魔法’的秘密。这是一个 进阶概念,但能极大地拓宽你的技术视野。

当 Vite 遇到一个导入语句时,会调用 resolveId 钩子来确定模块的实际路径。

resolveId 成功解析模块路径之后,Vite 会调用 load 来获取模块源码。

  • 痛点: 我正在开发一个博客或文档网站。我需要在首页展示所有文章的列表(包含标题、日期等元信息)。目前,我每写一篇新的 Markdown 文章,都必须 手动 去一个集中的列表文件(如 posts.ts)中添加一条新的记录。这个过程非常繁琐、容易遗漏,且违反了“单一信源”原则。

  • 解决方案: 创建一个名为 virtual:posts-index虚拟模块。当我们的代码 import 这个模块时,插件会 在构建时自动扫描 src/posts 目录下的所有 Markdown 文件,解析每个文件头部的 frontmatter 元信息,并动态生成一个包含所有文章信息的数组导出。从此,文章列表的维护将完全自动化

第一步:准备工作

  1. 安装依赖: 我们需要一个库来解析 Markdown 文件中的 frontmattergray-matter 是一个优秀的选择。

    1
    pnpm add -D gray-matter
  2. 创建内容文件: 在 src 目录下创建一个 posts 文件夹,并放入两篇示例文章。

    文件路径: src/posts/first-post.md

    1
    2
    3
    4
    5
    6
    ---
    title: 我的第一篇文章
    date: '2025-08-31'
    author: Prorise
    ---
    这是文章的正文内容...

    文件路径: src/posts/second-post.md

    1
    2
    3
    4
    5
    6
    ---
    title: Vite 插件太酷了
    date: '2025-09-01'
    author: Prorise
    ---
    插件让一切皆有可能。

第二步:插件实现

文件路径: plugins/vite-plugin-posts-index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import type { Plugin } from 'vite';
// 导入 Node.js 的内置模块
import { readdirSync, readFileSync } from 'fs';
import { resolve } from 'path';
// 导入我们安装的 gray-matter 库
import matter from 'gray-matter';

// 定义虚拟模块的 ID
const VIRTUAL_MODULE_ID = 'virtual:posts-index';
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;

export default function postsIndexPlugin(): Plugin {
return {
name: 'vite-plugin-posts-index',

resolveId(id) {
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
},

load(id) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
const postsDir = resolve(process.cwd(), 'src/posts');

// 1. 读取 posts 目录下的所有文件名
const files = readdirSync(postsDir);

const posts = files
.filter(file => file.endsWith('.md')) // 只处理 Markdown 文件
.map(file => {
// 2. 读取每个文件的内容
const filePath = resolve(postsDir, file);
const fileContent = readFileSync(filePath, 'utf-8');
// 3. 使用 gray-matter 解析 frontmatter
const { data } = matter(fileContent); // 我们只需要元数据 data
return {
...data,
// 顺便生成一个 slug,用于后续可能的路由
slug: file.replace('.md', ''),
};
})
// 4. 按日期降序排序
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

// 5. 将最终的数组作为模块的默认导出
return `export default ${JSON.stringify(posts)};`;
}
}
};
}

第三步:效果验证

  1. 配置插件: 在 vite.config.ts 中引入并使用 postsIndexPlugin

  2. 声明虚拟模块类型: 在 src/vite-env.d.ts 中添加类型声明,享受 TypeScript 的智能提示。
    文件路径: src/vite-env.d.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /// <reference types="vite/client" />

    declare module 'virtual:posts-index' {
    const posts: {
    title: string;
    date: string;
    author: string;
    slug: string;
    }[];
    export default posts;
    }
  3. 在应用中使用: 在 src/main.ts 中导入并渲染这个自动生成的文章列表。
    文件路径: src/main.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import allPosts from 'virtual:posts-index';

    const app = document.querySelector<HTMLDivElement>('#app')!;

    const postsHtml = allPosts.map(post => `
    <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">
    <h2>${post.title}</h2>
    <p>作者: ${post.author} | 发布于: ${post.date}</p>
    </div>
    `).join('');

    app.innerHTML += `<h1>文章列表</h1>${postsHtml}`;

    运行 pnpm dev,刷新页面,你将看到一个根据 src/posts 目录内容自动生成的、按日期排序的文章列表。现在,你可以尝试在 src/posts 目录中新增或删除 .md 文件,然后刷新浏览器,列表会立即自动更新!这完美地解决了我们最初的痛点。


配方四:自定义文件转换 (transform)

  • 痛点: 我希望能在项目中直接 import 一个 .txt 文件,并让它的内容自动转换成一段安全的 HTML 字符串(例如,每行都包在 <p> 标签里)。
  • 解决方案: 利用 transform 钩子,编写一个插件来拦截所有对 .txt 文件的导入请求,并将其内容转换为我们想要的 HTML 字符串格式。

插件实现: plugins/vite-plugin-txt-to-html.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import type { Plugin } from 'vite';

// 一个简单的函数,用于转义 HTML 特殊字符以防 XSS 攻击
function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (match) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;',
'"': '&quot;', "'": '&#39;'
}[match]!));
}

export default function txtToHtmlPlugin(): Plugin {
return {
name: 'vite-plugin-txt-to-html',
transform(code, id) {
if (!id.endsWith('.txt')) return null;

const html = code.split('\n')
.filter(line => line.trim())
.map(line => `<p>${escapeHtml(line)}</p>`)
.join('\n');

return {
code: `export default ${JSON.stringify(html)};`,
map: null
};
}
};
}

效果验证:

  1. 创建 .txt 文件:
    src 目录下创建一个 notes 文件夹,并放入 my-note.txt 文件。
    文件路径: src/notes/my-note.txt

    1
    2
    这是第一行笔记。
    这是第二行笔记,包含一些 <html> 特殊字符。
  2. 在应用中使用:
    src/main.ts 中导入并使用它。
    文件路径: src/main.ts

    1
    2
    3
    4
    import noteHtml from './notes/my-note.txt';

    const app = document.querySelector<HTMLDivElement>('#app')!;
    app.innerHTML += `<h2>My Note:</h2> ${noteHtml}`;

    运行 pnpm dev,刷新页面,你将看到 .txt 文件的内容被安全地渲染成了 HTML 段落。


配方五:构建后自动化任务 (closeBundle)

  • 痛点: 每次构建完成后,我都需要手动把 dist 目录压缩成一个 deploy.zip 包。这个重复性工作完全可以自动化。
  • 解决方案: 利用 closeBundle 钩子,在 Vite 构建流程 完全结束之后 执行自定义的 Node.js 脚本来压缩产物。

插件实现: plugins/vite-plugin-compress-dist.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import type { Plugin, ResolvedConfig } from 'vite';
import { createWriteStream } from 'fs';
import { resolve } from 'path';
import archiver from 'archiver'; // 需要 pnpm add -D archiver

export default function compressDistPlugin(): Plugin {
let config: ResolvedConfig;
return {
name: 'vite-plugin-compress-dist',
apply: 'build', // 仅在构建时生效
configResolved(resolvedConfig) { config = resolvedConfig; },
closeBundle() {
// config.build.outDir 是最终的输出目录, 默认为 'dist'
const distPath = resolve(process.cwd(), config.build.outDir);
const outputPath = resolve(distPath, `../${config.build.outDir}.zip`);

const output = createWriteStream(outputPath);
const archive = archiver('zip', { zlib: { level: 9 } }); // 设置压缩等级

output.on('close', () => {
console.log(`\n✅ ZIP created: ${outputPath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
});

archive.pipe(output);
archive.directory(distPath, false);
archive.finalize();
}
};
}

效果验证:

  1. 安装依赖:

    1
    pnpm add -D archiver
  2. 配置插件:
    compressDistPlugin 添加到 vite.config.tsplugins 数组中。

  3. 执行构建:

    1
    pnpm build

    构建流程的最后,你会在终端看到 ✅ ZIP created... 的提示,并且项目根目录下会自动生成一个 dist.zip 压缩文件。


9.6.3. 社区优秀插件集成与实战

Vite 的真正威力,有很大一部分来自于其活跃、创新的插件生态。学会发现、评估和集成社区插件,是衡量一个工程师能力的重要标准。本节,我们将以“卡片”的形式,为您逐一介绍一些最流行、最能提升开发体验的优秀插件。


1. unplugin-auto-import 自动按需导入

一句话解决什么问题:自动按需导入 API,让你告别冗长的 import { ref, computed } from 'vue'import { useState, useEffect } from 'react'

痛点: 在日常开发中,我们需要反复从框架或库中导入相同的 API,这些重复的 import 语句不仅占用了代码空间,也增加了心智负担。
解决方案: unplugin-auto-import 会扫描你的代码,自动识别使用到的 API,并在需要时“隐式地”为你注入导入语句。

实战配置与效果

  1. 安装依赖:
    1
    pnpm add -D unplugin-auto-import
  2. 配置 vite.config.ts:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import AutoImport from 'unplugin-auto-import/vite';

    export default defineConfig({
    plugins: [
    AutoImport({
    // 定义需要自动导入的库
    imports: ['vue', 'react', 'vue-router'],
    // 指定生成 d.ts 文件的位置,用于 TypeScript 类型提示
    dts: 'src/auto-imports.d.ts',
    }),
    ],
    });
  3. 效果展示 (以 Vue 为例):
    使用前:
    1
    2
    3
    4
    5
    <script setup>
    import { ref, computed } from 'vue';
    const count = ref(0);
    const double = computed(() => count.value * 2);
    </script>
    使用后:
    1
    2
    3
    4
    5
    <script setup>
    // ref 和 computed 都是自动导入的,可以直接使用,代码更纯粹
    const count = ref(0);
    const double = computed(() => count.value * 2);
    </script>

2. vite-plugin-svg-icons 自动化雪碧图

一句话解决什么问题:自动化 SVG 雪碧图方案,将多个 SVG 图标打包成一个大的符号集合,通过 <use> 标签按需引用,提升性能与管理效率。

痛点: 管理几十上百个零散的 SVG 图标文件非常繁琐,且每个图标作为独立文件请求也会影响性能。
解决方案: 该插件会创建一个 virtual:svg-icons-register 虚拟模块,将指定目录下的所有 SVG 图标打包成一个 SVG 雪碧图注入到 body 中。

实战配置与效果

  1. 安装依赖: pnpm add -D vite-plugin-svg-icons
  2. 配置 vite.config.ts:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
    import path from 'path';

    export default defineConfig({
    plugins: [
    createSvgIconsPlugin({
    iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
    symbolId: 'icon-[dir]-[name]',
    }),
    ],
    });
  3. src/main.ts 中引入注册脚本:
    1
    import 'virtual:svg-icons-register';
  4. 效果展示: 假设你在 src/assets/icons 目录下有一个 user.svg,现在可以在任何地方通过 <svg> 标签方便地使用它。
    1
    2
    3
    <svg aria-hidden="true" width="20" height="20">
    <use href="#icon-user" fill="currentColor" />
    </svg>

3. rollup-plugin-visualizer 可视化分析

一句话解决什么问题:可视化分析打包产物,让你清楚地知道是什么占用了你的包体积,是性能优化的“侦察兵”。

痛点: 项目构建后的 bundle 文件体积过大,但不知道具体是哪个依赖或模块导致的。
解决方案: visualizer 插件会在构建结束后,生成一个交互式的 HTML 文件,用矩形树图清晰地展示产物中所有模块的体积占比。

实战配置与效果

  1. 安装依赖: pnpm add -D rollup-plugin-visualizer
  2. 配置 vite.config.ts:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { visualizer } from 'rollup-plugin-visualizer';

    export default defineConfig({
    plugins: [
    visualizer({
    open: true, // 在默认浏览器中自动打开报告
    gzipSize: true,
    brotliSize: true,
    }),
    ],
    });
  3. 效果展示:
    执行 pnpm build 后,Vite 会自动在浏览器中打开一个名为 stats.html 的分析报告。通过这个图表,你可以轻松定位到需要进行代码分割或替换的“大体积”模块。

4. vite-plugin-pages 自动生成路由

一句话解决什么问题:基于文件目录结构,自动生成 vue-router 的路由配置,实现“文件即路由”。

痛点: 在 Vue Router 中,每新增一个页面,都需要手动在 router/index.tsimport 组件并添加一条新的路由配置,非常繁琐且容易出错。
解决方案: 该插件会扫描 src/pages 目录的结构,自动生成对应的路由配置数组,并通过虚拟模块提供给应用使用。

实战配置与效果

  1. 安装依赖: pnpm add -D vite-plugin-pages
  2. 文件目录结构约定:
    1
    2
    3
    4
    5
    6
    src/pages/
    ├── index.vue
    ├── about.vue
    └── user/
    ├── index.vue
    └── [id].vue
  3. 效果展示:
    该插件会自动生成等价于下面这样的路由配置,我们无需手写任何路由代码:
    1
    2
    3
    4
    5
    6
    7
    // 这是插件在内存中生成的路由配置(概念)
    [
    { path: '/', component: '/src/pages/index.vue' },
    { path: '/about', component: '/src/pages/about.vue' },
    { path: '/user', component: '/src/pages/user/index.vue' },
    { path: '/user/:id', component: '/src/pages/user/[id].vue' }, // 动态路由
    ]
    这个插件完美地诠释了“约定优于配置”,将繁琐的路由配置工作完全自动化。

5. unplugin-vue-components 按需导入-vue 加强版

一句话解决什么问题:自动按需导入并注册 Vue 组件,让你在 <template> 中直接使用组件,无需 importcomponents 选项。

痛点: 与 unplugin-auto-import 类似,每个组件都需要手动 import 并(在选项式 API 中)注册。当页面中组件繁多时,<script> 部分会变得非常臃肿,且都是模板化的引入代码。
解决方案: 该插件会扫描你的模板文件,自动发现使用的组件标签,并按需从指定目录或 UI 库中导入对应的组件。它是 unplugin-auto-import 的完美搭档。

实战配置与效果

  1. 安装依赖:
    1
    pnpm add -D unplugin-vue-components
  2. 配置 vite.config.ts:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import Components from 'unplugin-vue-components/vite';
    // 如果你使用像 Element Plus 这样的 UI 库,可以引入它的解析器
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

    export default defineConfig({
    plugins: [
    Components({
    // 指定组件位置,默认是 'src/components'
    dirs: ['src/components'],
    // 指定生成 d.ts 文件的位置
    dts: 'src/components.d.ts',
    // 配置需要自动导入的 UI 库解析器
    resolvers: [ElementPlusResolver()],
    }),
    ],
    });
  3. 效果展示:
    使用前:
    1
    2
    3
    4
    5
    6
    7
    8
    <template>
    <MyHeader />
    <el-button>Click Me</el-button>
    </template>
    <script setup>
    import MyHeader from './components/MyHeader.vue';
    import { ElButton } from 'element-plus';
    </script>
    使用后:
    1
    2
    3
    4
    5
    6
    7
    8
    <template>
    <!-- MyHeader 和 ElButton 都可以直接使用,无需任何 import -->
    <MyHeader />
    <el-button>Click Me</el-button>
    </template>
    <script setup>
    // <script> 区域变得无比干净!
    </script>

6. vite-plugin-compression 自动压缩

一句话解决什么问题:在构建时自动为你生成 .gz.br 压缩文件,极大减小生产环境的资源体积,加速网站加载。

痛点: 现代 Web 应用构建后的 JavaScript 和 CSS 文件可能很大。虽然服务器(如 Nginx)可以动态压缩它们,但这会消耗 CPU 资源。更高效的方式是直接提供预先压缩好的文件。
解决方案: 该插件在 pnpm build 完成后,会自动使用 Gzip 或 Brotli 算法对大文件进行压缩,生成对应的 .gz.br 文件。你只需配置服务器优先使用这些压缩文件即可。

实战配置与效果

  1. 安装依赖: pnpm add -D vite-plugin-compression
  2. 配置 vite.config.ts:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import viteCompression from 'vite-plugin-compression';

    export default defineConfig({
    plugins: [
    viteCompression({
    verbose: true, // 是否在控制台输出压缩结果
    disable: false, // 是否禁用
    threshold: 10240, // 文件大小大于 10kb 才进行压缩
    algorithm: 'gzip', // 压缩算法
    ext: '.gz', // 文件后缀
    }),
    ],
    });
  3. 效果展示:
    执行 pnpm build 后,查看 dist 目录。你会发现除了 *.js, *.css 等文件外,还多出了对应的 *.js.gz, *.css.gz 文件。将这些文件部署到服务器并配置 Nginx(例如开启 gzip_static on;),用户浏览器将直接下载压缩后的文件,首屏加载速度显著提升。

**7. vite-plugin-mkcert https 环境模拟 **

一句话解决什么问题:一条命令搞定本地 HTTPS 开发环境,让你的 localhost 拥有一个浏览器信任的绿色小锁。

痛点: 本地开发时,http://localhost 无法满足一些现代 Web API 的要求(如 Service Workers、Web Crypto API、安全 Cookie),这些 API 强制要求在安全上下文(HTTPS)中运行。手动创建和信任自签名证书流程极其繁琐,且浏览器总是会报不安全警告。
解决方案: 该插件集成了 mkcert 工具,可以自动在你的本地环境中生成一个被信任的证书颁发机构(CA),并为你的项目签发一个有效的 HTTPS 证书。

实战配置与效果

  1. 安装依赖: pnpm add -D vite-plugin-mkcert

  2. 配置 vite.config.ts:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import mkcert from 'vite-plugin-mkcert';

    export default defineConfig({
    // 必须开启 server.https 才能生效
    server: {
    https: true
    },
    plugins: [
    mkcert() // 插件的调用非常简单
    ],
    });

    注意: 首次运行时,插件可能会提示需要管理员(sudo)权限来安装本地 CA,请按提示操作。此操作仅需一次。

  3. 效果展示:
    再次运行 pnpm dev,Vite 将启动一个 https://localhost:5173 的服务。在浏览器中打开它,你会看到地址栏前面出现了一个安全锁标志,再也不会有任何安全警告,所有需要 HTTPS 的 API 都可以正常调试了。


9.7. 拓展疆界:Vite 的高级架构模式与场景

引言: 在掌握了 Vite 对单页应用(SPA)的极致优化后,我们将目光投向更广阔的领域。本节将探讨 Vite 如何作为底层引擎,优雅地支持多页面(MPA)、库开发、服务端渲染(SSR)乃至与传统后端框架集成等多种复杂的架构模式。我们将从“为何需要”的架构考量出发,深入到“如何实现”的核心配置与最佳实践。


9.7.1. 多页面应用 (MPA):回归与现代化

架构考量
虽然单页应用(SPA)是现代前端的主流,但在某些场景下,多页面应用(MPA)依然是更优选择:

  • 内容驱动型网站: 公司官网、营销活动页、博客等,页面之间逻辑独立,MPA 对 SEO 更友好。
  • 隔离性要求: 当不同模块(如应用前台和后台管理系统)需要严格的环境和依赖隔离时。

Vite 不仅是 SPA 的利器,同样也能为现代化的 MPA 开发提供顶级的开发体验。


实战:将一个 Vue SPA 改造为 MPA

我们的目标是构建一个项目,它包含两个完全独立的页面:

  1. index.html: 面向普通用户的“前台应用”。
  2. admin.html: 面向管理员的“后台应用”。这两个应用将共享底层的 UI 组件和工具函数。

第一步:初始化一个标准的 Vue 3 SPA 项目
这为我们提供了一个熟悉且功能完备的起点。

1
2
3
4
# 创建一个标准的 Vite + Vue 项目
pnpm create vite mpa-project -- --template vue-ts
cd mpa-project
pnpm install

第二步:为 MPA 模式重构项目结构
我们需要创建第二个 HTML 入口和对应的 JavaScript 入口。

  1. 在项目根目录创建一个 admin 文件夹。
  2. 将根目录的 index.html 复制 一份到 admin/ 目录中,作为后台应用的入口。
  3. src 目录下新建一个 pages 文件夹,用于存放不同页面的入口 JS/TS 文件。
  4. 将原有的 src/main.ts 移动src/pages/main.ts
  5. src/pages/新建 一个 admin.ts 作为后台应用的入口。
  6. (可选) 创建一个共享组件,例如 src/components/TheHeader.vue

重构后的核心文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
# mpa-project/
├── admin/
│ └── index.html # 后台入口 HTML
├── src/
│ ├── components/
│ │ └── TheHeader.vue # 共享组件
│ └── pages/
│ ├── main.ts # 前台入口 TS
│ └── admin.ts # 后台入口 TS
├── index.html # 前台入口 HTML
└── vite.config.ts

第三步:配置 HTML 和 TS 入口

  1. 修改 index.html: 确保它的 <script> 标签指向新的 main.ts 路径。
    1
    <script type="module" src="/src/pages/main.ts"></script>
  2. 修改 admin/index.html: 确保它的 <script> 标签指向 admin.ts
    1
    <script type="module" src="/src/pages/admin.ts"></script>
  3. 编写 admin.ts:
    文件路径: src/pages/admin.ts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { createApp } from 'vue'
    import './style.css'
    // 我们可以创建一个独立的后台根组件 AdminApp.vue
    // import AdminApp from '../AdminApp.vue'

    // 为简化演示,我们直接渲染一段 HTML
    const adminApp = document.createElement('div');
    adminApp.innerHTML = `
    <h1>这是admin页面</h1>
    <p>只有管理员才能访问</p>
    `;
    document.body.appendChild(adminApp);

第四步:配置 vite.config.ts 以识别多入口
这是最关键的一步。我们需要告诉 Vite,我们的项目现在有两个入口,而不是一个。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
// 配置插件数组,这里使用 Vue 插件来支持 Vue 单文件组件
plugins: [vue()],
// 构建配置
build: {
// Rollup 打包器的选项配置
rollupOptions: {
// 配置多页面入口
input: {
// 键名 "main" 是我们为这个入口起的名字
// 值是该入口的 HTML 文件的绝对路径
// resolve(__dirname, 'index.html') 将当前目录与 'index.html' 拼接成绝对路径
main: resolve(__dirname, 'index.html'),

// 另一个入口
// 定义名为 "admin" 的入口,指向 admin 文件夹下的 index.html
admin: resolve(__dirname, 'admin/index.html'),
},
// 输出文件的命名规则配置
output: {
// 代码分割后的 chunk 文件命名格式:assets/js/[chunk 名称].js
chunkFileNames: 'assets/js/[name].js',
// 入口文件的命名格式:assets/js/[入口名称].js
entryFileNames: 'assets/js/[name].js',
// 静态资源文件的命名格式:assets/[文件扩展名]/[文件名].[扩展名]
assetFileNames: 'assets/[ext]/[name].[ext]',
},
},
},
})

【重要提示】为什么 TypeScript 找不到 path__dirname?

当你像上面这样修改 vite.config.ts 后,TypeScript 可能会立即报错,提示“无法找到模块 ‘path’ 的声明文件”或“找不到名称 ‘__dirname’”。

  • 原因: 你的项目代码(如 .vue.ts 文件)最终是运行在 浏览器环境 的,而 vite.config.ts 这个文件是运行在 Node.js 环境 的。TypeScript 默认只为浏览器环境提供类型检查,它不认识 Node.js 的内置模块(如 path)和全局变量(如 __dirname)。

  • 解决方案: 我们需要手动安装 Node.js 的类型定义文件,告诉 TypeScript “请相信我,这个配置文件是在 Node.js 环境下运行的”。

    1
    pnpm add -D @types/node

    -D 表示这是一个开发依赖,因为它只在开发阶段的类型检查时需要。安装后,错误就会消失,因为 TypeScript 现在有了 Node.js 的“说明书”,能够理解 path 等 API 了。这在任何需要配置 Node.js 环境脚本的 TypeScript 项目中都是一个标准操作。

第五步:效果验证

  1. 开发环境:
    运行 pnpm dev。现在你可以:

    • 访问 http://localhost:5173/ 查看前台应用。
    • 访问 http://localhost:5173/admin/http://localhost:5173/admin/index.html 查看后台应用。修改任何一个应用的源文件(包括共享组件),对应的页面都会实现 HMR 热更新,开发体验依然丝滑。
  2. 生产构建:
    运行 pnpm build

    Vite 成功地为每个入口都生成了独立的 HTML 和关联的资源文件,它们被清晰地组织在 dist 目录中,可以直接部署。

通过这个从 SPA 到 MPA 的改造过程,我们掌握了 Vite 处理多页面应用的核心配置。关键在于利用 build.rollupOptions.input 明确告知 Vite 项目的所有 HTML 入口。即使是复杂的 MPA 项目,Vite 依然能提供与 SPA 相媲美的顶级开发体验。


9.7.2. 库模式:打造高质量前端资产

核心目标
当我们开发一个 JS 工具库或 UI 组件库时,目标不再是生成一个可以直接运行的 index.html 应用,而是要发布一个或多个健壮、高效、易用的 JS 文件(以及附带的 CSS 和类型定义),让其他开发者可以在他们的项目中轻松导入和使用。


实战:封装一个 Vue 3 UI 组件并打包为库

场景: 假设我们在多个项目中都需要一个样式统一、功能独特的按钮组件。为了避免在各个项目中重复“CV 大法”(复制粘贴),我们决定将其封装成一个独立的 UI 库 @prorise/button,这章节可以暂时预览一下,我们在后面的教学会带大家手搓一个真正的组件库

第一步:初始化库项目

1
2
3
pnpm create vite prorise-ui-lib -- --template vue-ts
cd prorise-ui-lib
pnpm install

删除 src/components 目录下的 HelloWorld.vue,并创建一个新的按钮组件。

文件路径: src/components/ProriseButton.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<button class="prorise-button">
<slot></slot>
</button>
</template>

<script setup lang="ts">
// 这里可以定义 props, emits 等
</script>

<style>
.prorise-button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.prorise-button:hover {
background-color: #2980b9;
}
</style>

第二步:创建库入口文件
创建一个 src/index.ts 文件,统一导出库的所有内容。
文件路径: src/index.ts

1
2
3
import ProriseButton from './components/ProriseButton.vue';

export { ProriseButton };

第三步:配置 vite.config.ts 的库模式 (build.lib)
这是 Vite 库模式的核心。我们需要告诉 Vite,这是一个库,而不是一个应用。

文件路径: vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { resolve } from 'path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
plugins: [vue()],
build: {
// 开启库模式
lib: {
// 指定库的入口文件
entry: resolve(__dirname, 'src/index.ts'),
// UMD/IIFE 模式下的全局变量名
name: 'ProriseUI',
// 输出的文件名,不包含后缀
fileName: 'prorise-ui',
// 输出的模块格式,'es' 表示 ES Module, 'umd' 表示通用模块定义
formats: ['es', 'umd'],
},
// ... 后续会在这里添加 rollupOptions
},
});

关键挑战与解决方案

仅仅配置 build.lib 还不够,要打造一个“高质量”的库,我们必须解决以下三个关键问题。

挑战一:依赖管理

  • 痛点: 我们的 ProriseButton.vue 依赖于 vue。如果我们将 vue 的源码一起打包进我们的库文件,而使用者的项目本身也引入了 vue,就会导致最终应用中存在 两份 Vue 的代码!这会急剧增大包体积,甚至可能因为版本或实例不一致而引发运行时错误。
  • 解决方案: 将 vue 声明为 peerDependencies (对等依赖),并将其 外部化,即不打包进我们的库。
1
2
3
4
5
6
{
...
"peerDependencies": {
"vue": "^3.0.0"
}
}

peerDependencies 的意思是:“嘿,使用我这个库的项目,你自己必须也要安装 Vue 3,我只是‘借用’你的 Vue 来运行。”

  1. 修改 vite.config.ts:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // vite.config.ts
    export default defineConfig({
    //...
    build: {
    lib: { /* ... */ },
    rollupOptions: {
    // 确保外部化处理那些你不想打包进库的依赖
    external: ['vue'],
    output: {
    // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
    globals: {
    vue: 'Vue',
    },
    },
    },
    },
    });

挑战二:样式处理

  • 痛点: 我们组件的样式 (<style> 块) 如何交付给用户?
  • 解决方案: Vite 在库模式下,默认会自动将所有组件的 CSS 抽离成一个单独的 style.css 文件。这是最佳实践,因为它允许用户选择是否引入样式,或者用自己的样式覆盖它。我们无需额外配置。

挑战三:类型定义

  • 痛点: 我们的库是用 TypeScript 写的,如何让使用我们库的 TS 用户获得完美的类型提示和自动补全?
  • 解决方案: 我们需要生成 .d.ts 类型声明文件,并正确配置 package.json
  1. 配置 tsconfig.json:开启 declaration 选项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    {
    // TypeScript 编译选项
    "compilerOptions": {
    // 生成 .d.ts 声明文件
    "declaration": true,
    // 只生成声明文件,不生成 JavaScript 文件
    "emitDeclarationOnly": true,
    // 声明文件输出目录
    "outDir": "dist/types",
    // 启用严格模式
    "strict": true
    },
    // 包含的源文件目录
    "include": ["src"],
    // 不包含任何单独的文件
    "files": [],
    // 项目引用配置
    "references": [
    // 应用程序的 TypeScript 配置
    { "path": "./tsconfig.app.json" },
    // Node.js 环境的 TypeScript 配置
    { "path": "./tsconfig.node.json" }
    ]
    }

  2. 添加构建脚本: 在 package.json 中添加一个专门用于生成类型文件的脚本。

    1
    2
    3
    4
    "scripts": {
    "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types",
    "build": "npm run build:types && vite build"
    }

    现在运行 pnpm build,它会先生成类型文件,然后再进行打包。

  3. 配置 package.json 的导出字段: 这是最后,也是最关键的一步,告诉使用者的项目如何找到我们的代码和类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "name": "@prorise/ui-lib",
    "version": "1.0.0",
    "private": false, // 必须为 false 才能发布
    "files": [ // 定义哪些文件会被发布到 npm
    "dist"
    ],
    "main": "./dist/prorise-ui.umd.js", // CommonJS/UMD 入口
    "module": "./dist/prorise-ui.es.js", // ES Module 入口
    "types": "./dist/types/index.d.ts", // TypeScript 类型定义入口
    "exports": {
    ".": {
    "import": "./dist/prorise-ui.es.js",
    "require": "./dist/prorise-ui.umd.js",
    "types": "./dist/types/index.d.ts"
    },
    "./style.css": "./dist/style.css" // 暴露 CSS 文件
    },
    "peerDependencies": {
    "vue": "^3.0.0"
    },
    // ...
    }

最终构建与验证
运行 pnpm builddist 目录的结构将非常专业:

1
2
3
4
5
6
7
8
dist/
├── types/
│ ├── components/
│ │ └── ProriseButton.vue.d.ts
│ └── index.d.ts # 类型定义入口
├── prorise-ui.es.js # ES Module 产物
├── prorise-ui.umd.js # UMD 产物
└── style.css # 抽离的 CSS 文件

现在,这个库已经可以发布到 NPM,并在任何 Vue 3 项目中通过 import { ProriseButton } from '@prorise/ui-lib'import '@prorise/ui-lib/style.css' 来使用了。


9.7.3. 与传统后端框架的“混合”模式

场景定义
我们来设想一个在企业中极为常见的场景:你正在维护一个由 Spring Boot 构建的、使用 Thymeleaf 作为模板引擎的电商网站。现在,你需要在一个由后端渲染的商品详情页上,嵌入一个由 Vite + Vue/React 开发的、功能复杂的“商品3D定制”组件。

在这个“混合”模式下,我们将让新旧技术协同工作:Spring Boot 继续负责路由、数据接口和页面骨架的渲染,而 Vite 则扮演前端资源构建管道的角色,专门负责编译、打包和提供现代化前端组件所需的 JS 和 CSS 资源。


生产环境工作流:Manifest 与后端服务的“契约”

核心挑战:在生产环境,Vite 构建出的资源文件名都带有哈希值(如 main-a1b2c3d4.js)以实现长效缓存。我们的 Spring Boot Thymeleaf 模板,如何能动态地知道这个每次构建都会变化的、正确的文件名呢?

解决方案:通过 build.manifest 文件。这是 Vite 与后端服务之间沟通的“契约”。

第一步:配置 Vite (vite.config.ts)
我们需要告诉 Vite 两件事:1. 开启 manifest 生成。 2. 将构建产物输出到 Spring Boot 约定的静态资源目录中 (src/main/resources/static)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' // 以 Vue 为例
import { resolve } from 'path'

export default defineConfig({
plugins: [vue()],
build: {
// 1. 生成 manifest.json 文件,它包含了源文件到构建后文件的映射
manifest: true,
// 2. 指定构建产物的输出目录,直接指向 Spring Boot 的静态资源文件夹
// '..' 表示上级目录,因为 vite.config.ts 通常在项目根目录,而 static 目录在后端模块中
outDir: resolve(__dirname, '../backend/src/main/resources/static/dist'),
rollupOptions: {
// 3. 指定 Vite 构建的入口文件
input: resolve(__dirname, 'src/main.ts'),
},
},
})

第二步:在 Spring Boot 中创建 manifest.json 解析服务
我们需要一个 Java 服务来读取和解析 dist/manifest.json 文件,以便在模板中调用。

  1. 添加依赖: 确保你的 pom.xml 中有 JSON 解析库,如 Jackson (spring-boot-starter-web 默认包含)。
  2. 创建服务类:
    文件路径: src/main/java/com/prorise/vite/ViteAssetResolver.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    package com.prorise.vite;

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.stereotype.Service;
    import org.springframework.util.ResourceUtils;

    import java.io.File;
    import java.io.IOException;

    @Service
    public class ViteAssetResolver {

    private final ObjectMapper objectMapper = new ObjectMapper();
    private JsonNode manifest;

    // 服务启动时,读取并解析 manifest.json 文件
    public ViteAssetResolver() {
    try {
    // 从 Spring Boot 的 classpath 中查找 manifest.json
    File file = ResourceUtils.getFile("classpath:static/dist/manifest.json");
    this.manifest = objectMapper.readTree(file);
    } catch (IOException e) {
    // 在生产环境中,如果找不到 manifest 文件,应该抛出异常或记录严重错误
    // 在开发中可以忽略,因为我们不会使用它
    this.manifest = null;
    System.err.println("Error reading manifest.json: " + e.getMessage());
    }
    }

    // 公共方法,根据源文件名(如 src/main.ts)获取最终带哈希的 JS 文件路径
    public String getJs(String entry) {
    if (manifest == null || !manifest.has(entry)) return "";
    return "/dist/" + manifest.get(entry).get("file").asText();
    }

    // 公共方法,获取入口文件关联的所有 CSS 文件路径
    public String[] getCss(String entry) {
    if (manifest == null || !manifest.has(entry) || !manifest.get(entry).has("css")) return new String[0];

    JsonNode cssNode = manifest.get(entry).get("css");
    String[] cssFiles = new String[cssNode.size()];
    for (int i = 0; i < cssNode.size(); i++) {
    cssFiles[i] = "/dist/" + cssNode.get(i).asText();
    }
    return cssFiles;
    }
    }

第三步:在 Thymeleaf 模板中动态注入资源
现在,我们可以在 Thymeleaf 模板中,通过调用 ViteAssetResolver 服务来动态生成 <script><link> 标签。

文件路径: src/main/resources/templates/product-detail.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品详情</title>
<link th:each="cssFile : ${@viteAssetResolver.getCss('src/main.ts')}"
th:href="${cssFile}" rel="stylesheet">
</head>
<body>
<h1>这是一个由 Spring Boot + Thymeleaf 渲染的页面</h1>
<p>下面这个区域将由 Vite + Vue/React 接管</p>

<div id="app"></div>

<script type="module" th:src="${@viteAssetResolver.getJs('src/main.ts')}"></script>
</body>
</html>

开发环境工作流:代理与 HMR

在开发时,我们的目标是:访问由 Spring Boot 服务(http://localhost:8080)提供的页面,但页面上的 Vue/React 组件,又能享受到 Vite Dev Server(http://localhost:5173)带来的 HMR 和极速更新。

这同样需要后端与 Vite 协同。我们需要让 Thymeleaf 模板能够判断当前环境,并加载不同的资源。

Thymeleaf 模板 (增加开发环境判断)

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="app"></div>

<th:block th:if="${isDevelopmentMode}">
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/src/main.ts"></script>
</th:block>

<th:block th:unless="${isDevelopmentMode}">
<script type="module" th:src="${@viteAssetResolver.getJs('src/main.ts')}"></script>
</th:block>
</body>
</html>

通过这种方式,我们完美地实现了开发与生产环境的隔离。开发时,前端资源由 Vite 实时提供,享受极致的开发体验;生产部署时,则加载由 Vite 精心构建和优化的静态资源。