Loading...

前端构建工具到底是用来干嘛的?

2025-06-11 · 14min

前言

之前在工作虽然针对业务定制化封装过脚手架,但脚手架这东西其实本质上就是模板复制,知道原理后实现起来真的没什么难度。

而对真正的前端工程化还停留在非片面会用的程度上,并没有深入思考前端构建的存在的原因,以及原理,所以决定深入了解下前端工程化的知识了。

这次先了解下,构建工具的作用与实现原理。

构建工具

构建工具是前端工程化不可缺少的重要部分,无论是用什么前端框架进行开发,都必须依赖于构建工具。

虽然浏览器也能够读懂 Javascript,CSS 和 HTML,但是我们在开发的过程中,我们习惯把代码按照目录规范,分开很多个文件去写来,语言有用 Typescript 的,有在 JSX 文件里写代码的,会调用很多新的 API,变量命名也有规范,注释写的全,CSS 喜欢用原子类或者预处理器等,这些方便问我们开发者体验的写法。

而浏览器则希望少请求文件,新的 API 会有兼容性问题,变量命名也希望是单字节的,注释也不需要看。

构建工具的存在,就是为了把我们编写的前端代码,输出成一份体积最小的可供浏览器运行的代码。

核心作用

虽然现在前端构建工具有非常多,也是新的越来越好用了,但无论是 Webpack 还是 Vite,核心作用都是一样的。

打包压缩 (Bundling & Minification)

打包压缩的目的就是为了将我们写的大量分散的源代码文件进行合并、优化,同时跟着静态资源一起进行压缩,生成浏览器需要的少量高效的静态资源文件。

其目标是为了:

  • 减少 HTTP 请求:浏览器对同一域名的并发请求数有限制(通常 6-8 个)。如果项目有成百上千个小文件,加载会极其缓慢。打包将多个文件合并成少数几个文件,大大减少请求数量。
  • 优化加载性能:对于代码文件压缩,其实就是移除空格换行、注释、缩短变量名为单字节、使用更高效的编码,这确实能够显著的缩小文件体积,加快网络传输速度。
  • 依赖管理: 现代项目依赖大量第三方库,构建工具能解析复杂的模块依赖关系(import/require),将它们正确地打包在一起或按需分离。
  • 代码转换/编译: 在打包过程中,可以集成 Babel 转换 ES6 + /JSX/TS 为兼容旧浏览器的 ES5、Sass/Less 编译器将预处理 CSS 转为普通 CSS 等。
  • Tree Shaking: 移除未使用的代码,尤其是第三方库中未引用的导出,但这依赖于 ES Module 的静态分析特性。如 Rollup, Webpack 打包工具在打包时会分析哪些导出被真正使用,剔除死代码,有效减小 bundle 体积。
  • 代码分割 (Code Splitting): 虽然要减少 HTTP 请求,但也不是把所有代码都打包进一个巨大的文件。构建工具可以将代码按路由、功能模块或动态 import() 拆分成多个较小的包(chunk)。用户首次加载只需下载当前页面必需的代码,后续页面或功能按需加载,提升首屏加载速度。

如何实现?

构建工具需要定义一个或多个入口起点,如 main.ts 或者 App.tsx。

通过递归分析入口文件以及其导入到所有模块,进行依赖解析,构建出一个完整的依赖关系图谱。

对于非 Javascript 的资源,会通过加载器(Loader)进行处理(如 .css 用 css-loader,.scss 用 sass-loader,图片资源会用 file-loader/url-loader),将它们转换为 Javascript 可以理解处理的格式,通常会转成字符串或者 URL。

然后再通过插件(Plugin)进行压缩、代码分割、生成 HTML 文件、复制静态资源、定义环境变量等其他更广泛的任务。

最终根据配置,将上面处理好的模块进行合并、优化、再压缩,打包输出成一个或多个 bundle 文件以及经过处理的其他静态资源。

文件指纹(Hashing)

文件指纹其实就是我们常说的哈希打包,会在压缩打包文件名中加入唯一的哈希字符串,最重要目的就是为了解决强缓存策略 (Long-term Caching / Cache Busting)。

同时也可以用于标记文件内容的版本,进行版本控制。

开发服务器(Development Server)

我们的开发环境其实也是通过构建工具运行一个开发服务器实现的,它带来我们那些耳熟能详不可或缺的能力:

  • 实时预览
  • 自动刷新
  • 热更新
  • 按需编译
  • 模拟生产环境
  • 错误定位

