vite面试题提交
This commit is contained in:
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 插件格式),降低迁移成本。
|
||||
Reference in New Issue
Block a user