前端工程化-构建打包
当前前端打包工具的痛点;
常用的就是Webpack,通过抓取-编译-构建整个应用的代码,生成一份编译、优化好的生产环境代码,生成环境也一样,整个应用构建打包后,交给devserver
但是随着JS代码量的增长,打包构建时间越来越久,Webpack只解析js代码,其他文件打包就需要用到loader(加载器),开发服务器就遇到了瓶颈
- 缓慢的服务启动:大型的项目,启动时间达到几十秒甚至几分钟
- 缓慢的HMR热更新:HMR有性能瓶颈,无多少优化空间
一、Babel的原理
Babel 是 JS 代码转译工具,核心原理分 解析(Parse)、转换(Transform)、生成(Generate) 三步 :
- 解析:先做词法分析,把代码拆成一个个 token(比如关键字、变量名等);再语法分析,构建出描述代码结构的 抽象语法树(AST),像把
const a = 1;
转成对应 AST 节点。 - 转换:用
babel-traverse
遍历 AST,根据预设 / 插件规则修改节点,比如把 ES6箭头函数
转成 ES5 函数语法,就是在这里对 AST 增删改。 - 生成:最后靠
babel-generator
,把修改后的 AST 再转回合法 JS 代码,完成新老语法适配。
1、Babel如何将ES6箭头函数转为ES5函数
- Babel 首先进行词法分析,将代码分解成一个个的词法单元。接着进行语法分析,构建出对应的抽象语法树(AST)。在这棵 AST 中,箭头函数部分会形成一个类型为
ArrowFunctionExpression
的节点,这个节点包含了箭头函数的各种信息。 - 在转换阶段,Babel 会使用
babel-traverse
来遍历这棵 AST。当遍历到ArrowFunctionExpression
类型的节点时,Babel 会根据预设或插件的规则,将这个箭头函数节点转换为 ES5 风格的函数表达式节点(类型为FunctionExpression
)同时处理this
指向等问题(比如用var that = this
或者bind
调整 ) - 生成阶段,Babel 使用
babel-generator
工具,将转换后的 AST 再转换回 JavaScript 代码
2、Babel和Vite怎么协作的
-
流程上:Vite 开发时基于浏览器
ES Module
实现极速热更新,但遇到 ES6+ 语法、JSX 等,会通过@vitejs/plugin-babel
触发 Babel 转译;生产构建阶段,Vite 调用 Rollup 打包,Babel 同步完成语法降级(如 ES6→ES5 )、Polyfill 注入,保证输出代码兼容目标浏览器 -
核心场景:比如写 React 的 JSX ,Vite 识别后,Babel 用
@babel/preset-react
转译(把<div />
转成React.createElement
);再比如用@babel/preset-env
,能根据浏览器兼容性列表,自动转译语法、补全Promise
这类 API 的 Polyfill ,Vite 负责整合这些转译后的代码,输出最终产物。 -
配置上:可在
vite.config.js
里通过@vitejs/plugin-babel
配置 Babel 预设 / 插件,也支持根目录放.babelrc
,项目自定义配置优先级更高。这种配合让开发既享 Vite 的极速体验,又通过 Babel 覆盖兼容性需求-
对比
Webpack+babel
在 Webpack 里,通过
babel-loader
串联 Babel 。Webpack 负责模块打包,遇到 JS 文件时,调用babel-loader
,它内部启动 Babel 流程(解析、转换、生成 ),把新语法转译后,再交回 Webpack 继续打包。和 Webpack + Babel 对比,Vite 不需要预打包所有代码,开发时 Babel 只处理当前请求的模块,热更新更快;生产构建则借助 Rollup 生态,Babel 专注语法层转译,整体流程更轻量高效。
-
-
Polyfill是指一段代码,用于在旧的浏览器或者环境中实现原生不支持的新特性。提高代码的兼容性和可移植性 —
core-js
可以配置
@babel/preset-env
并结合core-js
来按需引入 Polyfill,减少打包体积
二、Webpack(2012)和Vite(2020)的区别
都是前端打包工具,作用类似,但是实现方式和使用方法有所不同
- 构建速度:Vite更快一些,因为Vite在开发环境中使用了浏览器原生的ES模块加载,而不是像WebPack一样使用打包后的文件进行模块加载。其次在Vite中,每一个模块都可以独立编译和缓存,因此当我们进行一些修改,就只需要重新编译修改过的模块,而不是整个应用程序
- 配置复杂度:Vite的配置相对简单,只需要指定一些基本拿到选项就可以开发。Webpack的配置比较复杂,需要针对具体项目进行不同的配置。需要理解各种插件,Loader等
- 生态环境:Webpack的生态环境更成熟,社区中拥有更广泛的支持和插件库,Vite处于发展阶段
- 功能特性:Vite设计初衷就是开发环境下的快速构建,追求效率;Werbpack更加全面综合,支持各种Loader和插件,处理多种类型的文件和资源
Vite基本选项:
-
基本开发环境:
server.port 用于指定开发服务器启动的端口; publicDir 用于指定存放静态资源(如 HTML、图片等)的目录; resolve.alias 则可以通过别名更便捷地引入项目中的模块
-
类型文件处理
export default {css: {preprocessorOptions: {scss: {// 给所有的 scss 文件引入全局变量,不需要在每个文件中单独引入additionalData: `@import "@/styles/variables.scss";` }}} }
-
生成环境配置:
export default {build: {// 构建输出目录,默认是 distoutDir: 'build', // 启用压缩,默认是 'esbuild',也可以设置为 'terser' 或 false 不压缩minify: 'esbuild', // 静态资源的打包方式,设置为 'inline' 可以将小资源内联到代码中assetsInlineLimit: 4096 } }
三、评估项目需求选择合适的打包工具
想选对前端打包工具,得结合项目多维度需求综合评估,我一般从这些方向考量:
- 开发阶段与效率需求
- 若追求 开发环境极致快(像高频迭代、需实时热更新的项目),选 Vite 。它基于浏览器原生 ES 模块,启动和热更新秒级响应,开发体验丝滑,适合中小型项目、框架生态项目(Vue/React 等)快速迭代。
- 要是项目 需兼容老浏览器(比如企业内部系统、toB 项目),或开发流程依赖传统打包逻辑,Webpack 更稳,它能通过 Loader/Plugin 处理各种兼容性场景。
- 项目规模与复杂度
- 中大型复杂项目(多页面、需代码拆分 / 复杂资源管理),优先 Webpack 。它的配置虽然繁琐,但能通过精细拆分(代码分割、异步加载)、缓存策略优化生产包性能,适配复杂业务逻辑。
- 轻量项目 / 库开发(比如组件库、工具库),Vite 或 Rollup 更合适。Vite 开箱即用,Rollup 的 Tree - shaking 还能精简代码体积,输出更纯净的产物。
- 生态与定制化需求
- 项目 依赖特殊 Loader/Plugin(比如自定义资源处理、老项目迁移),Webpack 生态成熟,插件库丰富,能覆盖 99% 的定制场景。
- 若项目 主打 “开箱即用”,不想在配置上花时间,Vite 更贴合,它默认集成常用功能(如 CSS 预处理、静态资源处理),简单项目直接 “零配置” 启动。
- 生产环境与部署需求
- 需 极致生产包优化(CDN 部署、体积严格压缩),Webpack 的 Code Splitting、Terser 深度优化能力更强,能精准控制产物质量。
- 若团队技术栈新、接受 “渐进式优化”,Vite 也能通过 Rollup 打包生产环境,虽然高级优化场景略逊 Webpack,但基本能满足中小型项目需求。
四、Vite原理
Vite
其核心原理是利用浏览器现在已经支持ES6
的import
,碰见import
就会发送一个HTTP
请求去加载文件,Vite
启动一个 koa
服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM
格式返回返回给浏览器,Vite
整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack
开发编译速度快出许多!
Vite
有如下特点:
-
快速的冷启动:
No Bundle
+esbuild
预构建 -
即时的模块热更新: 基于
ESM
的HMR
,同时利用浏览器缓存策略提升速度 -
真正的按需加载: 利用浏览器
ESM
支持,实现真正的按需加载
传统的打包工具如Webpack
是先解析依赖、打包构建再启动开发服务器,然而Vite
利用浏览器对ESM
的支持,当 import
模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。
1、ES Modules
ESM
提供了更原生以及更动态的模块加载方案,最重要的就是它是浏览器原生支持的,也就是说我们可以直接在浏览器中去执行import
,动态引入我们需要的模块,而不是把所有模块打包在一起。
ESM
使用实时绑定的模式,导出和导入的模块都指向相同的内存地址,也就是值引用
2、Esbuild
Vite底层使用Esbuild对ts\jsx\js
代码进行转化,是一个打包压缩工具,但打包速度是其他的10-100倍。支持加载器、压缩、打包、TreeShaking、SorceMap生成,总共提供了4个函数:transform、build、buildSync、Sevice
3Rollup
生成环境下使用Rollup打包,基于ESM的JS打包工具,能够打出更小的更快的包,因为Rollup基于ESM模块,比Webpack使用的CommonJS模块机制高效。此外,Rollup的亮点在于同一个地方。一次性加载,针对源码进行treeshaking(去除定义但是未使用的)以及sourceHoisting(作用域提升:直接将所有模块的代码合并到一个作用域中)以减小输出文件大小
Rollup
分为build
(构建)阶段和output generate
(输出生成)阶段。主要过程如下:
-
获取入口文件的内容,包装成
module
,生成抽象语法树AST -
对入口文件抽象语法树进行依赖解析
-
生成最终代码
-
写入目标文件
4、基于ESM的热更新
- 实现热更新的思路:通过
WebSocket
创建浏览器和服务端的通信监听文件的改变,当文件修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
VS Webpack
Vite
通过 chokidar
来监听文件系统的变更,只用对发生变更的模块重新加载, 只需要精确的使相关模块与其临近的 HMR
边界连接失效即可,这样HMR
更新速度就不会因为应用体积的增加而变慢而 Webpack
还要经历一次打包构建
Vite
整个热更新过程可以分成四步
-
创建一个
websocket
服务端和client
文件,启动服务 -
通过
chokidar
监听文件变更 -
当代码变更后,服务端进行判断并推送到客户端
-
客户端根据推送的信息执行不同操作的更新
Vite
还利用HTTP
加速整个页面的重新加载。设置响应头使得依赖模块(dependency module
)进行强缓存,而源码文件通过设置 304 Not Modified
而变成可依据条件而进行更新
5、预构建
- 支持CommonJS依赖
- 减少模块和请求数量:常用的lodash工具库,lodash-es包含几百个子模块,导致几百个Http请求,造成网络堵塞、影响页面加载
Vite
将有许多内部模块的 ESM
依赖关系转换为单个模块,通过预构建 lodash-es
成为一个模块
- 为什么使用Esbuild
一个字“快”
-
大多数前端打包工具基于JavaScript实现,解释型语言,边运行边解释,。而Esbuild基于Go语言,编译时将语言转为机器语言,启动时直接执行,尤其CPU密集场景下
-
多线程
JS是一门单线程的语言,直到引入WebWorker才有可能在浏览器、服务端实现多线程操作。Webpack源码也没有使用WebWorker提供的多线程能力;而Go天生的多线程优势
-
对构建流程优化了,充分利用CPU资源
- 实现原理
vite预编译后,将文件缓存在node_modules/.vite/
文件下,根据以下地方来决定是否需要重新执行预构建。
-
package.json
中:dependencies
发生变化 -
包管理器的
lockfile
五、Webpack
1. Webpack的理解,解决了什么问题
最初就是为了前端的模块化,高效管理和维护项目中的每一个资源
模块化发展;
-
通过文件划分形式,每个功能及其相关状态数据放在一个JS文件,再引入到页面,一个
主要问题:页面这引入模块,到那时模块的加载不受代码控制,导致维护困难,此外还需要规定模块化的规范
2、开发中问题
- 通过模块化进行开发
- 使用高级特性加快开发效率:TypeScript、ES6+,通过sass、less等方式编写CSS样式
- 监听文件的变化并且反映到浏览器上,提高开发效率
- JS代码需要模块化,HTML和CSS这些也面临着模块化
- 开发完成还需要将代码压缩、合并和进一步优化
3、Webpack
- 编译代码,提高效率,解决浏览器兼容问题
- 模块整合打包成.bundle.js,提高性能,解决浏览器频繁请求文件的问题
- 支持不同种类的前端模块类型,并且所有的资源文件都可以通过代码控制
六、Webpack的Loader和Plugin
1、不同
-
Loader
:加载器,将一切文件视为模块,但是Webpack只能解析JS文件,因此就需要Loader去加载非JS的资源文件 -
Plugin
:插件,扩展Webpack
的功能。,因为Webpack运行的生命周期会广播许多事件,Plugin监听这些事件,并在合适时机通过Webpack提供的API改变输出结果 -
不同的用法
-
Loader
:在module.rules
中配置,作为模块的解析规则存在,类型为数组,每一项都是Object,描述了对什么类型的文件,使用什么样的加载和使用的参数也可以通过CLI:shell命令中指定
或者内联方式:每个import语句中显示指定
-
plugin
:在plugins
中单独配置,类型为数组,每一项是plugin的实例,参数通过构造函数传入
-
2、常见的plugin
define-plugin
:定义环境变量html-webpack-pluugin
:简化html文件创建uglifyjs-webpack-plugin
:通过UglifyES
压缩ES6代码webpack-parallel-uglify-plugin
:多核压缩,提高压缩速度webpack-bundle-analyzer
:可视化webpack输出文件的体积mini-css-extract-plugin
:CSS提取到单独的文件中,支持按需加载
3、常见的Loader
- 实现对不同文件的处理,将Scss转为css,将TypeScript转为JS
- 编译文件,从而使其添加到依赖关系
在webpack.config.js
中单独用module进行配置
常用的loader:
file-loader
:将文件输出到一个文件夹中,在代码中通过相对url引用输出文件url-loader
:文件很小的时候,以base64的方式将内容注入到代码中image-loader
:加载并压缩图片文件babel-loader
:将ES6转为ES5css-loader
:加载CSS,支持模块化、压缩、文件导入style-loader
:css代码注入JS中,通过DOM操作CSSeslint-loader
:通过ESLint检查JS代码
4、Loader
的配置方式
loader的配置,我一般通过module.config.js
中进行配置,写在module.rules
属性中
rules
:数组形式,配置多个loader,[]包裹loader
:对应一个对象的形式,对象属性test为匹配规则;use属性针对匹配到的文件类型,调用对应的loader处理
module.exports = {module: {rules: [{test: /\.css$/,use: ['style-loader', // 将 CSS 注入 DOM{loader: 'css-loader',options: {modules: true // 启用 CSS Modules}},'sass-loader' // 处理 SCSS/SASS(注意:这里匹配的是 .css 文件,可能需要调整 test 正则)]}]}
};
loader支持链式调用,每个loader会处理之前已经处理过的资源,转为JS代码。执行顺序为相反的顺序执行
-特性
- 可以同步,也可以异步
- 运行在Node.js中,并能执行任何操作
- plugin可以为loader带来更多特性
- loader能产生额外的任意文件
七、Loader和Plugin的实现原理
八、Webpack的构建流程
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译
- 确定入口:根据配置中的entry找出所有文件的入口
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有的入口文件都经过了本步骤的处理
- 完成模块编译:使用loader翻译完所有模块后,得到每个模块的最终内容和他之间的关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表
- 输出完成:根据配置确定输出的路径和文件名,把文件内容写入到文件系统
1、如何提高Webpack的构建速度
-
升级Webpack版本:
-
使用缓存;配置文件中添加
cache:true
因此后续的构建中可以使用之前的结果 -
多进程/多实例:使用工具
happypack
和thread-loader
将构建任务分发到子进程中 -
tee-shaking:消除未使用的代码,只加载必要的资源
-
减少文件搜素范围:在配置中指定
resolve
的modules
和extensions
resolve:{modules: ['node_modules'],extensions:['.js', '.jsx', '.json'] },
-
使用高效的
loader
:选择性能较好的loader,可考虑使用babel-loader的cacheDirectory
选项缓存Babel的编译结果 -
优化图片:使用像
image-webpack-loader
这样loader优化图像文件,减少文件大小 -
Webpack性能分析:使用Webpack Bundle Analyzer工具分析构建输出,找出体积较大的模块,
-
使用更轻量级的插件
-
合理使用 source map:开发环境选
cheap - module - eval - source - map
这类轻量的,生产环境禁用或选更轻量的,用于调试代码时映射到原始代码,平衡调试体验和构建性能。 -
Webpack Parallel Build:借助
webpack - parallel - uglify - plugin
插件,并行压缩优化代码,利用多线程加速构建,提升打包效率 。
九、tree-shaking原理
- 一是基于 ES6 Module 的静态特性,其引入是静态可分析的,编译阶段能精准判断实际加载的模块;
不同于@CommonJS中require是动态的,运行时才知道具体加载哪个模块,因此打包九很难提前判断
- 二是借助静态分析程序流,找出未被使用、引用的模块和变量,将对应冗余代码删除,实现打包优化
- 构建依赖图:打包工具(如 Webpack 等)在解析代码时,从入口文件开始,根据 ES6 Module 的静态导入关系,逐步构建起整个项目的依赖图,明确各个模块之间的引用关系。
- 标记可到达代码:依据构建好的依赖图,对代码进行遍历,标记出所有在程序运行过程中能够被访问到的代码。比如一个函数被其他地方调用,或者一个变量被读取,那么包含它们的代码块就被标记为可到达。
- 删除未使用代码:遍历完成后,未被标记的代码就被认为是未被使用的,打包工具会将这部分代码从最终的打包结果中移除
十、CommonJS
和ES6 Modules
模块引入区别
- CommonJS:Node.js 专用模块规范,早年前端靠 AMD 等适配,本质为服务端模块方案。
- ES6 Module:语言标准层的通用方案,旨在统一浏览器 + 服务端,不过浏览器完整兼容还需过程,Webpack 等工具常转译为 CommonJS 用。
区别
- 值传递逻辑:
CommonJS 输出值的拷贝,模块内值变化不影响已引入的拷贝;
ES6 Module 输出值的引用,原始值变,引入处也会同步变(依赖静态解析特性 ) - 加载时机:
CommonJS 是运行时加载,执行到require
才去读文件、执行代码并生成模块对象;
ES6 Module 是编译时(静态)确定依赖,import
在编译阶段就解析模块接口,更早做优化(比如 Tree - shaking 就依赖这点 ) - CommonJS 一般单个值导出(
module.exports
赋值,或exports.xxx
挂载);
ES6 Module 支持多值导出(export
导出多个,export default
导出默认,写法更灵活 ) - 语法动态性:
CommonJS 模块引入是动态语法,require
能写在条件判断里(运行时决定加载啥);
ES6 Module 的import
是静态语法,必须写在代码顶层,不能放判断、循环里(保证编译时就能确定依赖 ) - this 指向差异:
CommonJS 模块里,this
指向当前模块自身(类似模块作用域的全局对象 );
ES6 Module 里,this
是undefined
(设计上更纯粹,模块作用域和普通脚本作用域区分开 )
这两种规范的差异,本质是 “运行时动态逻辑” vs “编译时静态优化” 的设计取舍。日常开发里,用 ES6 Module 写代码更符合现代工程化(方便 Tree - shaking、静态分析 ),但 Node.js 环境里 CommonJS 仍占主流场景。
十一、Webpack5的优化
更快构建(缓存和性能优化)
- 持久缓存:引入更稳定的
HashedModuleIdsPlugin
+NamedChunksPlugin
,构建缓存更持久,重复构建时能复用结果,大幅提速。 - 默认配置优化:开箱即用性能更好,像优化解析逻辑,减少不必要的编译步骤。
- 构建性能提升:底层做了性能优化,持久化缓存 + 内部流程优化,整体构建速度更快。
更优产物(Tree-shaking、代码分割)
- Tree - shaking 增强:对未使用代码的检测更精准,冗余代码剔除更彻底,打包体积更小。
- 代码分割更灵活:
optimization.splitChunks
重新设计,配置更灵活,能更智能地拆分代码(比如拆分公共依赖、动态导入模块 ),配合 动态导入(import()
) 支持升级,代码分割体验更顺滑。
更开放的生态
- 原生支持 WebAssembly(WASM):项目里用 WASM 更方便,无需额外复杂配置,能直接集成高性能模块。
- Module Federation(模块联邦):颠覆性功能!允许多个独立 Webpack 构建 “互联互通”,实现跨应用模块共享,特别适配微前端、微服务架构,打破应用边界,复用代码更轻松。
架构与配置演进
- 缓存组(Caching Groups):新增细粒度缓存策略,可针对不同模块(比如第三方库、业务代码 )单独配置缓存规则,灵活控制哪些代码走缓存、哪些重新构建。
- 移除废弃特性:清理过时 API 和功能,让架构更简洁,但升级时要注意兼容问题(可提前梳理项目依赖 )。