项目构建

228 阅读6分钟

使用构建工具构建项目的意义

  • 提高开发效率:自动化的构建、热更新、模块化开发等功能大大提高了开发效率。
  • 优化性能:通过代码分割、资源压缩等优化手段,提高页面加载速度和响应速度。
  • 增强可维护性和扩展性:模块化开发和插件化支持使得项目结构更加清晰,便于维护和扩展。
  • 自动化工作流程:减少重复劳动和人为错误,提高开发质量。

前端项目构建工具

常见的项目构建工具有WebpackviteRollupesbuild等;这些打包工具出生的时间,侧重的目的性上都会有一些差异。

工具核心优势适用场景构建速度配置复杂度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做了哪些事情。其实简单来说包含下面几个方面

  1. 找到并且执行vite.js
  2. 配置加载:加载配置文件,包含用户配置。vite本身配置
  3. 服务器创建:创建静态服务器并且设置代理
  4. 依赖处理:将三方依赖打包成ESModule模块并存储到node_modules/.vite/deps
  5. 开发服务:服务启动

从使用者即业务开发者的角度来看 这样我们在使用一个简单的vite项目的时候访问就会看到,首先加载htmlhtml内部引入/@vite/clientmain.ts@vite/client是热更新模块,main.ts是用户模块。按顺序加载即可

image.png 但是同学们就发现了,在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

image.png

总结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的使用及实现三个方面来阐述。在我们日常开发中可以更好的使用这些构建工具来提升代码质量,提升生产效率。好的工具,事半功倍。