Skip to content

01 - 构建工具与 Vite 基础

一、构建工具优点

前端源码通常包含现代语法、模块导入、样式与静态资源引用。 而且就本科时编写的直接引入cssjs的方法,在项目中也容易出现各种作用域污染: 比如:

html
<!-- 页面直接挂多个脚本-->
<script src="./config.js"></script>
<script src="./request.js"></script>
<script src="./page.js"></script>
js
// config.js
// 全局挂载,任何脚本都可改写
window.appConfig = { apiBase: '/api/v1' }
js
// request.js
// 依赖全局变量,且有命名冲突风险
window.request = function request(path) {
  return fetch(window.appConfig.apiBase + path)
}
js
// page.js
// 另一个脚本若覆盖了 window.request,这里会直接受影响
window.request('/users')

同一场景改成模块化后,依赖关系会更清晰。

ts
// src/config.ts
// 显式导出,避免全局变量污染
export const apiBase = '/api/v1'
ts
// src/request.ts
import { apiBase } from './config'

// 参数 path 表示接口路径,返回 Promise<Response>
export function request(path: string) {
  return fetch(apiBase + path)
}
ts
// src/page.ts
import { request } from './request'

// 依赖边在 import 里可见,构建工具可静态分析
request('/users')

现代框架构建流程不仅提供了ts编译等方式,也提供了更多资源管理的途径。

构建工具负责的核心环节:

  1. 依赖图解析
  2. 语法转换
  3. 样式处理
  4. 资源处理
  5. 开发服务与热更新
  6. 生产产物输出

实际开发里的一个现象:

  1. 本地开发正常
  2. 线上子路径部署后资源 404
  3. 经常是构建 base 路径与部署目录不一致

这类的原因通常不在业务代码本身,而在资源定位规则。 本地开发阶段由开发服务器兜底,资源请求路径在根路径下看起来都能访问。 一旦部署到子路径,入口 js、css、图片的引用地址就必须统一带上前缀。 只要其中一类资源路径没有按同一规则生成,404 就会出现。

也因此,构建工具的价值不只是把代码打包。

  1. 构建阶段统一生成资源引用路径,减少拼接路径遗漏
  2. 把产物文件名做哈希,配合缓存策略降低旧资源误命中
  3. 输出稳定目录结构,便于静态服务和反向代理做一致映射
  4. 通过配置固定 base 规则,让开发环境与生产环境的路径语义保持一致
  5. 环境变量集中管理,避免把测试地址写进生产包
  6. 支持按路由或模块拆分代码,减少首屏不必要加载
  7. 支持 source map 产物,线上报错可回溯到源码位置
  8. 支持统一插件链,图片压缩、样式前缀、语法降级可集中处理
  9. 支持构建产物分析,便于追踪体积变化来源

比如:环境隔离与路径注入。

ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  // 根据 mode 读取 .env.development 或 .env.production
  const env = loadEnv(mode, process.cwd(), '')

  return {
    // 子路径部署时用环境变量注入,避免手写固定值
    base: env.APP_BASE || '/',

    define: {
      // 在构建阶段注入版本号,便于排查线上是否命中新包
      __APP_VERSION__: JSON.stringify(env.APP_VERSION || '0.0.0')
    }
  }
})

二、模块化与 ESM 基础

1. 为什么要模块化

模块化的目标是让代码拆分、复用、依赖管理更稳定。 一个业务模块需要明确输入与输出,避免全局变量污染,示例可见上文。

2. ESM 的核心

ESM 是浏览器和现代运行环境支持的模块系统。 关键字是 import 与 export。

不过微信小程序与 Node.js 里,require 的出现频率也不低,有说法是太多旧代码需要兼容。

对比一下的话,import:

  1. ESM 静态依赖更利于构建阶段分析
  2. tree shaking 更容易生效
  3. 动态 import 更容易做路由级懒加载
js
// CommonJS 写法
const dayjs = require('dayjs')

function formatTime(value) {
  return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}

module.exports = { formatTime }
js
// ESM 写法
import dayjs from 'dayjs'

