
2026-07-21 | 前端工程化
前端工程化
omnijk
前端工程化通过模块化拆分代码、构建打包编译优化、规范化约束团队,解决复杂项目中的维护、性能和协作难题,实现高效交付。
随着项目复杂度不断提高,前端工程化已成为一种必然。
模块化
随着JS的快速发展,项目的代码量也逐渐提升,单纯将所有代码写在一个文件里已经难以维护。 没有模块化,所有的JS变量都挂在windows中,命名冲突、全局变量污染等问题接踵而至。有了模块化,我们可以按照功能将JS代码拆成一个个独立的模块,一个文件就是一个模块,模块之间可以引入、使用,在提高代码独立性、复用性的同时,也让模块之间的依赖关系变得更加明晰。
早期的模块化是通过立即执行函数(IIFE)来实现的,通过创建一个新的作用域,避免了命名冲突、全局变量污染等问题,但是它仍然无法解决模块之间的依赖问题,真正的模块化应运而生,包括AMD、CMD、CJS、ESM等,现在以CJS、ESM为主。 其中CJS主要用于node环境,ESM则是ECMA Script官方发布的模块化标准,早期主要用于浏览器环境,现在node也支持ESM模块化。
来一道工程化必问的面经看看~
CJS、ESM的区别
1.语法层面
- CJS导入:
const a = require('./a.js'); - CJS导出:
module.exports = { name: 'cjs', age: 10 }; - ESM导入:
import { add } from './math' - ESM导出:
export default {add,name}
注意ESM只能有一个默认导出,CJS的module.exports是一个对象。
2.模块加载方式和时机
(1)CJS运行时同步加载、动态加载
require()本质上是一个函数,只有代码执行到这一行才会读取它引用的模块。
- 同步加载:只有
require()函数执行完了,才会执行后面的代码; - 动态加载:
require()导入语句可以放在if条件判断语句中,根据条件判断是否执行。
(2)ESM编译时静态分析
在代码编译阶段,就会扫描所有代码,确定模块之间的依赖关系图。
- 异步加载:
import导入语句不阻塞后续代码的正常编译; - 静态加载:
import语句必须写在最顶层,不能放在条件或者函数里,并且会自动提升到模块顶部执行
3.缓存机制不同
(1)CJS缓存的是module.export对象的值拷贝,即使模块内部后续修改了导出的值,通过require获取到的还是最初导出时缓存的结果。
(2)ESM缓存的是值的引用,导出的变量会和原模块保持动态关联,同步更新。
4.this指向不同
CJS 中:this 指向 module.exports;
console.log(this === module.exports); // true
ESM 默认使用严格模式"use strict",严格模式下全局 this 是 undefined。
5.__dirname 和 __filename
CJS中可以直接使用__dirname(当前模块所在目录) 和 __filename(当前模块的完整路径)。这是因为包裹函数提供了这两个参数:
(function(exports, require, module, __filename, __dirname) {
// __filename 和 __dirname 是参数传进来的,可以直接使用
// 模块代码位置
});
ESM 中不存在,需要用 import.meta.url 获取当前模块文件的url
构建打包
为什么需要打包?
(1)浏览器只认识原生的HTML、CSS、Javascript
为了开发的方便,你使用了很多浏览器不认识的东西:
- Scss\Less\TailWindCSS
- JSX\VUE SFC
- 模块化CJS
- 新语法(部分老版本浏览器不支持)
打包工具的作用:将这些高级语言、框架语法翻译成浏览器认识的代码
(2)开发环境和生产环境的追求不同
| 开发环境 | 生产环境 |
|---|---|
| 不压缩代码,方便调试 | 极致的压缩,以提高加载速度 |
| 使用新语法、更好用 | 兼容老版本浏览器 |
| 源码映射 | 代码混淆 |
打包工具干了啥
把 Webpack 想象成一个流水线工厂,你扔进去一堆“原材料”(各种前端文件),它按规则加工,最后生成一份份“成品”(打包后的 JS/CSS/图片等)。
- 1.Entry(入口)——从哪开始干活?
entry: './src/main.js'
Webpack 会从入口文件 main.js 开始,顺着 import / require 一层层找依赖,把所有用到的文件都拉进来打包。
- 2.Output(出口)——打包完放哪?叫啥名?
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
打包完的文件放在dist文件夹下,名为bundle.js
- 3.Loader ——遇到什么文件,怎么处理(通过配置
rules)
Webpack 只认识 JavaScript 和 JSON。Loader 就是翻译官 + 加工厂。
module: {
rules: [
{
test: /\.css$/, // 遇到 .css 文件
use: ['style-loader', 'css-loader'] // 用这两个 loader 处理
}
]
}
Loader执行顺序:从右到左,从下到上
.css 文件-->css-loader处理-->style-loader处理-->最终输出
- 4.Plugin(插件)——流水线上的“增强外挂”
凡是 Loader 做不了的“额外工作”,基本都是 Plugin 的活。
plugins: [
new HtmlWebpackPlugin({
// 自动生成 HTML 文件并自动注入打包后的 JS/CSS 资源
template: './public/index.html'
// 基于这个模板文件生成最终的 index.html
})
]
- 5.Mode(模式)——切换“开发 / 生产”环境
mode: 'development' // 或 'production'
开发模式:不压缩代码,热更新速度快、有报错提示;
生产模式:自动压缩、优化、剔除无用代码(摇树优化等方式)。
- 6.Bundle(打包结果)——得到最终产物
构建工具webpack和vite的区别
两者都是目前主流的构建工具,其中vite热更新、冷启动速度都更快。区别主要在开发环境中体现,启动开发服务器时:
(1)Webpack —— 开张前先全做出来
- 从 entry 开始递归解析所有 import/require,构建完整依赖图
- 用 Loader 转译所有文件(TS→JS、Vue→JS…)
- 打包合并成 bundle.js,放内存
- 才启动服务器让你访问
(2)Vite —— 只开个厨房,现点现做
- 不解析、不打包业务代码,直接启动轻量 HTTP Server
- 浏览器通过原生
type="module"直接请求需要的模块 - Vite 拦截请求 → 按需用 esbuild(Go 编写,比 Babel 快 10~100 倍)实时转译 → 返回给浏览器
- 第三方依赖做一次预构建(Pre-bundling) 转成 ESM 并缓存,解决请求瀑布流和CJS不能在浏览器环境中的问题
资源缓存
早期互联网时代,流量带宽是很大的成本,为了降低服务器负载和提升用户访问速度,引入了资源缓存。缓存策略是成本最低的性能优化策略,是指浏览器采用最优方式访问资源,分为强缓存和协商缓存,用于提高二次访问速度。
强缓存:首次请求时,直接缓存资源(包括资源存多久);后续再请求相同资源时,只要没过期,直接使用缓存,不会向服务器发任何请求。
协商缓存:首次请求缓存资源,后续请求相同资源时,先向服务器发请求问资源更新了吗?没更新-->服务器响应304(Not Modified),使用本地缓存;资源更新了-->服务器响应200(ok)+新资源。
具体使用哪一种缓存策略由服务器返回的响应头控制:
- 不适用任何缓存:
Cache-Control: no-store - 强缓存:设置
max-age=3600(缓存有效时间为3600s)或者Expires: Mon, 25 Jul 2025 12:00:00 GMT(具体的过期时间) - 协商缓存:设置
ETag: "686897696a7c876b7e"(资源的唯一标识,可通过对比标识符判断资源是否更新)或者Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT(资源上次更新时间);再次请求相同资源时,浏览器带着If-None-Match或If-Modified-Since去问服务器,服务器返回 304 就用缓存,返回 200 就用新资源。
资源缓存方案
- 不经常变动、带哈希的静态资源:强缓存+较长的缓存时间(资源内容发生变动时,文件名也变,旧缓存自动失效)
- 偶尔变动、不带哈希的资源:协商缓存(内容可能更新,定期询问)
- 要求实时性、经常变动的资源(如入口文件):不缓存
对访问速度要求更高的网站未止步于仅仅利用缓存策略,因为缓存策略只能提高二次访问速度;对于资源首次访问,最常见的提速手段是CDN内容分发网络,核心是就近获取资源,即把资源存储在离用户很近的CDN服务器上,在避免服务器过载的同时提高访问速度。可以理解为京东在全国各地的主要城市建设本地仓库(CDN服务器),当用户购买商品时根据收货地址智能匹配离用户最近的仓库发货,极大地提高了物流配送时间。
打包体积优化
打包是将项目源码转换成最终上线的代码的过程,但打包之后的代码体积过大,导致首屏加载时间久、用户交互卡顿、响应慢等问题,亟需解决,目前比较常用的较小打包体积的办法如下:
- 1.摇树优化:在打包阶段,通过静态分析 ES Module 的 import/export 关系,删掉没用到的代码
(1)把每个文件解析成抽象语法树,找出所有 import / export 声明;
(2)从入口 main.js 出发,构建依赖图;
(3)引用标记:顺着依赖图,用到的标记alive,没用到的标记dead;
(4)副作用检查:标记dead + 无副作用 才删除。
!!由于必须静态可分析,因此只对ES Module生效。
- 2.压缩资源:压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频
(1)HTML/CSS/JS代码:通过配置插件Plugin压缩
(2)字体/图像/音频/视频:部署到生产环境之前使用专业的工具压缩
- 3.按需加载:将路由界面/触发性功能单独打包成一个chunk,使用时加载、 首屏不需要的资源懒加载等
规范化
- 代码规范:Eslint
ESLint 负责发现代码中的逻辑问题和潜在错误,比如未使用的变量、可能的 bug、不安全的写法。
- 代码格式化:Prettier
Prettier 负责代码风格的统一,比如缩进、引号、分号、换行等。它不关心代码逻辑,只关心外观。
- 代码提交规范:Angular Commit Convention
Angular提交规范的格式包括 Header、Body 和 Footer 三个内容。Header 为必填项,Body 与Footer为可选项,在此不再赘述。header部分只写一行,包含以下三个字段:
(1)type:用于说明commit 的提交类型,也可加括号说明本次改动的影响范围,必选
(2)scope:用于说明commit 的影响范围,可选
(3)subject:用于说明 commit 的细节描述,可选
常见type如下
| type | 含义 | 示例 |
|---|---|---|
feat | 新功能 | feat(user): 新增用户登录功能 |
fix | Bug 修复 | fix(button): 修复按钮点击无响应 |
docs | 文档变更 | docs(readme): 更新安装说明 |
style | 代码格式(不影响逻辑) | style: 格式化代码 |
refactor | 重构 | refactor(api): 重构请求模块 |
perf | 性能优化 | perf(list): 优化虚拟滚动性能 |
test | 测试相关 | test(utils): 添加单元测试 |
chore | 构建/工具变更 | chore(deps): 升级依赖版本 |
ci | CI/CD 配置变更 | ci: 添加 GitHub Actions |