【自用】从模块化背起的前端工程化
博主有一天向ai问了一个神金的问题:是谁先发现代码需要打包的?^^ 博主没有经过知识污染的大脑就是这样的干净。作为学生,在自己做项目练手的时候,从来都是在开发环境下,很少有接触到生产环境。打包/构建/工程化/模块化/...,这种概念从来没有出现在博主的脑子里。然而八股文要考,于是博主只能硬着头皮理解了。
所以这是一个博主在阅读了部分八股文,拷打ai,加上个人理解之后的一篇关于前端工程化的自用总结文章。
一、模块化——代码组织的基本法
关于前端工程化,我想从模块化讲起。
1.1 早期方案
早期前端项目规模小,代码直接通过<script>
标签引入浏览器。
<!-- 传统的开发方式 --> <script src="jquery.js"></script> <script src="utils.js"></script> <script src="module1.js"></script> <script src="module2.js"></script> <script src="app.js"></script>
但由于所有变量都在全局作用域,造成全局命名空间污染的问题。并且,脚本加载顺序必须手动管理,依赖关系十分混乱。早期通过IIFE(立即执行函数)、命名空间等手动隔离作用域。
慢慢地,社区提出一些模块化理念以及规范。
1.2 社区规范
CommonJS 规范:同步加载,Node.js的默认模块系统,不适合浏览器。使用`require()` 和 `module.exports`。
2010年 Node.js 开始使用 CommonJS。2011 年 Browserify 将 CommonJS 转换后可在浏览器使用;2012 年 webpack 出现。
// math.js exports.add = function(a, b) { return a + b; }; // app.js const math = require('./math'); console.log(math.add(1, 2)); // 在浏览器中使用 require const $ = require('jquery'); const utils = require('./utils');
AMD规范(Asynchronous Module Definition):异步加载,适合浏览器。
// 使用 RequireJS define(['jquery', 'underscore'], function($, _) { return { init: function() { // 模块逻辑 } }; });
1.3 标准统一
一直到2015年,官方正式规定了ES6模块标准化ESM。2017年,浏览器开始支持ESM。2020年,Node开始支持ESM。
// math.js export const add = (a, b) => a + b; export const subtract = (a, b) => a - b; export default class Calculator { // ... } // app.js import Calculator, { add, subtract } from './math.js';
二、包管理器——依赖管理自动化
刚刚说到模块化,包就是模块的进一步集合。模块化后,项目依赖大量第三方模块。而各类模版可能会迭代更新,如果手动下载、更新、管理,效率极低且容易出现版本冲突。
包管理器的核心价值就是通过自动化工具解决上述痛点,实现依赖的 “安装、更新、卸载、版本控制” 全流程自动化。
2.1 主要功能
- 自动安装依赖:根据 package.json 中的依赖声明,一键下载所需模块及其子依赖,无需手动寻找资源地址。
- 版本控制与冲突解决:通过语义化版本规则(SemVer)管理版本范围,自动处理依赖间的版本兼容问题。
- 维护依赖树快照:生成 lock 文件(如 package-lock.json、yarn.lock),记录每个依赖的精确版本和依赖关系,确保不同环境安装的依赖完全一致。
- 脚本执行:通过 package.json 的 scripts 字段定义快捷命令(如 npm run dev、yarn build),简化开发流程(如启动开发服务器、执行构建)。
2.2 主流包管理器对比
|
|
| |
简介 | 最早的前端包管理器,随 Node.js 普及 | 解决了早期 npm 的性能和一致性问题 | 基于硬链接和符号链接 |
特点 | 嵌套树——>扁平树 | 扁平树 并行安装、全局缓存机制、依赖树预解析、原子安装 | 基于内容寻址的存储 + 硬链接/符号链接 |
优点 | 生态最成熟 兼容性强 无需额外安装 | 早期性能优于 npm 支持离线缓存 语法更简洁 | 速度极快 磁盘空间利用率高 对monorepo友好 |
缺点 | 安装速度较慢 磁盘空间利用率较低 | 需额外安装 性能被追赶 | 可能有兼容问题 概念学习成本较高 社区支持略差 |
lock 文件 |
|
|
|
适用场景 | 中小型项目、追求兼容性和生态稳定性的场景 | 习惯简洁语法、需要离线安装支持的团队 | 大型项目(如 monorepo 多包) 对安装速度和磁盘空间敏感的场景 |
2.3 依赖树方案对比
包管理器的依赖管理方案经历了从嵌套树、扁平树到 pnpm 的硬链接 + 符号链接的演进,具体对比如下:
① 嵌套树(npm v2 及之前)
严格按照依赖声明的层级关系,在node_modules中嵌套存储。例如,A 依赖 B,B 依赖 C,则A/node_modules/B/node_modules/C。
- 优点:
每个依赖独立拥有自己的子依赖,依赖结构清晰。
无 “幽灵依赖” 问题(只能访问项目声明的依赖)。
- 缺点:
磁盘空间浪费严重,同一包的不同版本可能在多个嵌套层级中重复存储,尤其深层依赖时体积爆炸。
性能问题,嵌套层级过深可能导致文件系统访问效率低(尤其 Windows )。重复下载相同包,耗时更长。
② 扁平树(npm v3+、Yarn)
将嵌套依赖 “提升” 到顶层node_modules,复用相同版本的包,减少重复。例如,A 依赖 **********,C 依赖 **********,则 ********** 会被提升到顶层,供 A 和 C 共享。
- 优点:
减少重复依赖,节省部分磁盘空间。
安装速度比嵌套树快。
- 缺点:
依赖结构不稳定,提升规则受安装顺序、版本范围影响,可能生成不同的依赖树,导致不一致问题。
幽灵依赖,可能访问项目声明外的依赖。
③ pnpm 的硬链接 + 符号链接方案
全局存储:所有下载的包会被存入一个全局store共享存储,不同项目通过硬链接复用,避免重复存储。
依赖树:使用符号链接(软链接)指向全局存储中的包,并严格按照依赖声明构建层级关系,但实际文件不重复。
硬链接类似文件分身,和源文件共享一个inode和数据块。软连接则像一个快捷方式。
- 优点:
节省磁盘空间,同一版本的包仅存储一次,通过硬链接共享,空间利用率远高于前两者。
安装速度快,无需重复下载和存储,依赖解析逻辑简单,效率高于扁平树。
依赖结构稳定,无提升逻辑,严格遵循声明的依赖关系,无幽灵依赖。
- 缺点:
对符号链接支持较差的环境可能存在兼容性问题(部分旧系统)。
学习成本略高,需熟悉硬链接和符号链接的工作原理。
三、构建与打包——从源码到可部署的资源
3.1 构建
开发时为了方便编写,我们的文件很分散;我们的语法很便捷,很易读;我们的依赖错综复杂;我们的资源类型五花八门。但当我们需要给其他人使用时,需要申请域名,需要把代码放在服务器上。有其他人访问网站,我们的业务逻辑谁来隐藏?部署到服务器后,没有了开发工具的 “实时翻译” ,我们写的高级语法谁转换后再给浏览器?谁考虑繁多的文件资源造成的网络请求?总而言之,开发环境下的代码是无法直接部署到生产环境的,那么就需要构建来进行转换。
构建是连接 “开发友好” 与 “生产可用” 的核心桥梁。在项目的开发和部署流程中,为了达到“某些目的”(如下),需要将文件转化成适合生产环境运行的资源,这就叫构建(build)。
而具体的目的是什么呢,将构建的目的作以下归纳整理:
- 压缩体积:打包可以减少文件体积和数量,比如合并零散文件为chunk(可减少浏览器的HTTP请求次数),减少冗余资源(tree-shaking:剔除条数代码、注释、未使用的变量),压缩算法(对于css,js,图片等)等可以优化加载性能。
- 适配运行环境:开发环境可能使用ES6+语法,Sass/Less 等预处理语言或 TypeScript 等,而部分旧浏览器或运行环境不支持这些语法,转换可将其编译为兼容的代码(如 ES5)。
- 提升代码安全性:通过混淆、压缩等处理,除了html元素外(浏览器的 DOM 解析与渲染机制),源代码不易被解读。
从具体的流程讲讲构建和打包。
构建的详细流程:
1、检查:如 ESLint ,TS校验
2、解析依赖:从文件入口开始解析模块依赖,如import/require
3、分割合并:代码分割成chunk,再合并成最终的bundle
4、转译:如 ES6,TS,Sass,PostCSS
5、优化:Tree-Shaking、资源优化(图片、字体)
6、输出:到dist 目录
7、部署:部署到生产环境
最终生成的生产文件通常存放在dist目录下:入口文件为index.html(部分项目可能有多个入口);assets子目录中则包含经过处理的 JS、CSS、图片、字体等资源文件,文件名常带有哈希值(如app.8f2d7.js),能实现缓存控制。
常用的构建(打包)工具
Webpack:功能全面,支持处理多种资源。loader 转换非 JS 资源,plugin 扩展功能。适合大型复杂项目
Vite:基于 ES Modules 和esbuild的新一代构建工具,开发环境无需打包,生产环境用 Rollup 打包。
Rollup:专注于 js 打包,输出更简洁的代码(Tree-Shaking 支持更好),适合如 UI 组件库、工具库等项目。
esbuild: 由 Go 语言编写的超快 JavaScript 打包器、转译器和压缩器。
Parcel:零配置打包工具,自动处理依赖和资源,适合快速原型开发。
下面根据刚刚整理的构建流程,列举每个流程常用的工具(随便举举):
1️⃣ 代码检查
Prettier:代码格式化。
ESlint:js/ts的语法检查。
Stylelint:css/sass/less的语法检查。如是否符合BEM
TypeScript:类型检查。
2️⃣ 解析依赖
webpack-resolve:webpack 内置的依赖解析机制,处理模块查找规则。
enhanced-resolve:webpack底层使用的依赖解析库,支持自定义解析逻辑。
esbuild:Vite 等工具使用的快速依赖解析器,处理 ES 模块导入导出关系。
resolve:Node.js 生态中常用的依赖路径解析库,可用于自定义脚手架或构建工具的依赖查找。
3️⃣ 分割合并
Webpack: optimization.splitChunks
Vite: build.rollupOptions.output.manualChunks
Rollup: output.manualChunks
4️⃣ 语法转译
Babel:通过插件(Plugins)和预设(Presets)处理语法转换。
TypeScript Compiler/tsc:TypeScript 官方编译器
Sass/Less:CSS预处理器
PostCSS:CSS后处理器
5️⃣ 优化
webpack
babel-loader (Loader) - JS/TS 转译
css-loader (Loader)+style-loader (Loader) - CSS 处理、注入
file-loader (Loader) - 文件处理
HtmlWebpackPlugin (Plugin) - HTML 生成
MiniCssExtractPlugin (Plugin) - CSS 提取
CleanWebpackPlugin (Plugin) - 清理输出
vite(都是plugin)
vitejs/plugin-vue - Vue 支持
vite-plugin-eslint - ESLint 集成
vite-plugin-compression - 资源压缩
rollup(都是plugin)
rollup/plugin-babel - Babel 转译
rollup/plugin-terser - 代码压缩
rollup-plugin-postcss - CSS 处理
6️⃣ 输出
Source Maps:源码映射文件生成
Manifest 文件:资源清单生成,版本号
7️⃣部署
Docker:容器化部署
CDN 上传:静态资源 CDN 部署
3.2 打包
打包:转译、解析依赖、分割合并、优化
流程:建立依赖树后,根据代码分割策略将依赖树拆分为多个chunk。对每个chunk应用loader处理(如转译),和plugin优化(如压缩)。最终生成多个分割后的bundle文件。
Bundle(包):最终输出的文件包,包含应用的所有代码和资源。
Chunk(块):Bundle 的组成部分,可以按需加载的代码片段,用于代码分割和优化。
分包策略最佳实践
1️⃣ 按功能分包
manualChunks: { 'auth': ['./src/pages/login', './src/pages/register'], 'dashboard': ['./src/pages/dashboard'], 'profile': ['./src/pages/profile'] }
2️⃣ 按依赖大小分包
manualChunks(id) { if (id.includes('node_modules')) { // 大型库单独分包 if (id.includes('echarts')) return 'echarts'; if (id.includes('moment')) return 'moment'; if (id.includes('antd')) return 'antd'; // 小型库合并 return 'vendor'; } }
3️⃣ 按加载优先级分包
manualChunks: { 'critical': ['react', 'react-dom'], // 关键依赖 'common': ['lodash', 'dayjs'], // 通用工具 'features': ['./src/features'] // 业务功能 }
四、其他
测试
单元测试:Jest、Vitest、Mocha 等
集成测试:Testing Library、Enzyme 等
端到端测试:Cypress、Playwright、Puppeteer 等
视觉回归测试:Chromatic、Percy 等
测试覆盖率:Istanbul、c8 等
部署
持续集成/持续部署 (CI/CD):GitHub Actions、GitLab CI、Jenkins 等
自动化构建:多环境构建、版本管理
自动化测试:测试流水线、自动化回归测试
自动化部署:蓝绿部署、滚动更新等
版本控制与协作
Git 工作流:GitFlow、GitHub Flow 等
分支管理策略:功能分支、发布分支管理
提交规范:Conventional Commits、Commitizen
变更日志:自动生成 CHANGELOG