从零手写mini-vite

从零开始手写 mini-vite

写在前面 :Vite 凭借其极速的冷启动和热更新,正在成为现代前端构建工具的新宠。本文将从零手写一个 mini-vite,旨在理解 Vite 的核心原理:利用浏览器原生 ES 模块(ESM)实现按需编译,通过 WebSocket 实现热更新。代码已上传至 GitHub,欢迎查阅交流:my-mini-vite。

一、Vite 核心流程

  1. 读取命令(npm run dev / build)

  2. 加载配置文件(vite.config.js)

  3. 合并默认配置

  4. 启动 dev:

    • 创建缓存目录
    • 依赖预构建(esbuild)
    • 启动 HTTP 服务器
    • 启动 HMR 热更新
  5. 处理请求:

    • 返回 HTML
    • 返回静态资源
    • 返回预构建依赖
    • 重写 import 路径
  6. 启动 build:

  • Rollup 打包
  • 输出 dist

二、手写mini-vite

1. 准备工作

bash 复制代码
# 项目结构
mini-vite/
├── node_modules/
├── src/
│   ├── a.js
│   ├── b.js
│   └── main.js
├── hmr-client.js   # 浏览器端 HMR 客户端
├── index.html
├── mini-vite.js
├── package-lock.json
├── package.json
└── vite.config.js    # 配置文件(可选)       
npm init -y
npm install esbuild chokidar ws rollup @rollup/plugin-node-resolve @rollup/plugin-terser
  1. vite.config.js
js 复制代码
// 项目的配置文件
export const userConfig = {
  port: 5017,
  root: './src',
  plugins: [],
  build: { outDir: './dist', sourcemap: true },
}
  1. index.html

注意:这里的 index.html 是初始版本,后续 HMR 章节中会添加 hmr-client.js 的引入,并将路径改为 /main.js(由 dev server 解析到 src 目录)

html 复制代码
<!DOCTYPE html>
<script type="module" src="/main.js"></script>
  1. a.js
js 复制代码
import { b } from './b.js'
export const a = '我是业务a模块 → ' + b
  1. b.js
js 复制代码
export const b = '我是业务b模块'
  1. main.js
js 复制代码
import { a } from './a.js'
console.log(a)

2. 加载并合并配置

js 复制代码
import path from 'path'
import { userConfig } from './vite.config.js'

// 【1】:加载并合并配置文件
function loadConfig() {
  // 提供默认配置
  const defaultConfig = {
    port: 3000,
    root: process.cwd(),
    cacheDir: 'node_modules/.mini-vite' // 依赖缓存目录
  }
  const config = { ...defaultConfig, ...userConfig } // 合并配置

  // 相对路径转绝对路径
  config.root = path.resolve(process.cwd(), config.root)
  config.build.outDir = path.resolve(process.cwd(), config.build.outDir)
  config.cacheDir = path.resolve(process.cwd(), config.cacheDir)

  return config
}

// 测试一下
console.log(loadConfig())

运行 命令:node .\mini-vite.js

bash 复制代码
# 控制台输出
{
  port: 5017,
  root: 'D:\\Code\\AllCode\\手写mini-vite\\mini-vite\\src',
  cacheDir: 'D:\\Code\\AllCode\\手写mini-vite\\mini-vite\\node_modules\\.mini-vite',
  plugins: [],
  build: {
    outDir: 'D:\\Code\\AllCode\\手写mini-vite\\mini-vite\\dist',
    sourcemap: true
  }
}

3. 创建缓存目录

js 复制代码
import fs from 'fs'

function prepareDir(config) {
  fs.mkdirSync(config.cacheDir, { recursive: true })
  console.log('依赖缓存目录:',config.cacheDir);
}

// 测试一下:
const config = loadConfig()
prepareDir(config) 

此时node_modules文件夹下将会多出一个文件.mini-vite,用于缓存依赖包

4. 依赖预构建

为什么需要依赖预构建?