具体实现步骤:

  • HTTP(S) 服务器:构建工具本质就是一个 Node 应用程序,Node 内置了 http 和 https 模块,通过导入需要的模块,以及中间件(如 serve-static 用于提供项目根目录下的静态文件、拦截入口 HTML 文件,注入开发环境脚本、环境变量等操作)和请求路由框架(如 connect、express、koa)。Vite 早期使用的是 connect,Webpack 早期基于 express,后面也转向更加轻量化的方案了。
  • 监听项目文件系统:核心是通过 fs.watch / fs.watchFile 两个 API 进行监听,但现代也有更强的封装库 chokidar ,可以做到更好的监听效果(例如忽略某些目录下的文件监听)。
  • 模块解析/按需编译:通过 Loaders 或者类似的机制实现,同时生成一个 Source Map,将解析/编译后的代码映射回源代码,得以让开发者在浏览器中进行调试和错误定位。
  • 实现 HMR 协议:当监听到文件发生变化时,服务器需要计算出变化的模块,并通过 Websocket 通知浏览器。当浏览器端收到更新消息时,会下载新模块并进行替换。浏览器端的相关逻辑,就是在第一步中,拦截入口 HTML 文件注入开发环境脚本实现的。

虽然开发环境也是通过构建工具搭建的,但与生产环境的打包不同:

开发环境生产环境
主要目标快速迭代、实时反馈、保留状态极致性能优化、最小体积、长期缓存
打包压缩通常不打包 (Vite) / 轻量打包 (Webpack)深度打包、压缩、Tree Shaking、代码分割
文件指纹通常不需要必需,用于强缓存
代码形态原始模块 (ESM)、未压缩、带 Source Map合并 Bundle、高度压缩、优化
输出位置主要在内存物理磁盘

常见构建工具

构建工具核心作用就是上面这些,但随着技术不断发展也越来越多的构建工具出现,这次主要简单记一下各自的特点。

Webpack

Webpack 是最早期的构建工具,定位是一款模块化打包工具。虽然他现在在开发体验上不太好,但他的项目存量以及生态都非常大,未来很长一段时间内估计都不会被淘汰。

特点:

  • 生态完善:支持 Common/ESM/AMD 等模块规范,通过 Loader/Plugin 处理 CSS 与图片等一系列资源。
  • 高度可配置:有着非常高自由度的配置能力,可以自定义代码分割、动态导入等操作,适合用于需要深度定制的场景进行开发。
  • 缺点:
    • 冷启动慢:即便是开发环境,也需要完整打包整个应用的入口文件和所有依赖项来构建一个依赖关系图谱;同时还需要通过 Loader/Plugin 对静态资源进行处理和转换,从而导致了冷启动慢,这种情况在大型复杂项目中尤为明显。
    • 配置复杂:虽然 Webpack 有着非常高的配置能力,但代价就是造就了配置方式非常复杂,过于复杂的配置,不仅学习成本高,还也会进一步影响构建速度。

Vite

Vite 是现代新项目的首选构建工具,基于原生 ESM 构建,主打开发效率。

特点:

  • 冷启动快:利用浏览器原生的 ESM 按需加载,开发阶段是几乎不打包的,从而大幅度提高了开发效率。
  • 热更新快:通常 node_modules 的依赖是 CJS 格式或者包含大量的小文件,而 CJS 是不能被浏览器原生 ESM 直接使用的。但通过 ESBuild ,在 Vite 首次启动或 node_modules 发生变化时,进行一次性预构建:
    • 格式转换:将 CJS/UMD 依赖转换为 ESM 格式。
    • 合并文件:将依赖众多小文件合并成少量的大文件。

    当浏览器请求依赖时,Vite 服务器直接返回这些预构建好的 ESM 文件,后续除非依赖发生变更,否则无需再处理依赖,从而提高热更新的速度。
  • 生产优化:生产环境打包使用 Rollup 打包。
  • 开箱即用:提前内置好了 Typescript、CSS预处理器、JSX支持。
  • 缺点:因为开发环境与生产环境用了两种构建方案,所以可能会出现生产环境才特有的问题。

Rollup

面向库和组件的轻量级打包工具。

特点:

  • 极致 Tree Shaking:静态分析依赖项,生产最小化的输出。
  • ESM 输出:无运行时的冗余代码,适合库开发。
  • 缺点:资源处理弱,需要插件扩展。

总结

以上3个就是目前前端比较常用的构建工具和各自的特点,但现在依然有新的构建工具在不断出现。其原因是上述构建工具都是 Node 程序,性能上与其他语言开发的程序还是有着瓶颈上的差距,而像 Rolldown、Rspack 这些新的构建工具,都是通过 Rust 来进行开发的。

我个人来说还是挺期待 Rolldown 的,希望不久的将来可以有更好的开发体验。