Skip to content

构建工具选型 ​

构建工具平时安静地待在后台,一出问题却很要命:

  • 本地开发一切正常,打包部署后页面白屏,控制台一排 404;
  • process.env.API_URL 在 Vite 项目里报 process is not defined;
  • 发布了新版本,用户刷新了好几次还是旧页面;
  • 在自己的 Mac 上构建成功,到了 CI 上报“找不到模块”。

这些问题都和“构建工具到底做了什么”有关。这篇先讲原理,再讲选型,最后逐个修坑。

读完这篇你会

  • 说清构建工具的工作,以及开发环境和生产环境的差别
  • 根据项目类型选择 Vite、Webpack 或 Rollup
  • 修复部署路径、环境变量、缓存、文件名大小写等常见构建问题

1. 原理:构建工具把“开发用的代码”变成“浏览器用的代码” ​

你写的代码浏览器并不能直接高效地运行:有 TypeScript、Vue 单文件组件、Sass,还有成百上千个小模块。构建工具要做这些事:

工作说明
转译TypeScript、JSX、.vue 文件、新语法 → 浏览器能运行的 JS
打包把大量小模块合并成少量文件,减少请求
拆分按路由、按需把代码切成多个块,首屏只加载需要的
优化压缩代码、删除没用到的代码(tree-shaking)
资源处理图片、字体、CSS 的处理与引用
文件指纹输出 index.3f9a1c.js 这样带内容哈希的文件名,方便长期缓存

开发和生产是两套不同的流程 ​

以 Vite 为例:

  • 开发时:不打包。浏览器请求哪个模块,Vite 就现场转译哪个,利用浏览器原生的 ES Module 加载。改一个文件只需重新处理这一个文件,所以启动和热更新都很快。
  • 生产构建时:完整打包、拆分、压缩、加哈希,产出一套静态文件。

打个比方:开发环境像餐厅现点现做,你点哪道菜就做哪道;生产环境像中央厨房提前做好、分装好的套餐。两者用的食材一样,但流程完全不同。很多“开发环境好好的,上线就坏了”的问题,根源都在这个差别上。所以发布前一定要在本地用 vite build && vite preview 跑一次生产版本。

2. 选哪个工具 ​

工具特点适合
Vite开发服务器启动快、配置简单;生产构建使用 Rollup 系打包器(新版本逐步切换到 Rust 编写的 Rolldown)新的应用项目,Vue / React 都适用
Webpack生态最成熟,插件最多,配置灵活但复杂已有的 Webpack 项目、需要特殊插件的大型应用
Rollup产物干净,支持输出多种模块格式开发 npm 包、组件库、SDK
esbuild / Rspack 等用 Go、Rust 编写,速度很快作为其他工具的底层,或替换 Webpack 加速构建

大多数情况下,用框架官方脚手架生成的默认配置就够了。如果你发现自己在维护几百行的构建配置,先停下来想想是不是工具选错了,或者是不是在解决一个本不该存在的问题。

不要为了“用新工具”而迁移。从 Webpack 迁到 Vite 的收益(开发速度)要明确大于成本(插件替换、配置改写、回归测试)。

3. 坑一:部署到子路径后白屏,资源全部 404 ​

现象:项目部署在 https://example.com/admin/ 下,打开后白屏,Network 面板里 https://example.com/assets/index.js 全部 404。

原因:构建工具默认假设应用部署在网站根路径 /,生成的资源地址都是 /assets/...,没有带上 /admin/ 前缀。

修复:配置部署的基础路径,路由也要使用同一个前缀:

js
// vite.config.js
export default defineConfig({
  base: '/admin/'
})
js
// router
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL), // 与 base 保持一致
  routes
})

代码里引用 public 目录的资源时,也要拼上 import.meta.env.BASE_URL,而不是写死以 / 开头的路径。

4. 坑二:环境变量取不到,或者把密钥打包进了前端 ​

现象一:在 Vite 项目里写 process.env.API_URL,浏览器报 process is not defined;改成 import.meta.env.API_URL,拿到的是 undefined。

原因:process.env 是 Node.js 的东西,浏览器里没有。Vite 通过 import.meta.env 暴露环境变量,并且只暴露以 VITE_ 开头的变量,防止你不小心把服务器的变量带进前端。

修复:

text
# .env.production
VITE_API_BASE=https://api.example.com
js
const baseURL = import.meta.env.VITE_API_BASE