js 复制代码
// 假设你在代码中写了:
import { debounce } from 'lodash-es';
// 浏览器需要向vite server请求lodash这个模块
  1. 请求太多 :lodash-es 有 300 多个小文件,浏览器要发 300 个请求,卡死。
  2. 格式不兼容 :很多包(如 React)是 CommonJS 格式(require),浏览器不认识。

预构建把 300 个文件合并成 1 个,把 CommonJS 转成 ESM,浏览器一次请求就搞定。预构建用到了esbuild ,它是一个极快的打包器 (用 Go 写的,比 JS 打包器快 10-100 倍),它把300 个文件合并成 1 个,并放到依赖缓存目录 node_modules/.vite/deps/中,当浏览器请求的时候,直接发一个文件就行

js 复制代码
import esbuild from 'esbuild'

// 【3】:依赖预构建
async function preBundle(config) {
  const pkgPath = path.join(config.root, '../package.json')
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
  const deps = Object.keys(pkg.dependencies || {})

  for (const dep of deps) {
    await esbuild.build({
      entryPoints: [dep],
      bundle: true,
      format: 'esm',
      platform: 'node',
      outfile: path.join(config.cacheDir, dep + '.js')
    })
  }
  console.log(`\x1b[36;1m${deps.length}/${deps.length} 个依赖预构建完成\x1b[0m`);
}

// 测试一下
const config = loadConfig()
prepareDir(config)
preBundle(config)

可以看到,在node_modules文件夹下多出来一个.mini-vite,里面就是打包并处理成ES6的第三方依赖包

5. import 路径重写

为啥需要import路径重写?

在上一步中,我们完成了依赖预构建,将第三方模块打包到了缓存目录。但浏览器在解析 import 语句时,只能识别相对路径、绝对路径或 URL,而无法直接处理裸模块 ------比如 import { debounce } from 'lodash-es' 中的 'lodash-es'。如果没有重写,浏览器会向vite服务器请求 http://localhost:5173/lodash-es,这个路径显然不存在,导致 404 错误。

Vite 的做法是:拦截所有裸模块导入,将其重写为以 /@modules/ 开头的特殊路径 ,然后在服务器端将 /@modules/xxx 映射到预构建好的依赖文件上。

js 复制代码
// 【4】:import 路径重写
function rewriteImports(code) {
  // 匹配 import导入
  const importRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
  return code.replace(importRegex, (match, importPath) => {
    // 判断是否是裸模块:不以 . 或 / 或 http 开头
    const isBareModule = !importPath.startsWith('.') &&
                         !importPath.startsWith('/') &&
                         !importPath.startsWith('http');
    if (isBareModule) {
      // 将 lodash-es 替换成 /@modules/lodash-es.js
      const newPath = `/@modules/${importPath}.js`;
      return match.replace(`'${importPath}'`, `'${newPath}'`).replace(`"${importPath}"`, `"${newPath}"`);
    }
    return match;
  });
}

// 测试一下
const code = `
  import lodash from 'lodash';
  import { createApp } from 'vue'
`;
console.log(rewriteImports(code));
# 控制台输出:
import lodash from '/@modules/lodash.js';
import { createApp } from '/@modules/vue.js'

注意:这个简化版正则无法处理动态导入 import() 和复杂的重导出,但足以说明核心原理。完整的 Vite 会使用更严谨的 AST 解析(如 es-module-lexer)。

6. 请求处理

handleRequest 的作用

根据 URL 返回不同内容:HTML、HMR 客户端、预构建依赖(/@modules/)、业务 JS(并重写 import)。

Vite 源码真实处理

不是用一个函数硬编码,而是用 中间件链 (基于 connect 框架),每个中间件负责一件事(如 CORS、静态文件、源码转换),插件可插入自定义中间件。

