使用构建工具构建项目的意义
- 提高开发效率:自动化的构建、热更新、模块化开发等功能大大提高了开发效率。
- 优化性能:通过代码分割、资源压缩等优化手段,提高页面加载速度和响应速度。
- 增强可维护性和扩展性:模块化开发和插件化支持使得项目结构更加清晰,便于维护和扩展。
- 自动化工作流程:减少重复劳动和人为错误,提高开发质量。
前端项目构建工具
常见的项目构建工具有Webpack、vite、Rollup、esbuild等;这些打包工具出生的时间,侧重的目的性上都会有一些差异。
| 工具 | 核心优势 | 适用场景 | 构建速度 | 配置复杂度 | Tree-shaking |
|---|---|---|---|---|---|
| webpack | 生态强大、高度可配置 | 复杂应用 | 慢 | 高 | 支持 |
| vite | 开发速度极快、现代框架 | 现代SSR、SPA | 快 | 中 | 支持 |
| Rollup | 输出精简、库打包优化 | 类库、NPM 包 | 中 | 中 | 优秀 |
| esbuild | 极速编译、低层级工具 | 底层引擎、辅助工具 | 极快 | 低 | 支持 |
上表中是目前常见的打包工具性能对比及核心优势。目前常用的是vite,下面将从如何使用、如何实现vite、HMR热更新原理几个方面描述。深入学会一个构建工具,举一反三其他的都与之类似。
vite
vite使用
简单使用
第一步
npm install -D vite
第二步
并创建一个像这样的 b b.html 文件:
<p>Hello Vite!</p>
第三步
npx vite
你看看,就这么简单vite项目就启动起来了。如何要学习vite如何配置vue项目,react项目,最好的方式是看官方文档 vite。
vite实现流程
初学vite都会有个问题,之前用webpack都是从index.js作为主入口文件。这个vite怎么就变成index.html了呢?
这是vite实现的核心原理,vite使用ESModule方式,将index.html作为入口文件,通过moudle模块加载依赖的所有文件。第三包依赖包vite都将其转为ESModule可以加载的方式。
很多同学就很好奇,那运行npm run dev启动项目,vite做了哪些事情。其实简单来说包含下面几个方面
- 找到并且执行vite.js
- 配置加载:加载配置文件,包含用户配置。vite本身配置
- 服务器创建:创建静态服务器并且设置代理
- 依赖处理:将三方依赖打包成ESModule模块并存储到
node_modules/.vite/deps - 开发服务:服务启动
从使用者即业务开发者的角度来看
这样我们在使用一个简单的vite项目的时候访问就会看到,首先加载html,html内部引入/@vite/client和main.ts。@vite/client是热更新模块,main.ts是用户模块。按顺序加载即可
但是同学们就发现了,在mian.ts里面本来自己写的是
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
怎么加载的时候就变成
//引入方式不一致,vite替换依赖为ESModule模块
import {createApp} from "/node_modules/.vite/deps/vue.js?v=1c0b10b7";
import "/src/style.css";
import App from "/src/App.vue";
createApp(App).mount("#app");
vite通过esbuild编译整个项目,将所有第三方依赖打包成支持ESModule模块的js并存储在/node_modules/.vite/deps/xxxx下。并且用户在请求这个依赖的时候将目标路径指向编译后结果的位置。
构建工具角度分析需要做什么
npm run dev到底在干嘛?
其实真正执行的是vite --mode dev,这里会指向本地项目中node_modules下的package.json,如果是全局安装的话则指向全局环境vite下面package.json
总结 :npm run dev执行的时候去运行了 vite --mode dev命令,然后就会找到全局,或当前项目中vite的package.json文件;这里面bin记录需要执行js文件的位置。然后用node来运行这个文件bin/vite.js。一句话概括:告诉操作系统去执行vite.js
在项目运行阶段,运行vite.js是去执行了cli.js
cli.js运行
- 配置加载
- 服务创建
- 依赖处理
- 开发服务
- 打印服务信息
- 启动服务
import { loadConfigFromFile } from './config'
import { createServer } from './server'
import { optimizeDeps } from './optimizer'
import { printServerUrls } from './logger'
async function startDevServer() {
// 1. 配置加载
const config = await loadConfigFromFile()
// 2. 服务器创建
const server = await createServer()
// 3. 依赖处理
if (config.optimizeDeps.auto !== false) {
await optimizeDeps()
}
// 4. 开发服务
await server.listen()
// 设置文件监听
server.watcher.on('change', async (file) => {
// 处理文件变更
await server.moduleGraph.onFileChange(file)
// 触发 HMR
server.ws.send({
type: 'update',
updates: [{
type: 'js-update',
path: file,
timestamp: Date.now()
}]
})
})
// 打印服务器信息
printServerUrls(server.config.server, server)
}
// 启动开发服务器
startDevServer().catch((err) => {
console.error('error starting server:', err)
process.exit(1)
})
optimizeDeps依赖处理
- 扫描阶段
- 执行预构建
在
.vite/deps目录下生成预构建的依赖文件
export async function optimizeDeps(
config: ResolvedConfig,
force = false,
asCommand = false
): Promise<DepOptimizationMetadata | null> {
// 1. 扫描项目依赖
const scan = await scanImports(config)
// 2. 执行预构建
if (scan.deps || scan.missing) {
return await runOptimizeDeps(config, scan.deps, scan.missing)
}
支持热更新
- 初始化ws,给HMR客户端注入代码
- 文件变化监听设置
- 处理文件变更
- 客户端HMR处理
初始化ws,给HMR客户端注入代码
// packages/vite/src/node/server/index.ts
export async function createServer() {
// 1. 创建WebSocket服务器
const ws = createWebSocketServer(httpServer)
// 2. 注入HMR客户端代码
app.use(async (req, res, next) => {
if (req.url === '/') {
const html = await readFile('index.html')
// 注入HMR客户端代码
const injectedHtml = injectHMRClient(html)
res.send(injectedHtml)
}
})
}
文件变化监听设置
// packages/vite/src/node/server/index.ts
function setupHMR(server: ViteDevServer) {
const watcher = chokidar.watch(server.config.root, {
ignored: [/node_modules/, /\.git/],
ignoreInitial: true
})
watcher.on('change', async (file) => {
// 处理文件变更
handleFileChange(server, file)
})
}
处理文件变更
// packages/vite/src/node/server/hmr.ts
async function handleFileChange(server: ViteDevServer, file: string) {
// 1. 更新模块依赖图
const module = server.moduleGraph.getModuleByFile(file)
await module.update()
// 2. 确定受影响的模块
const affected = await getAffectedModules(module)
// 3. 发送HMR更新
const updates = affected.map(mod => ({
type: 'update',
path: mod.url,
timestamp: Date.now()
}))
server.ws.send({
type: 'update',
updates
})
}
客户端处理
// packages/vite/src/client/client.ts
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'update':
// 1. 获取需要更新的模块
const { path, timestamp } = payload
const mod = await import(`${path}?t=${timestamp}`)
// 2. 执行模块热更新
if (mod.hot) {
try {
await mod.hot.accept()
} catch (e) {
// 3. 如果热更新失败,执行页面刷新
location.reload()
}
}
break
}
}
vite使用优化
开发环境优化:
- 依赖预构建:将 CommonJS/UMD 转换为 ESM
- 按需编译:请求时才编译对应模块
- 热更新:精确更新变更模块
- 缓存优化:使用内存和文件系统双重缓存
构建优化:
- CSS 代码分割
- 资源压缩
- 动态导入处理
- Tree-shaking
- 懒加载分包
性能优化:
- 路由懒加载
- 资源预加载
- 并行处理
- 增量编译
开发体验优化:
- 快速冷启动
- 即时热更新
- 源码映射
分别从vite的使用,及vite的实现两个角度描述,Vite 是一个新一代的前端构建工具,和传统的打包工具(如 Webpack)相比,Vite 在开发模式下具有明显的优势:
- 极速启动:Vite 利用浏览器原生的 ES 模块支持,启动时只需加载入口文件,避免了传统打包工具的全量打包过程。
- 依赖于构建:Vite 会在开发时预构建第三方依赖并且缓存,提高浏览器加载速度。
- 即时热更新:Vite 利用精细化的热更新,只更新修改的模块而非整个项目,从而实现极快的更新速度。
- 开箱即用的配置:Vite 默认支持 TypeScript、CSS、PostCSS 等,无需额外的配置。
总结
本文分别从项目构建的意义、常见的构建工具、以及vite的使用及实现三个方面来阐述。在我们日常开发中可以更好的使用这些构建工具来提升代码质量,提升生产效率。好的工具,事半功倍。