vite面试题提交

This commit is contained in:
2026-06-26 17:56:46 +08:00
parent 40a1a2248b
commit 82c0f31cae
7 changed files with 1176 additions and 19 deletions

View File

@@ -1,3 +1,5 @@
{
"topic_20260626-084849_ae13b749f70ea556": "auto"
"topic_20260626-084849_ae13b749f70ea556": "auto",
"topic_20260626-085627_e3928116af40710e": "auto",
"topic_20260626-093001_b040128cfc0f086c": "auto"
}

View File

@@ -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
View 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 | 随项目规模递减 | 几乎恒定 |
| 预构建 | 无 | esbuildGo |
| 生产打包 | 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 serverESM 按需编译 + 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
View 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 — 构建产物 bundleserve 命令下可能为 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
// ============================================================

View 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
}
}
}

View File

@@ -1,2 +1,2 @@
[permissions]
allow = ["Edit"]
allow = ["Edit", "Bash(rm docs/plugin-hooks-interview.md:*)", "Bash(where tesseract 2>nul || where powershell 2>nul)", "run_skill"]

View File

@@ -1,30 +1,16 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import noConsole from './plugins/plugin-noconsole'
function helloPlugin() {
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/
export default defineConfig({
plugins: [
vue(),
helloPlugin()
// noConsole()
],
clear:true
})