js 复制代码
function handleRequest(req, res, config) {
  const url = req.url.split('?')[0] // 去掉查询参数(HMR 会加 ?t=xxx 绕过缓存)

  // 1. HTML:直接读取 index.html文件内容,然后相应给服务器
  if (url === '/') {
    /**
     *                          HMR热更新原理
     * 【注意】:在启动服务器的时候(写在后续),vite会在本地启动一个8080服务器,
     * 浏览器访问 http://localhost:5017/ 的时候,直接请求 index.html
     * 在这个index.html中,使用script将main.js和hmr-client.js 以 ES 模块引入
     * 在hmr-client.js内部,将会开启一个websocket并连接到本地vite服务器
     * 后续,本地代码有更新的时候,vite将通过chokidar监听到变化,
     * 然后vite服务器将变更清单通过socket推送给浏览器,浏览器重新请求变更模块的代码
     */
    const html = fs.readFileSync(path.join(process.cwd(), 'index.html'), 'utf-8')
    res.writeHead(200, { 'Content-Type': 'text/html' })
    return res.end(html)
  }

  // 2. HMR 客户端
  if (url === '/hmr-client.js') {
    const js = fs.readFileSync(path.join(config.root, '../hmr-client.js'), 'utf8')
    res.writeHead(200, { 'Content-Type': 'application/javascript' })
    return res.end(js)
  }

  // 3. 预构建依赖
  if (url.startsWith('/@modules/')) {
    const name = url.replace('/@modules/', '')
    const code = fs.readFileSync(path.join(config.cacheDir, name), 'utf8')
    res.writeHead(200, { 'Content-Type': 'application/javascript' })
    return res.end(code)
  }

  // 4. 业务 JS
  if (url.endsWith('.js')) {
    const code = fs.readFileSync(path.join(config.root, url.slice(1)), 'utf8')
    const transformed = rewriteImports(code)

    res.writeHead(200, { 'Content-Type': 'application/javascript' })
    return res.end(transformed)
  }

  res.writeHead(404)
  res.end('Not found')
}
// 测试一下
const config = loadConfig() // 加载配置
prepareDir(config) // 准备缓存文件
await preBundle(config) // 预构建模块
// 模拟请求函数
function mockReq(url) {
  const req = { url }, res = {}
  res.writeHead = (c, h) => (res.status = c, res.type = h?.['Content-Type'])
  res.end = d => (res.body = d)
  handleRequest(req, res, config)
  return res
}

// 模拟浏览器向vite服务器动态请求资源
console.log('(1)请求html:',mockReq('/').body,'\n\n')
console.log('(2)请求预构建模块:\n',mockReq('/@modules/chokidar.js').body.slice(0, 100) + '...查看更多') // 这个包太多了,只打印前面一点点
# 控制台输出:
6/6 个依赖预构建完成
(1)请求html: 
      <!DOCTYPE html>
      <script type="module" src="/main.js"></script>
      <script type="module" src="/hmr-client.js"></script>

(2)请求预构建模块:
 // node_modules/chokidar/index.js
