vite面试题提交
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"topic_20260626-084849_ae13b749f70ea556": "auto"
|
"topic_20260626-084849_ae13b749f70ea556": "auto",
|
||||||
|
"topic_20260626-085627_e3928116af40710e": "auto",
|
||||||
|
"topic_20260626-093001_b040128cfc0f086c": "auto"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"topic_20260626-084849_ae13b749f70ea556": "这个文件中的plugin调用的函数h…"
|
"topic_20260626-084849_ae13b749f70ea556": "这个文件中的plugin调用的函数h…",
|
||||||
|
"topic_20260626-085627_e3928116af40710e": "我写这个项目主要是用做面试用的,所以…",
|
||||||
|
"topic_20260626-093001_b040128cfc0f086c": "帮我在plugins/plugin…"
|
||||||
}
|
}
|
||||||
617
docs/vite.md
Normal file
617
docs/vite.md
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
# Vite 面试题(含详细解答)
|
||||||
|
|
||||||
|
> 基于本项目的 Vue 3 + Vite + 自定义插件实战经验整理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vite 和 Webpack 在项目开发中有哪些区别?
|
||||||
|
|
||||||
|
这是一道高频面试题,关键不是背概念,而是从**开发体验、构建机制、配置复杂度、生态兼容性**四个维度说清楚。
|
||||||
|
|
||||||
|
### 1.1 核心差异:开发阶段的模块处理方式
|
||||||
|
|
||||||
|
**Webpack** 是一个 **Bundle-based** 的构建工具。开发时它会从入口文件出发,递归解析所有依赖,把所有模块打包成一个(或多个)bundle,然后启动 dev server。每次代码变更,它需要重新打包受影响的模块并刷新 bundle,这就是为什么大项目中 Webpack 的 HMR 有时会明显变慢。
|
||||||
|
|
||||||
|
**Vite** 在开发阶段不走打包流程,而是利用浏览器原生支持的 **ES Module (ESM)**。它直接把 `.vue`、`.ts`、`.jsx` 等源码文件按需编译后通过 ESM 发给浏览器。浏览器通过 `<script type="module">` 发起一个个 import 请求,Vite 的 dev server 拦截这些请求并实时编译返回。这种「按需编译」的方式使得冷启动和热更新都非常快,几乎与项目规模无关。
|
||||||
|
|
||||||
|
```
|
||||||
|
Webpack 开发流程:
|
||||||
|
入口 → 递归打包所有依赖 → bundle → dev server → 浏览器
|
||||||
|
|
||||||
|
Vite 开发流程:
|
||||||
|
浏览器 import → dev server 拦截 → 按需编译单个文件 → 返回浏览器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 冷启动速度
|
||||||
|
|
||||||
|
| | Webpack | Vite |
|
||||||
|
|---|---|---|
|
||||||
|
| 冷启动 | 预先打包整个应用,大型项目可能需要几十秒甚至几分钟 | 只编译浏览器当前请求的文件,通常 < 1s |
|
||||||
|
| 原理 | Bundle-first | ESM on-demand |
|
||||||
|
|
||||||
|
### 1.3 热更新(HMR)
|
||||||
|
|
||||||
|
- **Webpack**:文件变更后,需要重新构建受影响的模块链并替换 bundle,项目越大越慢。
|
||||||
|
- **Vite**:只失效变更文件及其直接依赖链上的模块,通过 WebSocket 推送更新,浏览器重新 import 即可。因为本身就是按需加载的,HMR 粒度天然更细。
|
||||||
|
|
||||||
|
### 1.4 底层依赖
|
||||||
|
|
||||||
|
| 阶段 | Webpack | Vite |
|
||||||
|
|---|---|---|
|
||||||
|
| 开发构建 | Webpack 自己(JS 实现) | **esbuild**(Go 实现,快 10-100 倍)做预构建 |
|
||||||
|
| 生产打包 | Webpack / Terser | **Rollup**(成熟的 ES module 打包器) |
|
||||||
|
|
||||||
|
**为什么 Vite 不用 esbuild 打包生产环境?** esbuild 虽然极快,但在旧版本js代码支持,代码分割、Tree Shaking 的精细控制方面不如 Rollup 成熟。Vite 选择 Rollup 作为生产构建引擎是务实的选择。
|
||||||
|
|
||||||
|
#### 1.4.1 依赖预构建的两大作用(关键技术)
|
||||||
|
|
||||||
|
Vite 的依赖预构建解决两个核心问题:
|
||||||
|
|
||||||
|
**① 格式兼容 — CJS/UMD → ESM**
|
||||||
|
|
||||||
|
很多第三方依赖(特别是老牌 npm 包)发布的是 CommonJS 或 UMD 格式。浏览器原生只支持 ESM,无法直接加载 CJS 模块。esbuild 在预构建阶段将这些包统一转换为浏览器可识别的 ESM 格式。
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node_modules 中的 CJS 包
|
||||||
|
module.exports = function sum(a, b) { return a + b }
|
||||||
|
|
||||||
|
// 预构建后(node_modules/.vite/deps/sum.js)
|
||||||
|
export default function sum(a, b) { return a + b }
|
||||||
|
```
|
||||||
|
|
||||||
|
**② 性能优化 — 合并细碎模块**
|
||||||
|
|
||||||
|
有些包内部由成百上千个独立文件组成(典型如 `lodash-es`,每个工具函数一个文件)。如果不做合并,浏览器将发起数百个级联的 HTTP 请求,形成严重的请求瀑布:
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器 import lodash-es
|
||||||
|
→ 请求 /node_modules/lodash-es/add.js
|
||||||
|
→ 请求 /node_modules/lodash-es/_baseAdd.js
|
||||||
|
→ 请求 /node_modules/lodash-es/_createMathOperation.js
|
||||||
|
→ ...(数百个请求层层依赖)
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite 将这些细碎模块预构建为单个或少数几个文件,把成百上千次请求合并成数次,大幅减少浏览器请求带来的性能开销。预构建结果缓存在 `node_modules/.vite/deps/`,只在依赖变更时重新执行。
|
||||||
|
|
||||||
|
### 1.5 配置复杂度
|
||||||
|
|
||||||
|
**Webpack**:你需要配置 `entry`、`output`、`loader`(如 `css-loader`、`style-loader`、`babel-loader`)、`plugin`(如 `HtmlWebpackPlugin`、`MiniCssExtractPlugin`)。一个中等规模的项目 webpack.config.js 动辄几百行。
|
||||||
|
|
||||||
|
**Vite**:开箱即用。`.vue`、`.ts`、`.jsx`、CSS、静态资源都不需要额外配置。一个典型的 Vite 配置:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js — 本项目实际配置
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()] // 一行搞定 Vue SFC 支持
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.6 生态与兼容性
|
||||||
|
|
||||||
|
- **Webpack**:生态极其庞大,loader 和 plugin 覆盖几乎所有场景。如果你需要兼容 IE11 或做一些非常规的构建操作,Webpack 是更安全的选择。
|
||||||
|
- **Vite**:插件生态快速增长中,但一些老旧/小众的 Webpack loader 可能没有直接对应。不过 Vite 的 Rollup 插件兼容层可以复用大量 Rollup 插件。
|
||||||
|
|
||||||
|
### 1.7 总结
|
||||||
|
|
||||||
|
| 维度 | Webpack | Vite |
|
||||||
|
|---|---|---|
|
||||||
|
| 开发模式 | Bundle-first | ESM on-demand |
|
||||||
|
| 冷启动 | 慢(全量打包) | 快(按需编译) |
|
||||||
|
| HMR | 随项目规模递减 | 几乎恒定 |
|
||||||
|
| 预构建 | 无 | esbuild(Go) |
|
||||||
|
| 生产打包 | Webpack + Terser | Rollup |
|
||||||
|
| 配置复杂度 | 高 | 低 |
|
||||||
|
| 生态成熟度 | 极高 | 快速增长 |
|
||||||
|
| 浏览器要求 | 无限制 | 需支持 ESM(现代浏览器) |
|
||||||
|
|
||||||
|
**一句话总结**:Vite 用「开发时不打包 + esbuild 预构建 + Rollup 生产打包」的组合拳,在开发体验上对 Webpack 形成了代际优势;Webpack 则在兼容性和极端定制场景上仍有不可替代的地位。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Vite 的核心工作原理是什么?
|
||||||
|
|
||||||
|
### 2.1 开发阶段
|
||||||
|
|
||||||
|
1. **启动 dev server**:Vite 启动一个基于 [connect](https://github.com/senchalabs/connect) 的 HTTP 服务器。
|
||||||
|
2. **预构建依赖**:用 esbuild 把 `node_modules` 中的 CommonJS/UMD 依赖转换为 ESM,并合并细碎模块(如 lodash-es 的几百个文件)以减少浏览器请求数。
|
||||||
|
3. **源码按需编译**:浏览器请求 `src/main.js` → Vite 拦截 → 用 esbuild 编译 JS/TS → 返回 ESM 格式。
|
||||||
|
4. **`.vue` 文件处理**:由 `@vitejs/plugin-vue` 拦截 `.vue` 请求,将其拆分为 template、script、style 三部分分别处理。
|
||||||
|
5. **HMR**:文件变更时,Vite 通过 WebSocket 通知浏览器,浏览器重新请求变更的模块。
|
||||||
|
|
||||||
|
### 2.2 生产构建
|
||||||
|
|
||||||
|
直接使用 Rollup 打包,因为 Rollup 在 tree-shaking、代码分割、输出稳定性方面是最好的 ESM 打包器。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Vite 的预构建(Pre-bundling)做了什么?为什么需要它?
|
||||||
|
|
||||||
|
### 3.1 做了什么
|
||||||
|
|
||||||
|
Vite 使用 **esbuild** 对 `node_modules` 中的依赖进行预构建:
|
||||||
|
|
||||||
|
1. **CJS → ESM**:将 CommonJS/UMD 格式的包转为 ESM。
|
||||||
|
2. **合并碎片模块**:有些包(如 `lodash-es`)由数百个独立文件组成,Vite 将它们合并为单个模块,大幅减少 HTTP 请求。
|
||||||
|
|
||||||
|
### 3.2 为什么需要
|
||||||
|
|
||||||
|
- **CJS 兼容**:浏览器只认识 ESM,而 npm 上有大量 CJS 包。
|
||||||
|
- **请求瀑布**:如果不合并,`lodash-es` 的一个 import 可能触发几百个级联请求,拖慢页面加载。
|
||||||
|
|
||||||
|
### 3.3 缓存策略
|
||||||
|
|
||||||
|
预构建结果缓存在 `node_modules/.vite/deps/`,只有 `package.json` 中的依赖列表变更时才会重新执行,开发阶段几乎无感。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 为什么 Vite 开发时用 esbuild,生产打包用 Rollup?
|
||||||
|
|
||||||
|
| | esbuild | Rollup |
|
||||||
|
|---|---|---|
|
||||||
|
| 优势 | 极快(Go 语言,原生并发) | 成熟的 tree-shaking、精细的代码分割控制 |
|
||||||
|
| 劣势 | 代码分割能力弱,对 ESM 输出的精细控制不够 | 相对较慢 |
|
||||||
|
| 适用阶段 | **开发**——需要快速编译和预构建 | **生产**——需要最优的输出质量 |
|
||||||
|
|
||||||
|
Vite 不是绑死在一个工具上,而是「谁适合干什么就让它干什么」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vite 插件钩子完整有哪些?执行顺序是怎样的?
|
||||||
|
|
||||||
|
Vite 插件兼容 Rollup 全部钩子,并扩展了 Vite 特有钩子。钩子分为 **Build 阶段**(解析模块依赖)和 **Output 阶段**(生成产物)。
|
||||||
|
|
||||||
|
### 5.1 钩子分类总览
|
||||||
|
|
||||||
|
| 阶段 | 钩子 | 要点 |
|
||||||
|
|------|------|------|
|
||||||
|
| Build | `options` | 修改构建配置 |
|
||||||
|
| Build | `buildStart` | 构建开始 |
|
||||||
|
| Build | `resolveId` | 自定义模块解析(alias、虚拟模块) |
|
||||||
|
| Build | `load` | 返回模块源码(虚拟模块核心) |
|
||||||
|
| Build | `transform` | **最常用** — 转换单个模块代码 |
|
||||||
|
| Build | `moduleParsed` | AST 解析完成,读取 import/export |
|
||||||
|
| Build | `resolveDynamicImport` | 处理动态 `import()` |
|
||||||
|
| Build | `buildEnd` | 构建结束(成功/失败都调用) |
|
||||||
|
| Output | `outputOptions` | 修改输出配置 |
|
||||||
|
| Output | `renderStart` | 产物生成开始 |
|
||||||
|
| Output | `banner / footer / intro / outro` | chunk 首尾注入代码 |
|
||||||
|
| Output | `renderChunk` | 转换整个 chunk 代码 |
|
||||||
|
| Output | `augmentChunkHash` | 影响 chunk 文件名 hash |
|
||||||
|
| Output | `generateBundle` | **最常用** — 产物生成完、写盘前增删改 |
|
||||||
|
| Output | `writeBundle` | 产物写盘后 |
|
||||||
|
| Output | `closeBundle` | 收尾清理 |
|
||||||
|
| Vite | `config` | 修改/合并 vite 配置(插件入口首选) |
|
||||||
|
| Vite | `configResolved` | 配置解析完毕,缓存最终配置 |
|
||||||
|
| Vite | `configureServer` | 给 dev server 加中间件 |
|
||||||
|
| Vite | `configurePreviewServer` | 给预览服务器加中间件 |
|
||||||
|
| Vite | `transformIndexHtml` | 向 HTML 注入 `<script>` / `<link>` 标签 |
|
||||||
|
| Vite | `handleHotUpdate` | 自定义 HMR 行为 |
|
||||||
|
|
||||||
|
### 5.2 完整执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
options → buildStart
|
||||||
|
→ resolveId → load → transform → moduleParsed
|
||||||
|
→ resolveDynamicImport
|
||||||
|
→ buildEnd
|
||||||
|
|
||||||
|
→ outputOptions → renderStart
|
||||||
|
→ banner → footer → intro → outro
|
||||||
|
→ renderChunk → augmentChunkHash
|
||||||
|
→ generateBundle → writeBundle → closeBundle
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 钩子执行类型
|
||||||
|
|
||||||
|
| 类型 | 含义 | 典型钩子 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **async** | 可返回 Promise | `buildStart`、`transform` |
|
||||||
|
| **sync** | 同步执行 | `outputOptions` |
|
||||||
|
| **first** | 多个插件排队,第一个返回非 null 即停止 | `resolveId`、`load` |
|
||||||
|
| **sequential** | 多个插件按序依次执行 | `transform`、`renderChunk` |
|
||||||
|
| **parallel** | 多个插件并行执行 | `buildEnd`、`moduleParsed` |
|
||||||
|
|
||||||
|
### 5.4 常见面试追问
|
||||||
|
|
||||||
|
**transform vs renderChunk?** transform 针对**单个模块**(AST 解析前),renderChunk 针对**整个 chunk**(多个模块合并后)。
|
||||||
|
|
||||||
|
**generateBundle 为什么最常用?** 产物已生成但未写盘,可 `this.emitFile()` 追加资源、`delete bundle[fileName]` 删除、直接改 `chunk.code`。
|
||||||
|
|
||||||
|
**虚拟模块怎么实现?** `resolveId` 拦截特定 id → `load` 返回代码字符串(id 前加 `\0` 前缀标记虚拟)。
|
||||||
|
|
||||||
|
**插件间怎么共享数据?** `moduleInfo.meta` 对象可读写;闭包缓存 `configResolved` 的配置引用。
|
||||||
|
|
||||||
|
**废弃钩子:** `transformBundle` → `generateBundle`;`transformChunk` → `renderChunk`;`ongenerate` / `onwrite` → `generateBundle` / `writeBundle`。
|
||||||
|
|
||||||
|
### 5.5 enforce 执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
enforce: 'pre' 插件
|
||||||
|
↓
|
||||||
|
核心插件(如 @vitejs/plugin-vue)
|
||||||
|
↓
|
||||||
|
普通插件(默认,如本项目中的 helloPlugin)
|
||||||
|
↓
|
||||||
|
enforce: 'post' 插件
|
||||||
|
↓
|
||||||
|
Vite 内置构建插件
|
||||||
|
```
|
||||||
|
|
||||||
|
本项目中的自定义插件示例:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js 中的 helloPlugin
|
||||||
|
function helloPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'helloPlugin',
|
||||||
|
transform(src, id) {
|
||||||
|
if (!id.includes('/src/')) return null // 只处理业务代码
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const result = src.replace(/console\.log\([^)]*\);?/g, '')
|
||||||
|
return { code: result, map: null }
|
||||||
|
}
|
||||||
|
return null // 返回 null 表示不处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `transform` 钩子在每个模块被编译时调用,可修改源码。
|
||||||
|
- 返回 `null` 表示不作为,Vite 继续默认流程。
|
||||||
|
- `name` 字段必须有,用于调试和错误追踪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Vite 的 HMR(热模块替换)是如何工作的?
|
||||||
|
|
||||||
|
1. **文件监听**:Vite 使用 [chokidar](https://github.com/paulmillr/chokidar) 监听文件变更。
|
||||||
|
2. **模块失效**:文件变更后,Vite 以该文件为起点,沿着模块依赖图找到所有受影响的模块。
|
||||||
|
3. **边界判定**:找到最近的「HMR 边界」(通常是接受热更新的组件/模块),只重载边界内的模块。
|
||||||
|
4. **WebSocket 推送**:Vite 通过 WebSocket 向浏览器发送更新信息(变更文件路径 + 更新类型)。
|
||||||
|
5. **浏览器处理**:客户端 runtime 收到消息后,重新 import 变更的模块并执行框架特定的更新逻辑(如 Vue 的 `hot.accept`)。
|
||||||
|
|
||||||
|
对于 Vue SFC,`@vitejs/plugin-vue` 为每个 `.vue` 文件注入 HMR 边界代码,所以修改一个组件只更新它自己,不影响页面状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Vite 中如何处理环境变量?
|
||||||
|
|
||||||
|
Vite 通过 `.env` 文件加载环境变量,支持四种优先级:
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `.env` | 所有环境共享 |
|
||||||
|
| `.env.local` | 本地覆盖(应加入 .gitignore) |
|
||||||
|
| `.env.[mode]` | 特定模式(如 `.env.production`) |
|
||||||
|
| `.env.[mode].local` | 特定模式的本地覆盖 |
|
||||||
|
|
||||||
|
在代码中通过 `import.meta.env` 访问:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 所有变量,VITE_ 前缀的才会暴露给客户端
|
||||||
|
console.log(import.meta.env.VITE_API_BASE_URL)
|
||||||
|
|
||||||
|
// 内置变量
|
||||||
|
import.meta.env.MODE // 'development' | 'production'
|
||||||
|
import.meta.env.DEV // boolean
|
||||||
|
import.meta.env.PROD // boolean
|
||||||
|
import.meta.env.BASE_URL // 部署基础路径
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全性**:只有 `VITE_` 前缀的变量会暴露给客户端,这防止了服务端密钥泄露到浏览器。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Vite 如何做代码分割(Code Splitting)?
|
||||||
|
|
||||||
|
Vite 使用 Rollup 的生产构建能力,代码分割主要通过以下方式:
|
||||||
|
|
||||||
|
### 8.1 动态 import(自动分割)
|
||||||
|
|
||||||
|
任何动态 import 都会在打包时自动产生独立的 chunk,无需任何配置:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 源码
|
||||||
|
import('./sum.js').then(({ sum }) => {
|
||||||
|
console.log(sum(1, 2))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打包产物
|
||||||
|
// dist/assets/sum-xxxx.js ← 自动分离为独立 chunk
|
||||||
|
```
|
||||||
|
|
||||||
|
打包产物文件名中的 `xxxx` 是内容哈希,用于浏览器长期缓存。当文件内容不变时,哈希不变,浏览器直接命中缓存。
|
||||||
|
|
||||||
|
### 8.2 手动配置分割策略 — 对象语法
|
||||||
|
|
||||||
|
如果你的项目里有一个通用模块需要单独拆包(比如一段多处引用的工具函数),可以通过 `manualChunks` 手动指定:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/test.js
|
||||||
|
export const sayHello = () => {
|
||||||
|
console.log('hello')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// key: chunk 名称,value: 模块路径
|
||||||
|
'test-vendor': './src/test.js'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 手动配置分割策略 — 函数语法(更灵活)
|
||||||
|
|
||||||
|
对象语法只能匹配精确路径,函数语法可以基于模块 ID 做任意逻辑判断:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('test')) {
|
||||||
|
return 'test-vendor' // 所有路径含 'test' 的模块合并为一个 chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
函数语法更适合实际项目:你可以按 `node_modules` 来源拆分 vendor,按业务域拆分 page chunks,或者按体积阈值动态决定是否拆分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Vite 的 CSS 处理有什么特点?
|
||||||
|
|
||||||
|
1. **原生 CSS 支持**:直接 `import './style.css'`,Vite 自动注入到页面。
|
||||||
|
2. **CSS Modules**:文件名以 `.module.css` 结尾,自动启用 CSS Modules。
|
||||||
|
3. **PostCSS**:项目根目录放置 `postcss.config.js` 即可,Vite 会自动应用。
|
||||||
|
4. **CSS 代码分割**:生产构建时,每个异步 chunk 的 CSS 会被提取为独立文件,按需加载。
|
||||||
|
5. **`@import` 内联与 rebase**:`@import` 和 `url()` 路径会自动重写,避免开发与生产路径不一致。
|
||||||
|
6. **预处理器**:安装 `sass` 或 `less` 即可直接使用 `.scss`、`.less` 文件,无需额外配置 loader。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Vite 的静态资源处理策略是怎样的?
|
||||||
|
|
||||||
|
- **小资源(< 4KB)**:自动转为 base64 内联,减少 HTTP 请求。
|
||||||
|
- **大资源**:复制到 `dist/assets/`,文件名包含 hash 用于缓存。
|
||||||
|
- **`public` 目录**:此目录下的资源**不会被处理**,直接复制到 `dist/` 根目录,适合 `robots.txt`、`favicon.ico` 等。
|
||||||
|
- **`new URL`**:支持通过 `new URL('./img.png', import.meta.url)` 动态引用资源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 如何调试 Vite 的自定义插件?
|
||||||
|
|
||||||
|
1. **添加 `name`**:每个插件必须有唯一的 `name`,方便定位问题。
|
||||||
|
2. **在项目中引入 vite-plugin-inspect**
|
||||||
|
3. **使用 console.log**:在插件的各个钩子中添加日志,观察执行时机和参数。
|
||||||
|
**启用 debug 模式**:
|
||||||
|
```bash
|
||||||
|
DEBUG=vite:* vite
|
||||||
|
```
|
||||||
|
5. **使用 Vite 的 `apply` 选项**:可以限制插件只在开发或生产环境生效:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'dev-only-plugin',
|
||||||
|
apply: 'serve' // 或 'build',分别限制开发/生产
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Vite 项目从开发到部署的完整流程是怎样的?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发
|
||||||
|
npm run dev # vite — 启动 dev server,ESM 按需编译 + HMR
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
npm run build # vite build — Rollup 打包,输出到 dist/
|
||||||
|
|
||||||
|
# 本地预览生产构建
|
||||||
|
npm run preview # vite preview — 在本地启动静态服务器预览 dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
典型部署:将 `dist/` 目录部署到 CDN 或静态服务器(Nginx、Vercel、Netlify 等)。
|
||||||
|
|
||||||
|
对于 SPA 应用,需要配置 fallback 到 `index.html`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 有没有了解微内核设计?Vite 的插件化设计思想是怎样完成个性化打包构建需求的?
|
||||||
|
|
||||||
|
### 13.1 什么是微内核架构
|
||||||
|
|
||||||
|
微内核(Microkernel)是一种架构模式,核心思想是:**内核只提供最精简的基础能力,所有扩展功能通过插件机制加载**。操作系统领域最经典的例子是 Minix 和 macOS 的 XNU 内核。
|
||||||
|
|
||||||
|
在前端构建工具领域,这个思想同样适用:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ 插件层(Plugin Layer) │
|
||||||
|
│ Vue SFC │ JSX │ TS │ CSS │ ... │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ 微内核(Minimal Core) │
|
||||||
|
│ dev server │ HMR │ module graph │
|
||||||
|
│ esbuild预构建 │ 中间件机制 │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.2 Vite 的微内核设计
|
||||||
|
|
||||||
|
Vite 的核心(内核)只做几件事:
|
||||||
|
|
||||||
|
| 内核能力 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| Dev Server | 基于 connect 的 HTTP 服务器,拦截浏览器请求 |
|
||||||
|
| 模块图谱 | 追踪所有模块的依赖关系,驱动 HMR |
|
||||||
|
| esbuild 预构建 | CJS → ESM 转换 + 细碎模块合并 |
|
||||||
|
| 中间件/钩子系统 | 提供插件接入点,但不做具体编译 |
|
||||||
|
|
||||||
|
**Vue SFC 编译、JSX 转换、TypeScript 类型检查、CSS 预处理 —— 这些全部由插件完成,不在内核里。** 这就是微内核思想:内核保持精简稳定,功能由插件按需扩展。
|
||||||
|
|
||||||
|
### 13.3 插件机制如何实现个性化构建
|
||||||
|
|
||||||
|
Vite 的插件体系基于 **Rollup 插件接口** 并做了增强:
|
||||||
|
|
||||||
|
**① 通用钩子(Rollup 兼容层)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'my-plugin',
|
||||||
|
// 构建开始
|
||||||
|
buildStart() {},
|
||||||
|
// 模块解析(可拦截 import 路径,实现虚拟模块)
|
||||||
|
resolveId(id) {},
|
||||||
|
// 模块加载(可返回自定义内容)
|
||||||
|
load(id) {},
|
||||||
|
// 代码转换(最常用)
|
||||||
|
transform(src, id) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**② Vite 独有钩子**
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
name: 'my-vite-plugin',
|
||||||
|
// 配置解析阶段(修改用户配置)
|
||||||
|
config(config) {},
|
||||||
|
// dev server 启动后(注入自定义中间件)
|
||||||
|
configureServer(server) {},
|
||||||
|
// 处理 HMR 更新
|
||||||
|
handleHotUpdate(ctx) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.4 一个完整的个性化构建案例
|
||||||
|
|
||||||
|
回到本项目的 `helloPlugin`,它实现了一个个性化需求——「生产环境自动移除 console.log」:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// vite.config.js
|
||||||
|
function helloPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'helloPlugin',
|
||||||
|
// 接入 transform 钩子,拿到每个模块的源码
|
||||||
|
transform(src, id) {
|
||||||
|
if (!id.includes('/src/')) return null // 只处理业务代码
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const result = src.replace(/console\.log\([^)]*\);?/g, '')
|
||||||
|
return { code: result, map: null }
|
||||||
|
}
|
||||||
|
return null // 不处理则透传
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个插件展示了插件化的核心价值:**不需要修改 Vite 源码,不需要 fork 项目,只需写一个函数接入钩子,就能定制打包行为。** 这种「内核不动、插件扩展」的模式,正是微内核架构在前端工程化中的最佳实践。
|
||||||
|
|
||||||
|
### 13.5 总结:Vite 的插件化设计思想怎样完成个性化打包构建需求?
|
||||||
|
|
||||||
|
一个结构化的回答应包含以下几点:
|
||||||
|
|
||||||
|
1. **架构层面**:Vite 采用了**微内核设计**。它的核心非常轻量,仅包含一个 ESM 开发服务器和一套插件加载机制。所有高层功能,如框架支持、CSS 预处理、特定文件加载等,都由独立的插件实现。
|
||||||
|
2. **接口层面**:Vite 提供了一套**兼容 Rollup 的、统一的插件接口**。这套接口通过暴露一系列生命周期钩子 (**Hooks**),允许开发者在构建过程的各个关键节点(如配置解析、模块加载、代码转换、HTML 生成等)注入自定义逻辑。
|
||||||
|
3. **实践层面**:开发者可以编写一个插件,通过选择合适的钩子来完成特定任务。例如,使用 `transform` 钩子可以支持一种新的语言或文件格式;使用 `configureServer` 钩子可以在开发时添加自定义的服务器中间件;使用 `transformIndexHtml` 则可以动态修改主页面的内容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 站在前端架构角度说说 Bundleless 原理
|
||||||
|
|
||||||
|
### 14.1 什么是 Bundleless
|
||||||
|
|
||||||
|
传统的 Bundle 模式(Webpack 为代表)是「先打包再启动」:
|
||||||
|
|
||||||
|
```
|
||||||
|
源码 → 打包器(Bundle)→ 一个/多个 bundle 文件 → 浏览器加载
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundleless 模式(Vite 为代表)跳过了打包这一步:
|
||||||
|
|
||||||
|
```
|
||||||
|
源码 → 开发服务器按需编译 → 浏览器通过 ESM 直接加载
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.2 为什么 Bundleless 现在才成为主流
|
||||||
|
|
||||||
|
Bundleless 不是新概念——ESM 规范早在 2015 年就发布了。之前无法普及的原因:
|
||||||
|
|
||||||
|
| 时间 | 瓶颈 | 现状 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2015-2018 | 浏览器 ESM 支持率低(IE 占主流) | IE 已淘汰,现代浏览器全覆盖 |
|
||||||
|
| 2015-2019 | npm 包几乎全是 CJS 格式 | 新包逐渐提供 ESM 入口,且有 esbuild 做转换 |
|
||||||
|
| 2015-2019 | HTTP/1.1 并发连接有限(6个/域名) | HTTP/2 多路复用,数百个请求不再是瓶颈 |
|
||||||
|
|
||||||
|
**三个条件同时满足——浏览器 ESM 支持、npm 生态转换能力、HTTP/2 普及——Bundleless 才真正可行。**
|
||||||
|
|
||||||
|
### 14.3 Bundleless 的架构优势
|
||||||
|
|
||||||
|
**① 按需编译,冷启动 O(1)**
|
||||||
|
|
||||||
|
传统打包:启动时间 ∝ 项目模块数。Bundleless:只编译浏览器当前请求的文件,启动几乎恒定。
|
||||||
|
|
||||||
|
**② HMR 粒度天然匹配 ESM 边界**
|
||||||
|
|
||||||
|
每个文件就是一个 ESM 模块,热更新不需要重新打包 bundle,只需要让浏览器重新 import 变更的那个文件。
|
||||||
|
|
||||||
|
**③ 开发与生产的明确分工**
|
||||||
|
|
||||||
|
```
|
||||||
|
开发阶段(Bundleless) 生产阶段(Bundled)
|
||||||
|
───────────────────── ─────────────────────
|
||||||
|
不打包,按需编译 Rollup 全量打包
|
||||||
|
esbuild 做预构建 Tree-shaking
|
||||||
|
ESM 原生加载 代码分割 + 压缩
|
||||||
|
追求开发体验 追求加载性能
|
||||||
|
```
|
||||||
|
|
||||||
|
这两个阶段的目标本就不同:开发要快,生产要小。Bundleless 把这个分工理清了。
|
||||||
|
|
||||||
|
### 14.4 Bundleless 的工程代价
|
||||||
|
|
||||||
|
从架构角度,不是所有场景都适合 Bundleless:
|
||||||
|
|
||||||
|
| 场景 | Bundleless 是否适用 | 原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| 现代浏览器开发 | ✅ 完美 | ESM + HTTP/2 |
|
||||||
|
| 需要兼容 IE | ❌ 不行 | IE 不支持 ESM |
|
||||||
|
| 巨型 node_modules | ⚠️ 需预构建 | 否则请求数爆炸 |
|
||||||
|
| 非 JS 资源(CSS/图片) | ⚠️ 需额外处理 | ESM 只管 JS |
|
||||||
|
|
||||||
|
这也是为什么 Vite 不是纯 Bundleless——它用预构建和 Rollup 生产打包弥补了 Bundleless 的短板。**务实的设计比纯粹的理念更重要。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展思考:如果你来设计一个类似的工具,会怎么做?
|
||||||
|
|
||||||
|
这通常是大厂面试的进阶追问。可以从以下角度组织回答:
|
||||||
|
|
||||||
|
1. **利用浏览器原生能力**:现代浏览器已经支持 ESM、ES2020+ 语法,开发阶段不需要向下兼容。
|
||||||
|
2. **语言选择**:高频编译用 Go/Rust 实现的工具(如 esbuild、SWC),生产打包用成熟的 JS 生态工具。
|
||||||
|
3. **按需编译**:改变「先打包再启动」的思路,变成「请求时编译」。
|
||||||
|
4. **插件兼容**:设计插件接口时尽量对齐已有标准(如 Rollup 插件格式),降低迁移成本。
|
||||||
533
plugins/plugin-hooks.js
Normal file
533
plugins/plugin-hooks.js
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
/**
|
||||||
|
* ============================================================
|
||||||
|
* Rollup / Vite 插件钩子函数 完整参考手册
|
||||||
|
* ============================================================
|
||||||
|
*
|
||||||
|
* 钩子分为两大阶段:
|
||||||
|
* 1. Build 阶段 — 解析模块、构建依赖图
|
||||||
|
* 2. Output Generate 阶段 — 生成最终产物(chunk/bundle)
|
||||||
|
*
|
||||||
|
* 钩子类型:
|
||||||
|
* - async : 可返回 Promise,等待 resolve 后继续
|
||||||
|
* - sync : 同步执行
|
||||||
|
* - first : 多个插件有该钩子时,按顺序执行,第一个返回非 null 值的生效
|
||||||
|
* - sequential : 多个插件按顺序依次执行
|
||||||
|
* - parallel : 多个插件并行执行
|
||||||
|
*
|
||||||
|
* 以下是按 Rollup → Vite 分类的全部钩子。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 一、Rollup 通用钩子(Build 阶段)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export default function pluginHooksDemo() {
|
||||||
|
return {
|
||||||
|
name: 'plugin-hooks-demo',
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 1. options
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: 读取完用户传入的 rollup/vite 配置后,开始构建之前
|
||||||
|
// 用途: 替换或操纵传给 rollup 的 options 对象
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
options(rawOptions) {
|
||||||
|
// rawOptions — 用户的原始配置对象
|
||||||
|
// 返回 null/undefined 不做修改
|
||||||
|
// 返回一个 options 对象来覆盖(会合并到现有 options)
|
||||||
|
console.log('[options] 原始配置:', Object.keys(rawOptions))
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 2. buildStart
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 构建开始时
|
||||||
|
// 用途: 构建前的初始化、创建输出目录、注册构建级变量等
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
buildStart(options) {
|
||||||
|
// options — 最终合并后的配置对象
|
||||||
|
console.log('[buildStart] 构建开始, input:', options.input)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 3. resolveId
|
||||||
|
// 类型: async, first
|
||||||
|
// 触发时机: 解析模块导入路径时(对每个 import 都会调用)
|
||||||
|
// 用途: 自定义模块解析逻辑,例如 alias、虚拟模块
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
resolveId(source, importer, options) {
|
||||||
|
// source — import 语句中的原始路径,如 './utils' 或 'vue'
|
||||||
|
// importer — 发起导入的模块的绝对路径(入口模块为 undefined)
|
||||||
|
// options — { attributes, custom, isEntry }
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 交给下一个插件的 resolveId
|
||||||
|
// string — 解析后的模块绝对路径
|
||||||
|
// { id } — 对象形式,id 为解析路径
|
||||||
|
// false — 标记为 external(不打包)
|
||||||
|
console.log('[resolveId]', source, '←', importer)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 4. load
|
||||||
|
// 类型: async, first
|
||||||
|
// 触发时机: 加载模块源码内容时(resolveId 之后)
|
||||||
|
// 用途: 返回模块内容,用于虚拟模块、加载非 JS 文件等
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
load(id) {
|
||||||
|
// id — 模块的绝对路径
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 交给下一个插件
|
||||||
|
// string — 模块源代码
|
||||||
|
// { code, map, ... } — 带 sourcemap 等
|
||||||
|
console.log('[load] 加载模块:', id)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 5. transform
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: 拿到模块源码后,AST 解析之前
|
||||||
|
// 用途: 对单个模块源码做转换(最常见的钩子之一)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
transform(code, id) {
|
||||||
|
// code — 模块源代码(前置插件 transform 后的结果)
|
||||||
|
// id — 模块的绝对路径
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 不做转换
|
||||||
|
// string — 转换后的代码
|
||||||
|
// { code, map, ... } — 带 sourcemap
|
||||||
|
// { code, ast, ... } — 直接提供 AST(跳过解析)
|
||||||
|
console.log('[transform] 转换模块:', id)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 6. moduleParsed
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 模块 AST 解析完成(所有 transform 之后)
|
||||||
|
// 用途: 读取模块信息,如 import/export 列表
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
moduleParsed(moduleInfo) {
|
||||||
|
// moduleInfo 主要属性:
|
||||||
|
// id — 模块路径
|
||||||
|
// code — 模块源码
|
||||||
|
// ast — ESTree 语法的 AST 树
|
||||||
|
// importedIds — 静态 import 的模块 id 数组
|
||||||
|
// dynamicallyImportedIds — 动态 import
|
||||||
|
// importedIdResolutions — 静态 import 解析详情
|
||||||
|
// dynamicallyImportedIdResolutions — 动态 import 解析详情
|
||||||
|
// importers — 导入了本模块的模块 id 列表
|
||||||
|
// dynamicImporters — 动态导入了本模块的模块 id 列表
|
||||||
|
// isEntry — 是否为入口
|
||||||
|
// isExternal — 是否为 external
|
||||||
|
// meta — 插件间共享的自定义元数据对象
|
||||||
|
// assertions/attributes — import assertions
|
||||||
|
console.log('[moduleParsed] 解析完成:', moduleInfo.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 7. resolveDynamicImport
|
||||||
|
// 类型: async, first
|
||||||
|
// 触发时机: 解析动态 import() 时
|
||||||
|
// 用途: 自定义动态导入的解析逻辑
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
resolveDynamicImport(specifier, importer) {
|
||||||
|
// specifier — import(xxx) 中的 xxx
|
||||||
|
// importer — 发起动态导入的模块路径
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 交给下一个插件
|
||||||
|
// { id, external }
|
||||||
|
console.log('[resolveDynamicImport]', specifier, '←', importer)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 8. buildEnd
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 构建结束(无论成功/失败)时
|
||||||
|
// 用途: 清理资源、关闭文件句柄、输出统计信息
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
buildEnd(error) {
|
||||||
|
// error — 如果构建失败则传入 Error 对象,成功则为 null
|
||||||
|
if (error) {
|
||||||
|
console.log('[buildEnd] 构建失败:', error.message)
|
||||||
|
} else {
|
||||||
|
console.log('[buildEnd] 构建结束')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 9. watchChange [Rollup watch 模式]
|
||||||
|
// 类型: sync, sequential
|
||||||
|
// 触发时机: rollup --watch 时文件发生变化
|
||||||
|
// 用途: 监控文件变化,根据变更决定是否重新构建
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
watchChange(id, change) {
|
||||||
|
// id — 变化文件的绝对路径
|
||||||
|
// change — { event: 'create' | 'update' | 'delete' }
|
||||||
|
console.log('[watchChange]', change.event, ':', id)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 10. closeWatcher [Rollup watch 模式]
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: watch 进程关闭时
|
||||||
|
// 用途: 清理 watcher 相关资源
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
closeWatcher() {
|
||||||
|
console.log('[closeWatcher] watcher 关闭')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 二、Rollup Output Generation 阶段钩子
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 11. outputOptions
|
||||||
|
// 类型: sync, sequential
|
||||||
|
// 触发时机: 获取 outputOptions 时
|
||||||
|
// 用途: 替换或操纵输出选项
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
outputOptions(outputOptions) {
|
||||||
|
// outputOptions — 输出配置对象
|
||||||
|
console.log('[outputOptions] 输出配置:', Object.keys(outputOptions))
|
||||||
|
return null // 返回新对象则替换
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 12. renderStart
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 每次调用 bundle.generate() 或 bundle.write() 时
|
||||||
|
// 用途: 输出阶段的初始化
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
renderStart(outputOptions, inputOptions) {
|
||||||
|
console.log('[renderStart] 开始生成产物')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 13. banner
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 每个 chunk 生成前
|
||||||
|
// 用途: 在 chunk 最前面插入内容(如版权声明)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
banner(chunk) {
|
||||||
|
// chunk — 当前 chunk 信息对象
|
||||||
|
// 返回 string 或 Promise<string>
|
||||||
|
// console.log('[banner] chunk:', chunk.fileName)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 14. footer
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 每个 chunk 生成前
|
||||||
|
// 用途: 在 chunk 最后面追加内容
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
footer(chunk) {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 15. intro
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 每个 chunk 生成前
|
||||||
|
// 用途: 在模块打包代码最前面插入内容(banner 之后、wrapper 内部)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
intro(chunk) {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 16. outro
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: 每个 chunk 生成前
|
||||||
|
// 用途: 在模块打包代码最后面追加内容(footer 之前、wrapper 内部)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
outro(chunk) {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 17. renderChunk
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: 每个 chunk 的代码生成时
|
||||||
|
// 用途: 对 chunk 代码做最终转换(可返回 sourcemap)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
renderChunk(code, chunk, outputOptions) {
|
||||||
|
// code — chunk 的完整代码
|
||||||
|
// chunk — chunk 信息对象
|
||||||
|
// outputOptions — 输出配置
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 不修改
|
||||||
|
// string — 修改后代码
|
||||||
|
// { code, map }
|
||||||
|
console.log('[renderChunk]', chunk.fileName)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 18. augmentChunkHash
|
||||||
|
// 类型: sync, sequential
|
||||||
|
// 触发时机: 计算 chunk hash 时,用于追加自定义信息
|
||||||
|
// 用途: 让 chunk hash 也能反映插件自定义内容的变化
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
augmentChunkHash(chunkInfo) {
|
||||||
|
// 返回一个字符串,会被追加到 hash 输入中
|
||||||
|
// 常用于:[hash] 文件名需要反映插件版本等场景
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 19. renderDynamicImport
|
||||||
|
// 类型: sync, first
|
||||||
|
// 触发时机: 渲染动态 import 表达式时
|
||||||
|
// 用途: 自定义动态 import 的运行时行为
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
renderDynamicImport({ format, moduleId, targetModuleId, customResolution }) {
|
||||||
|
// format — 输出格式 (es / cjs / …)
|
||||||
|
// moduleId — 发起导入的模块 id
|
||||||
|
// targetModuleId — 被导入模块 id
|
||||||
|
// customResolution — resolveDynamicImport 返回的 custom 值
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// null — 默认渲染
|
||||||
|
// { left, right } — 自定义 import 表达式的左/右半部分
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 20. resolveFileUrl
|
||||||
|
// 类型: sync, first
|
||||||
|
// 触发时机: 渲染文件引用 URL 时(如 import.meta.ROLLUP_FILE_URL_reference_xxx)
|
||||||
|
// 用途: 自定义文件 URL 解析
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
resolveFileUrl({ chunkId, fileName, format, moduleId, referenceId, relativePath }) {
|
||||||
|
// 返回 null 使用默认,或返回自定义 URL 字符串
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 21. resolveImportMeta
|
||||||
|
// 类型: sync, first
|
||||||
|
// 触发时机: 渲染 import.meta 属性时
|
||||||
|
// 用途: 自定义 import.meta.xxx 的展开逻辑
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
resolveImportMeta(property, { chunkId, moduleId, format }) {
|
||||||
|
// property — 访问的属性名,如 'url'
|
||||||
|
// 返回:
|
||||||
|
// null — 交给下一个插件
|
||||||
|
// string — 替换为指定字符串
|
||||||
|
// false — 保持 import.meta.property 不变
|
||||||
|
console.log('[resolveImportMeta]', property, 'in', moduleId)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 22. generateBundle
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: bundle 生成完毕、写入磁盘之前
|
||||||
|
// 用途: 增、删、改最终产物(最常用的输出钩子之一)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
generateBundle(options, bundle, isWrite) {
|
||||||
|
// options — 输出配置
|
||||||
|
// bundle — { [fileName]: AssetInfo | ChunkInfo }
|
||||||
|
// isWrite — bundle.write() 为 true, bundle.generate() 为 false
|
||||||
|
//
|
||||||
|
// AssetInfo: { type: 'asset', fileName, source, name, needsCodeReference, ... }
|
||||||
|
// ChunkInfo: { type: 'chunk', fileName, code, map, modules, exports, facadeModuleId, isEntry, ... }
|
||||||
|
console.log('[generateBundle] 产物数:', Object.keys(bundle).length)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 23. writeBundle
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: bundle 写入磁盘后
|
||||||
|
// 用途: 产物写盘后的后处理(上传 CDN、生成报告等)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
writeBundle(options, bundle) {
|
||||||
|
console.log('[writeBundle] 产物已写入磁盘')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 24. closeBundle
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: bundle 生成完成后(最后的清理钩子)
|
||||||
|
// 用途: 收尾清理
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
closeBundle() {
|
||||||
|
console.log('[closeBundle] Bundle 流程结束,清理资源')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 三、Vite 独有钩子
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 25. config
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: 解析 Vite 用户配置之前
|
||||||
|
// 用途: 修改/扩展用户配置,或返回部分配置与已有配置合并
|
||||||
|
// 这是 Vite 插件最常用的入口钩子
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
config(config, env) {
|
||||||
|
// config — 用户传入的 vite 配置(未解析)
|
||||||
|
// env — { mode: 'development'|'production', command: 'serve'|'build', ssrBuild }
|
||||||
|
//
|
||||||
|
// 返回 null 或 部分配置对象(会被 deep-merge 到用户配置)
|
||||||
|
console.log('[config] 模式:', env.mode, '命令:', env.command)
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 26. configResolved
|
||||||
|
// 类型: async, parallel
|
||||||
|
// 触发时机: Vite 配置解析完毕后
|
||||||
|
// 用途: 读取最终配置,做插件内部初始化(常在此缓存 resolvedConfig)
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
configResolved(resolvedConfig) {
|
||||||
|
// resolvedConfig — 完整解析后的 Vite 配置(只读参考)
|
||||||
|
console.log('[configResolved] root:', resolvedConfig.root)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 27. configureServer
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: Vite 开发服务器创建时
|
||||||
|
// 用途: 注册自定义中间件、WebSocket 事件等
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
configureServer(viteDevServer) {
|
||||||
|
// viteDevServer 主要属性/方法:
|
||||||
|
// middlewares — Connect 实例,可用 .use() 添加中间件
|
||||||
|
// httpServer — Node http.Server 实例
|
||||||
|
// ws — WebSocket 服务器
|
||||||
|
// watcher — chokidar 文件监听器
|
||||||
|
// moduleGraph — 模块图
|
||||||
|
// config — 最终配置
|
||||||
|
// listen() — 启动服务器
|
||||||
|
// close() — 关闭服务器
|
||||||
|
// printUrls() — 打印服务地址
|
||||||
|
// transformIndexHtml() — 转换 HTML
|
||||||
|
// ssrFixStacktrace() — SSR 堆栈修复
|
||||||
|
// ssrLoadModule() — SSR 加载模块
|
||||||
|
// ssrTransform() — SSR 转换
|
||||||
|
viteDevServer.middlewares.use((req, res, next) => {
|
||||||
|
// 在这里可以拦截请求,自定义响应
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
console.log('[configureServer] 开发服务器已创建')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 28. configurePreviewServer
|
||||||
|
// 类型: async, sequential
|
||||||
|
// 触发时机: vite preview 命令创建预览服务器时
|
||||||
|
// 用途: 类似 configureServer,针对预览服务器
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
configurePreviewServer(previewServer) {
|
||||||
|
// previewServer 接口与 viteDevServer 类似
|
||||||
|
previewServer.middlewares.use((req, res, next) => {
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
console.log('[configurePreviewServer] 预览服务器已创建')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 29. transformIndexHtml
|
||||||
|
// 类型: async, sequential / order: 'pre' | 'post' | undefined
|
||||||
|
// 触发时机: 转换 index.html 时
|
||||||
|
// 用途: 注入 <script> / <link> 标签、修改 HTML 内容
|
||||||
|
// 这是 Vite 插件最常用的输出钩子之一
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
transformIndexHtml: {
|
||||||
|
// order: 'pre' — 在其他插件之前执行
|
||||||
|
// order: 'post' — 在其他插件之后执行
|
||||||
|
// 不设 order — 按插件注册顺序
|
||||||
|
order: 'pre',
|
||||||
|
handler(html, ctx) {
|
||||||
|
// html — index.html 文本内容
|
||||||
|
// ctx — 上下文对象
|
||||||
|
// ctx.path — 请求路径
|
||||||
|
// ctx.filename — HTML 文件路径
|
||||||
|
// ctx.server — ViteDevServer 实例
|
||||||
|
// ctx.bundle — 构建产物 bundle(serve 命令下可能为 undefined)
|
||||||
|
// ctx.chunk — 当前 HTML 对应的 chunk
|
||||||
|
// ctx.originalUrl — 原始请求 URL
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// string — 转换后的 HTML
|
||||||
|
// { html: string, tags: [...] } — HTML + 注入标签
|
||||||
|
// { tags: [...] } — 仅注入标签
|
||||||
|
// Tag 格式:
|
||||||
|
// { tag: 'script', attrs: { src: '/foo.js' }, injectTo: 'head'|'body'|'head-prepend'|'body-prepend' }
|
||||||
|
// { tag: 'link', attrs: { rel: 'stylesheet', href: '/foo.css' }, injectTo: 'head' }
|
||||||
|
// { tag: 'meta', attrs: { name: 'viewport', content: 'width=device-width' }, injectTo: 'head' }
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// 30. handleHotUpdate
|
||||||
|
// 类型: async, sequential / order: 'pre' | 'post' | undefined
|
||||||
|
// 触发时机: HMR 热更新时文件变化
|
||||||
|
// 用途: 自定义 HMR 更新行为:过滤哪些变更触发热更新、
|
||||||
|
// 执行自定义 HMR 逻辑而非默认重载页面
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
handleHotUpdate(ctx) {
|
||||||
|
// ctx 上下文对象:
|
||||||
|
// ctx.file — 变更的文件路径
|
||||||
|
// ctx.modules — 受变更影响的模块数组
|
||||||
|
// ctx.read() — 读取变更后文件内容
|
||||||
|
// ctx.server — ViteDevServer 实例
|
||||||
|
// ctx.timestamp — 变更时间戳
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// ModuleNode[] — 自定义受影响模块列表
|
||||||
|
// void — 默认行为,使用 ctx.modules
|
||||||
|
console.log('[handleHotUpdate] 文件变更:', ctx.file)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 四、已废弃的 Rollup 钩子(兼容旧插件,不推荐使用)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// transformBundle(code, options) → 请用 generateBundle 替代
|
||||||
|
// transformChunk(code, options, chunk) → 请用 renderChunk 替代
|
||||||
|
// ongenerate(options, bundle) → 请用 generateBundle 替代
|
||||||
|
// onwrite(options, bundle) → 请用 writeBundle 替代
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 附:钩子执行顺序总览
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// 【Build 阶段】
|
||||||
|
// options → buildStart → (每个入口模块并行)
|
||||||
|
// resolveId → load → transform → moduleParsed
|
||||||
|
// → resolveDynamicImport(遇到动态 import 时)
|
||||||
|
// → buildEnd
|
||||||
|
//
|
||||||
|
// 【Output Generate 阶段】
|
||||||
|
// outputOptions → renderStart → (每个 chunk 顺序)
|
||||||
|
// banner → footer → intro → outro
|
||||||
|
// → renderChunk → augmentChunkHash
|
||||||
|
// → renderDynamicImport → resolveFileUrl → resolveImportMeta
|
||||||
|
// → generateBundle → writeBundle → closeBundle
|
||||||
|
//
|
||||||
|
// 【Watch 模式】
|
||||||
|
// watchChange (文件变化时)
|
||||||
|
// closeWatcher (退出时)
|
||||||
|
//
|
||||||
|
// 【Vite 独有】
|
||||||
|
// config → configResolved
|
||||||
|
// → configureServer → configurePreviewServer
|
||||||
|
// → transformIndexHtml
|
||||||
|
// → handleHotUpdate
|
||||||
|
// ============================================================
|
||||||
17
plugins/plugin-noconsole.js
Normal file
17
plugins/plugin-noconsole.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default () => {
|
||||||
|
return {
|
||||||
|
name: 'helloPlugin',
|
||||||
|
transform(src, id) {
|
||||||
|
// 只处理 src 目录下的文件,跳过 node_modules
|
||||||
|
if (!id.includes('/src/')) return null
|
||||||
|
|
||||||
|
// 生产环境:自动移除 console.log
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const result = src.replace(/console\.log\([^)]*\);?/g, '')
|
||||||
|
return {code: result, map: null}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
[permissions]
|
[permissions]
|
||||||
allow = ["Edit"]
|
allow = ["Edit", "Bash(rm docs/plugin-hooks-interview.md:*)", "Bash(where tesseract 2>nul || where powershell 2>nul)", "run_skill"]
|
||||||
|
|||||||
@@ -1,30 +1,16 @@
|
|||||||
import {defineConfig} from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import noConsole from './plugins/plugin-noconsole'
|
||||||
function helloPlugin() {
|
function helloPlugin() {
|
||||||
console.log("hello Plugin")
|
console.log("hello Plugin")
|
||||||
return {
|
|
||||||
name: 'helloPlugin',
|
|
||||||
transform(src, id) {
|
|
||||||
// 只处理 src 目录下的文件,跳过 node_modules
|
|
||||||
if (!id.includes('/src/')) return null
|
|
||||||
|
|
||||||
// 生产环境:自动移除 console.log
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
const result = src.replace(/console\.log\([^)]*\);?/g, '')
|
|
||||||
return {code: result, map: null}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
helloPlugin()
|
// noConsole()
|
||||||
],
|
],
|
||||||
clear:true
|
clear:true
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user