现象二:安全扫描发现,前端的 JS 文件里有第三方服务的密钥。

原因:有人把密钥写进了 VITE_ 开头的环境变量。构建时,这些变量会被直接替换成字符串写进 JS 文件,任何人打开浏览器开发者工具都能看到。

修复:前端代码里不能放任何密钥。需要用密钥调用的服务,由后端代理:前端请求自己的后端,后端带上密钥去调用第三方。已经泄露的密钥要立即作废、重新生成。

5. 坑三:发布了新版本,用户看到的还是旧页面 ​

现象:发布后,部分用户反馈还是旧界面;还有用户点击某个菜单时页面报错,刷新后又好了。

原因:浏览器缓存了旧的 index.html。旧的 HTML 引用的是旧的 JS 文件,如果服务器上旧文件还在,用户看到旧版本;如果旧文件已经被删除,懒加载某个页面时就会 404。

修复:给不同的文件设置不同的缓存策略:

文件缓存策略原因
index.htmlCache-Control: no-cache(每次都向服务器确认)它是入口,必须及时拿到新版本
带哈希的 assets/*.js、*.cssCache-Control: max-age=31536000, immutable内容变了文件名就会变,可以放心长期缓存

发布时保留上一个版本的资源文件一段时间,让还开着旧页面的用户也能继续加载。懒加载失败后的自动恢复,见 懒加载与发布恢复。

6. 坑四:Mac 上构建成功,CI 上报“找不到模块” ​

现象:本地开发和构建都正常,推送代码后,Linux 上的 CI 构建失败:Could not resolve "./components/UserCard.vue"。

原因:文件实际名叫 userCard.vue,导入语句写的是 UserCard.vue。macOS 和 Windows 的文件系统默认不区分大小写,所以本地能找到;Linux 区分大小写,就找不到了。

另一种常见情况是:在 Mac 上把文件名从 usercard.vue 改成 UserCard.vue,Git 默认没有记录这次改名,仓库里还是旧名字。

修复:

  • 统一文件命名规范,并开启编辑器的导入路径自动补全,不手写路径;
  • 改文件名大小写时用 git mv usercard.vue UserCard.vue;
  • 让 CI 在 Linux 上构建,尽早暴露这类问题。

7. 坑五:第三方依赖打成一个巨大的包 ​

现象:每次发布,用户都要重新下载一个 1MB 的 vendor.js,哪怕只改了一行业务代码。

原因:所有第三方依赖被合并成一个文件。只要依赖有任何变化(甚至只是升级一个小工具包),整个文件的哈希就会变,缓存全部失效。

修复:按“变化频率”拆分。很少变化的框架核心单独一个块,可以长期缓存;大型但只在部分页面使用的库,跟着页面懒加载:

js
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          framework: ['vue', 'vue-router', 'pinia'] // 很少变化,单独缓存
        }
      }
    }
  }
})

拆得太碎也不好,请求数会变多。用体积分析工具看清楚构成后再决定。

排查思路 ​

  1. 在本地复现生产环境:vite build && vite preview(Webpack 项目同理,构建后用静态服务器打开 dist)。大部分“只在线上出问题”的情况都能在这一步复现。
  2. 看 Network 面板的 404:资源的请求地址错在哪里——是少了路径前缀,还是请求了旧版本的文件名。
  3. 开发环境出现奇怪的依赖报错(尤其是刚升级或安装依赖后):Vite 会预构建依赖并缓存,删除 node_modules/.vite 或用 vite --force 启动,强制重新预构建。
  4. 体积问题:用 rollup-plugin-visualizer 等工具生成体积报告,先处理最大的几块。
  5. CI 失败而本地成功:对比 Node 版本、包管理器版本、锁文件是否提交,以及文件名大小写。

检查清单 ​

  • [ ] 发布前在本地运行过生产构建和预览
  • [ ] 部署在子路径时配置了 base,路由使用相同前缀
  • [ ] 前端环境变量里没有任何密钥
  • [ ] index.html 不长期缓存,带哈希的资源长期缓存
  • [ ] 文件名大小写和导入路径一致,CI 在 Linux 上构建
  • [ ] 依赖拆分考虑了缓存,定期检查产物体积
  • [ ] 锁文件已提交,CI 与本地使用相同的 Node 和包管理器版本

延伸阅读 ​

为复用而记录,为理解而整理。