import { EventEmitter } from "node:events";
import { stat as statc...查看更多

7. HMR热更新

HMR 本质:基于浏览器原生 ESM + 模块依赖图谱,实现单文件级精准热替换,全程不打包、不刷新、最小范围更新。


两端角色

  • 服务端(Vite Dev Server):文件监听 → 维护模块依赖图 → 计算更新边界 → 推送更新指令
  • 客户端(HMR Runtime):接收消息 → 拉取最新模块 → 执行模块替换 → 处理 accept / 降级刷新

  1. vite服务端:
js 复制代码
function startHMR(config) {
  const wss = new WebSocketServer({ port: 8080 })
  console.log('\x1b[36;1m[HMR] WebSocket 服务已启动,端口: 8080\x1b[0m')

  const watcher = chokidar.watch(config.root, { persistent: true })
  console.log(`\x1b[36;1m[HMR] 正在监听文件变化: ${config.root}\x1b[0m`)

  watcher.on('change', (file) => {
    const rel = '/' + path.relative(config.root, file).replace(/\\/g, '/')
    console.log(`\x1b[33m[HMR] 检测到文件变化: ${rel}\x1b[0m`)
    const msg = JSON.stringify({ type: 'update', path: rel })
    console.log(`\x1b[33m[HMR] 推送消息: ${msg}\x1b[0m`)
    wss.clients.forEach(ws => ws.send(msg))
  })
}

// // 测试一下
const config = loadConfig()
startHMR(config)

运行 node .\mini-vite.js 后,websocket 和 chokider启动,监听src文件,当我们更改了src下的文件,此时chokider将会监听到文件变化,并通过websocket通知所有客户端拉取变更模块

bash 复制代码
[HMR] WebSocket 服务已启动,端口: 8080
[HMR] 正在监听文件变化: D:\Code\AllCode\手写mini-vite\mini-vite\src
[HMR] 检测到文件变化: /main.js
[HMR] 推送消息: {"type":"update","path":"/main.js"}
[HMR] 检测到文件变化: /a.js
[HMR] 推送消息: {"type":"update","path":"/a.js"}
  1. 客户端(浏览器):
js 复制代码
const ws = new WebSocket('ws://localhost:8080')

ws.onmessage = async (e) => {
  const data = JSON.parse(e.data)
  if (data.type === 'update') {
    console.log('[HMR] ', data.path,'模块变化,即将重新拉取新代码')
    // 动态 import 只重新请求变更的模块,?t= 绕过浏览器缓存
    await import(data.path + '?t=' + Date.now())
    console.log('[HMR] 拉取成功!')
  }
}

console.log('HMR 客户端已连接')

vite 热更新机制的核心步骤

  1. 文件变化监听(chokidar):文件变动 → chokidar 捕获事件 → 进入 handleHMRUpdate 处理。仅监听源码目录,跳过 node_modules/dist 等无关路径。

  2. 定位模块并标记脏模块 :根据文件路径从 ModuleGraph 找到对应 ModuleNode,标记该模块,并清空该模块的缓存编译,此时并不立即编译,等待浏览器请求时再按需编译。

  3. 计算热更新边界 :从变更模块向上遍历 importers 依赖链,遇到 accept() 后停止,否则继续遍历,遇到 decline() → 标记需整页刷新,遍历至入口仍无 accept → 标记需整页刷新。最终输出:更新模块列表、可接受边界、是否刷新。

  4. 生成轻量更新清单(不推送代码):服务端构造更新消息,仅含路径、类型、时间戳,无冗余代码:

json 复制代码
{
  "type": "update",
  "updates": [
    {
      "type": "js-update",
      "path": "/a.js",
      "acceptedPath": "/a.js",
      "timestamp": 1712345678901
    }
  ]
}
  1. WebSocket 推送更新指令 :复用 Dev Server 连接,通过 WebSocket 把更新清单广播给浏览器。区别于webpack的热更新,vite只传指令,不传代码本体,通信极轻量。

  2. 客户端接收并拉取新模块 :浏览器 HMR Runtime 收到消息后,首先解析更新路径,接着带时间戳参数发起 HTTP 请求,强制获取最新模块:

http 复制代码
GET /a.js?t=1712345678901
  1. 模块热替换 :执行旧模块的dispose清除副作用 ,接着用新模块替换旧模块缓存 ,执行 accept 回调,更新客户端模块关系,同步依赖图

  2. 降级策略:遇到配置文件变更、异常、HTML 等非 JS/CSS 文件修改等清空,vite无法热更新此时直接整页刷新

8. 启动开发服务

js 复制代码
import http from 'http'

async function startDev() {
  const config = loadConfig()
  prepareDir(config)
  await preBundle(config)

  const server = http.createServer((req, res) => {
    handleRequest(req, res, config)
  })

  startHMR(config)

  server.listen(config.port, () => {
    console.log(`✅ mini-vite 已启动: http://localhost:${config.port}`)
  })
}

启动 node .\mini-vite.js,并在浏览器中打开http://localhost:5017,此时的请求路径为 ' / ',于是将index.html的内容响应给浏览器。

下面将一步一步的展示整个过程:

  1. 启动服务器:

  2. 浏览器请求 ' / ' ,服务器响应给浏览器index.html

  3. 遇到script标签并且type="module"于是请求 main.js

    这里有一个细节点:我们发现,在请求完main.js 后,浏览器并没有继续请求a.js ,而是直接请求hmr-client.js,这是怎么回事呢?

    回顾 浏览器渲染原理-CSDN博客 的知识:浏览器在解析HTML的时候,为了提高效率,开始解析前,会启动一个 预解析的线程 ,率先下载 HTML中的外部的JS文件 。而type="module" 的脚本默认是 defer 行为,于是预解析线程就会并行请求 main.js 和 hmr-client.js并按顺序返回,当渲染主线程 ,解析到main.js后,发现它 import 了 a.js,于是也去请求 a.js,a.js 又 import 了 b.js...

    所以你看到的网络面板顺序是:

    bash 复制代码
    index.html        ← # 先拿到 HTML
    main.js           ← # 和 hmr-client.js 并行下载
    hmr-client.js     ← # 和 main.js 并行下载
    a.js              ← # 解析 main.js 发现的依赖,并行下载
    b.js              ← # 解析 a.js 发现的依赖,并行下载
  4. 请求 hmr-client热更新客户端脚本

    浏览器执行整个脚本后,将会开启websocket与vite服务器建立连接,这样子,当后续文件有更新的时候,服务器通过websocket给浏览器发送更新清单的时候,浏览器就能够监听到这个清单,然后用 import动态请求清单上的文件,这就是vite的HMR热更新机制的核心原理

  5. 浏览器渲染主线程执行main.js,并按照依赖依次请求 a.js → b.js

  6. 请求完后,依次执行main.js和hmr-client.js:

  7. 当本地某个文件改动

  8. 浏览器的hmr-client脚本收到清单

  9. 浏览器请求清单中的变更模块地址

9. 配置到npm 命令

我们在开发模式下启动vite服务器,运行的是 npm run dev,实际上就是执行的 node .\mini-vite.js,对其官方,我们将node .\mini-vite.js配置到package.json的script中,使得mini-vite也可以使用 npm run dev启动

json 复制代码
// package.json
{
  "name": "mini-vite",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "mini-vite.js",
  "scripts": {
    "dev": "node mini-vite.js",  // 增加这一行命令
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@rollup/plugin-node-resolve": "^16.0.3",
    "@rollup/plugin-terser": "^1.0.0",
    "chokidar": "^5.0.0",
    "esbuild": "^0.28.0",
    "rollup": "^4.60.1",
    "ws": "^8.20.0"
  }
}

在控制台,使用 npm run dev:

三、详解Vite整个启动和热更新过程:

输入 npm run dev 按下回车后,npm 找到 package.json 中的 scripts 字段,执行对应的 dev 指令 ------ 也就是 node mini-vite.js。此时,startDev 函数被调用,Vite正式启动

首先,调用loadConfig 函数,此时Vite 会读取用户项目根目录下的 vite.config.js,合并默认配置。这个过程中会加载并执行配置文件 (可能包含插件、alias、proxy 等自定义配置)。加载完配置后,计算用户源码目录的绝对路径,并同步创建依赖缓存目录,缓存目录将用于存放预构建的依赖 和转换后的模块元数据。

然后,启动prepareDir 函数从项目入口文件开始,对实际使用到的第三方依赖进行预构建 处理,依赖预构建的好处在于可以将loadash这样包含了数百个独立文件的第三方JS库 打包到一个JS文件中以减少浏览器并发请求数量,在打包的过程中还会将其中的CommonJS转成浏览器能识别的ESModule。依赖预构建是基于esbuild 进行的打包的,它是一个go语言开发的高性能打包器,打包完毕之后生成依赖映射表 ,记录每个依赖的哈希值和导出成员信息。当后续 package-lock.json 或依赖版本发生变化时,Vite 会通过比较哈希值来决定是否需要重新预构建,从而避免重复工作。

完成预构建后,Vite 开始创建开发服务器,基于 connect 框架构建中间件链。用于处理 CORS、静态文件、代理、源码转换、HMR WebSocket 等。与此同时,Vite 会在内存中初始化一个依赖图图谱 ,图中每个节点对应一个模块,记录了该模块的相关信息。当浏览器首次请求某个模块时,Vite 会解析该模块的内容,提取所有 import 语句,动态的更新这张依赖图。

服务器启动完成后,开始监听配置文件中指定的端口(例如 5017),此时用户在浏览器中输入 http://localhost:5017,此时浏览器向服务器发起请求。请求路径为 /,服务器收到请求,读取项目根目录下的 index.html 文件,并在其中注入 HMR 客户端脚本 (⭐这个很重要,等会在HMR阶段将会具体讲解)以及其他必要的运行时代码,然后将这份 HTML 返回给浏览器。

浏览器解析 HTML 后,发现 script 标签引用了 ./src/main.js,于是立即向 Vite 服务器发起一个新的请求:/src/main.js。这个请求会被 vite服务器捕获。此时vite将使用 es-module-lexer对main.js进行精确的 AST 解析,并对其依赖进行路径重写 ,将所有裸模块 的路径改写为 /@modules/xxx。后续当浏览器请求 /@modules/xxx 时,Vite 会直接从预构建的缓存目录 中读取对应的文件并返回,无需再次转换。在返回转换后的 main.js 给浏览器之前,Vite 还会更新依赖图谱。

浏览器收到 main.js 后,解析出其中的 import 语句,立即发起对 ./a.js 和 /@modules/lodash-es.js 的请求。对于lodash.js,vite将直接返回缓存目录中的文件,速度极快。对于 ./a.js 的请求流程与 main.js 完全相同。这个过程会递归进行,直到所有依赖模块都加载完毕。最终浏览器执行入口代码,页面完成首次渲染。


其次是HMR阶段

Vite会基于chokidar对项目源码目录Src进行监听。当src/a.js变更后,Vite拿到变更文件的路径,在依赖图谱将该节点标记为"脏模块",并清空其编译缓存,注意,这里并不会立即重新编译,而是等到浏览器真正请求该模块时再按需编译,这样可以避免不必要的计算。

接下来,Vite 从变更模块出发,沿着 importers 链向上遍历 以确定更新边界,遍历过程中,遇到accept后立刻更新当前模块并停止遍历 ,遇到decline立刻强制刷新 ,如果遍历结束仍然没有accept,则说明没有模块愿意接受这次更新,Vite 会降级为整页刷新

确定了边界后,Vite 会构造一个轻量级的更新消息 ,消息中只包含需要更新的模块路径、边界模块路径和时间戳,并不包含任何代码本体。然后通过 WebSocket 将这个 JSON 消息广播 给所有已连接的浏览器客户端

⭐浏览器在最开始请求的index.html中包含了HMR 客户端脚本,浏览器的HMR脚本内部开启了一个websocket并连接到了Vite服务器,当HMR客户端脚本收到Vite服务器发送的轻量级的更新消息 后,从中解析出需要更新的模块路径 ,对每个路径发起一个动态 import() 请求 ,并在 URL 后附加一个 ?t=时间戳 参数,强制绕过浏览器缓存。Vite服务器在收到这个带时间戳的请求后,重新读取并编译该模块(此时才会编译!)然后返回给浏览器

浏览器拿到新的模块代码后,先执行旧模块的dispose方法清理副作用 ,然后HMR客户端脚本会用新模块替换掉缓存中的旧模块 ,最后递归地调用边界模块以及下游依赖的 accept 回调完成模块替换

值得一提的是:如果更新涉及 CSS 文件,Vite 会将 CSS 更新标记为 css-update,浏览器端收到后直接找到对应的 <link> 或 <style> 标签,替换其内容,而不需要执行任何 JavaScript 逻辑,因此 CSS 热更新通常更加简单可靠。

当遇到无法热更新的情况(如 HTML 文件本身变化、vite.config.js 变化、WebSocket 断开、某个模块调用了 decline() 等),Vite 会回退到整页刷新。这种降级策略保证了即使在复杂场景下,开发者的修改也能最终呈现出来。

四、采用rollup打包

js 复制代码
import * as rollup from 'rollup'
// 使用rollup进行打包
async function startBuild() {
  const startTime = Date.now()
  console.log('\x1b[36;1m[build] 正在构建...\x1b[0m')
    
  const config = await loadConfig()
  const { nodeResolve } = await import('@rollup/plugin-node-resolve')
  const { default: terser } = await import('@rollup/plugin-terser')

  // 读取配置
  const input = path.join(config.root, 'main.js')
  const outDir = path.resolve(config.build.outDir)
  const sourcemap = config.build.sourcemap

  // 打包
  const bundle = await rollup.rollup({
    input,
    plugins: [nodeResolve(), terser()]
  })

  await bundle.write({ format: 'esm', file: path.join(outDir, 'index.js'),sourcemap })

  // 输出 HTML
  const html = `
    <!DOCTYPE html>
    <script type="module" src="./index.js"></script>
  `
  fs.writeFileSync(path.join(outDir, 'index.html'), html)

  const cost = Date.now() - startTime
  console.log(`\x1b[32;1m[build] 构建完成\x1b[0m → \x1b[36m${outDir}\x1b[0m (\x1b[33m${cost}ms\x1b[0m)`)
}

顺带的将npm 命令配置了:

json 复制代码
// package.json
{
  "name": "mini-vite",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "mini-vite.js",
  "scripts": {
    "build": "node mini-vite.js build", // 添加这一行
    "dev": "node mini-vite.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@rollup/plugin-node-resolve": "^16.0.3",
    "@rollup/plugin-terser": "^1.0.0",
    "chokidar": "^5.0.0",
    "esbuild": "^0.28.0",
    "rollup": "^4.60.1",
    "ws": "^8.20.0"
  }
}

封装一个入口函数

js 复制代码
function run() {
  const command = process.argv[2] || 'dev'
  if (command === 'build') {
    startBuild()
  } else {
    startDev()
  }
}

run()

测试一下:

五、结尾

核心链路:读取用户配置并合并默认配置 → 创建缓存目录 → 依赖预构建(esbuild)→ 启动 HTTP 服务器与 WebSocket 服务 → 处理请求(返回 HTML、重写 import 路径、返回预构建依赖)→ 文件变动触发热更新(chokidar 监听 → 计算更新边界 → WebSocket 推送指令 → 浏览器动态拉取新模块)→ 生产构建(Rollup 打包输出 dist)。

本文实现的只是一个 mini 版 Vite,但基本复刻了 Vite 的核心流程与设计思想。真实的 Vite 在路径解析、插件机制、模块图管理、错误处理等方面要精妙和严谨得多。

Vite 的可贵之处,不在于它比 Webpack 快了多少,而在于它敢于质疑"开发即打包"这条走了十年的老路,当别人还在打包巨无霸bundle的时候,它转身拥抱了 ES Module,用浏览器的能力做浏览器的事,把复杂留给自己,把简单还给开发者。

相关推荐
liangshanbo12155 天前
面试题:Webpack 的 publicPath 有什么作用?
前端·webpack·node.js
cindershade6 天前
首屏不是 dist:用“加载账本”治理 Vite 的大包
性能优化·vite
liangshanbo12157 天前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
liangshanbo12157 天前
面试题:AI 对话上下文超限怎么办?
面试·webpack·devops
liangshanbo12158 天前
Webpack 文件指纹面试题整理
前端·webpack·node.js
liangshanbo12158 天前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
liangshanbo12158 天前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
陪我一起学编程8 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
liangshanbo12158 天前
Webpack的分包策略面试题
前端·webpack·node.js
liangshanbo121510 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js