export function formatTime(value) {
  return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
js
// 静态导入,在编译阶段即可分析依赖边
import { createApp } from 'vue'

// 命名导出,便于按需导入
export const apiBase = '/api'

// 动态导入,常用于路由级别懒加载
const view = () => import('./views/Home.vue')

ESM 两个特性:

  1. 依赖关系静态可分析
  2. 支持按模块粒度加载

3. ESM 与 CommonJS 的区别

常见差异:

  1. 声明时机 ESM 通过 import 在语法层声明依赖,构建工具可提前拿到依赖图。 CommonJS 通过 require 在执行阶段加载依赖,依赖关系更多依赖运行路径。

  2. 构建优化 ESM 更容易触发 tree shaking,未引用代码更容易被剔除。 不过其实 CommonJS 也能优化,但优化深度依赖额外转换链路。

  3. 代码拆分 ESM 支持动态 import,路由级懒加载。 CommonJS 场景下虽然能拆分,不过配置复杂度通常更高。

js
// CommonJS 片段
// 运行阶段按条件加载
const role = process.env.ROLE
const page = role === 'admin'
  ? require('./pages/admin')
  : require('./pages/user')

module.exports = page
js
// ESM 片段
// 动态 import 
const role = import.meta.env.VITE_ROLE
const page = role === 'admin'
  ? () => import('./pages/admin.js')
  : () => import('./pages/user.js')

export default page
  1. 首屏仅请求主路由与必要公共包
  2. 次级路由代码在访问时拉取
  3. 包体积与首屏耗时更容易压下来

三、Vite 的最小可用配置

1. 初始化与基础命令

bash
npm create vite@latest [项目名]
# 运行后,提示多种配置信息,勾选操作即可
cd [项目名]
npm install
npm run dev
npm run build
npm run preview

另外一点,有两个类似的命令:npm create vite@latestnpm create vue@latest

  1. npm create vite@latest 定位偏通用,支持多框架模板,包含 vanillavuereactsvelte

  2. npm create vue@latest 定位偏 Vue 官方项目模板,底层构建链路依赖 Vite,同时附带 Vue 生态。

  3. npm create vite@latest 优点:模板覆盖面更广,跨框架更方便。 缺点:Vue 配套能力需后续手动接入,路由、状态管理、规范工具多一步配置。

  4. npm create vue@latest 优点:创建阶段可勾选 vue-routerpiniaeslintvitestcypress,比较一步到位一些。 短板:单面向 Vue 生态。

2. Vite创建后目录结构

text
my-app
  index.html
  src
    main.ts
  public
  vite.config.ts
  1. index.html 主入口
  2. src 放业务源码
  3. public 放无需构建处理的静态文件
  4. vite.config.ts 放项目级构建与开发配置

3. 常用配置项

ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'

export default defineConfig({
  // 插件链,按顺序参与源码转换
  plugins: [vue()],

  server: {
    // 允许局域网设备访问,联调移动端时常用
    host: '0.0.0.0',
    // 开发端口
    port: 5173,
    // 启动后自动打开浏览器
    open: true
  },

  resolve: {
    alias: {
      // 路径别名,避免 ../../ 多层相对路径
      '@': path.resolve(__dirname, 'src')
    }
  },

  // 依赖预构建配置,优化冷启动和依赖解析
  optimizeDeps: {
    // 显式预构建,适合体积大且多页面共用的依赖
    include: ['lodash-es']
  },

  build: {
    // 构建输出目录
    outDir: 'dist',
    // 产出 source map,便于错误平台定位源码
    sourcemap: true,

    // 静态资源目录
    assetsDir: 'assets',

    rollupOptions: {
      output: {
        // 分包示例,便于长期缓存稳定命中
        manualChunks: {
          vue: ['vue', 'vue-router'],
          utils: ['lodash-es']
        }
      }
    }
  }
})

配置说明。

  1. server 控制本地开发服务
  2. resolve.alias 统一路径别名,减少相对路径混乱
  3. optimizeDeps 对冷启动与依赖解析有明显影响
  4. build.outDir 指定构建输出目录
  5. build.assetsDir 指定静态资源子目录
  6. build.sourcemap 便于线上问题定位
  7. manualChunks 常用于公共包拆分与缓存稳定

4. 实际开发中的基础配置案例

后端接口通过网关提供,前端本地联调常见跨域。 开发阶段通常通过代理转发解决。

ts
server: {
  host: '0.0.0.0',
  port: 5173,
  proxy: {
    '/api': {
      // 本地访问 /api 开头路径时转发到目标服务
      target: 'http://localhost:8080',
      // 允许改变请求头中的 host
      changeOrigin: true,
      // 可选重写,按后端路由规则决定是否开启
      // rewrite: path => path.replace(/^\/api/, '')
    }
  }
}

这个配置在前后端分离项目中非常高频。


四、Vite 开发阶段执行流程

开发阶段最核心的感受是快。 快来自按需处理,而不是一次性全量打包。

1. 首次启动

  1. 启动本地开发服务
  2. 扫描依赖并执行预构建
  3. 浏览器请求页面入口
  4. 浏览器按 ESM 规则继续请求依赖模块

常见观察点。

  1. 首次启动日志里可看到依赖预构建
  2. 第二次启动通常更快,缓存命中后预构建成本下降

2. 模块请求与按需转换

Vite 不会在开发阶段先打完整包。 浏览器请求到哪个模块,Vite 就转换哪个模块。

这带来两个直接收益。

  1. 启动时间短
  2. 修改单文件后,失效范围更小

典型案例。

  1. 页面拆分为 40 个模块
  2. 修改其中一个表格组件
  3. 热更新只影响相关模块,页面状态通常可保留

3. 热更新路径

修改某个业务模块后,Vite 会。

  1. 识别变更文件
  2. 重新转换该文件及必要依赖
  3. 通过热更新协议通知浏览器
  4. 浏览器只替换受影响模块

这个过程决定了日常迭代时的反馈速度。


五、Vite 构建阶段执行流程

构建阶段目标是输出可部署产物。

1. 核心步骤

  1. 读取入口与依赖图
  2. 执行插件转换流程
  3. 做代码分割与产物生成
  4. 输出到 dist 目录

构建阶段和开发阶段的边界。

  1. 开发阶段强调实时响应
  2. 构建阶段强调可部署产物与缓存策略

2. 产物去向与命名

默认会在 dist 下看到这类结构。

text
dist
  index.html
  assets
    index.xxxxx.js
    index.xxxxx.css
    vendor.xxxxx.js

关键点。

  1. 资源文件通常带哈希,便于强缓存
  2. 业务代码与公共依赖会按策略切分
  3. index.html 会引用最终产物路径

3. 与部署协作的关注点

  1. 服务器要正确托管 dist 目录
  2. 需要为静态资源配置长期缓存
  3. 入口 html 通常使用短缓存,保证新版本可达
  4. sourcemap 上传策略要与监控系统一致

经典线上案例。

  1. 构建产物路径为 /assets
  2. 站点部署在 /admin 子路径
  3. 未设置 base 时浏览器会从根路径取资源,最终触发 404

对应配置通常是。

ts
export default defineConfig({
  // 子路径部署时常见配置
  base: '/admin/'
})

六、Vite 为何比 Webpack 更快

这个问题在面试与工作里都很常见。 可以按开发阶段和构建阶段分开理解。

1. 开发阶段速度优势

核心原因是基于原生 ESM 的按需加载。

对比思路。

  1. Webpack 开发阶段通常先做整图打包
  2. Vite 开发阶段以浏览器原生 ESM 请求为主
  3. Vite 只转换当前请求模块,不先打全量包

所以项目越大,冷启动与更新速度差距越明显。

一个常见联想。

  1. 当项目依赖图很大时
  2. 全量打包的初始成本持续上升
  3. 按需转换方案在开发阶段优势更明显

2. 依赖预构建策略

Vite 会把第三方依赖先做一次预构建。 预构建目标是把复杂依赖转为更适合浏览器快速处理的模块格式。

收益。

  1. 减少浏览器多层依赖解析开销
  2. 避免重复转换第三方包
  3. 提升后续页面加载稳定性

实际场景。

  1. 组件库和图表库都较重
  2. 首次启动后缓存预构建结果
  3. 后续迭代时依赖层转换成本明显下降

3. 构建阶段说明

Vite 构建阶段依然会做完整打包流程。 所以构建速度优势主要体现在开发阶段。

结论要准确。

  1. Vite 快的关键点不是完全不打包
  2. Vite 快的关键点是开发阶段利用原生 ESM 按需处理
  3. 生产构建仍然需要完整产物生成

七、工程化记录点

这部分更像复盘清单。 用于上线前后快速对照。

1. 配置层

  1. 别名统一在配置层维护
  2. 环境变量按开发、测试、生产分层
  3. base 路径与部署目录保持一致

2. 代码组织

  1. 业务模块按域拆分
  2. 公共工具与业务逻辑分层
  3. 静态资源按缓存策略划分目录

3. 发布链路

  1. 构建前做类型检查与代码检查
  2. 构建后做体积对比与关键路由冒烟
  3. 发布后观察错误率与资源命中率

八、常见问题排查清单

1. 本地启动慢

  1. 检查依赖数量与历史冗余包
  2. 检查是否误把大文件放入实时转换链路
  3. 清理缓存后复测启动耗时
  4. 检查 optimizeDeps 是否覆盖核心重依赖

2. 热更新不生效

  1. 检查文件是否在监听范围
  2. 检查插件顺序是否影响模块转换
  3. 检查是否存在全局副作用导致页面强刷
  4. 检查是否误用了无法被热更新边界接管的写法

3. 线上资源 404

  1. 检查构建 base 路径配置
  2. 检查静态资源托管目录映射
  3. 检查发布系统是否漏传 assets 目录
  4. 检查反向代理是否改写了静态资源路径

九、学习路径记录

第一阶段。

  1. 跑通最小 Vite 项目
  2. 看懂入口、依赖、产物目录
  3. 完成本地构建与预览

第二阶段。

  1. 接入别名、代理、环境变量
  2. 观察修改代码后的热更新影响范围
  3. 比对构建前后资源体积变化

第三阶段。

  1. 接入规范脚本与检查流程
  2. 完成一次完整发布演练
  3. 能定位一类构建链路问题

<< 返回首页