本文是一个工程化培训的初稿
今天我们来聊一聊前端工程化这个主题。在开始之前,我想先问大家一个问题:当你加入一个新的前端团队时,是否遇到过这样的困扰 —— 代码风格不统一、构建流程复杂、部署过程繁琐?这些问题的解决方案,就是我们今天要讨论的前端工程化。
1. 前端工程化概述
1.1. 什么是前端工程化
1.1.1. 模块化
随着项目越来越大,我们需要将代码拆分成模块,实现代码的解耦和复用。这不仅包括JavaScript的模块化,还包括CSS的模块化,以及各种静态资源的模块化管理。比如我们熟悉的ES Module、CSS Modules等,都是模块化的具体实践。
模块化指将一个复杂应用根据预设规范封装为多个块并组合起来,对内实现数据私有化,对外暴露接口与其它模块通信。
1.1.2. 组件化
组件化是现代前端开发的基石,我们将页面拆分成一个个独立的组件,每个组件都有其独立的功能和职责。像React、Vue这样的框架,都是基于组件化的思想构建的。组件化不仅提高了代码的复用性,还让我们的开发更加高效和可维护。
组件化指将一个具备通用功能的交互设计划分为模板、样式和逻辑组成的功能单元,对内管理内部状态满足交互需求,对外提供属性接口扩展用户需求。
1.1.3. 规范化
规范化包括代码规范、目录结构规范、文档规范等。想象一下,如果团队中每个人都按自己的习惯写代码,代码风格各不相同,这会给代码维护带来很大困扰。通过ESLint、Prettier这样的工具,我们可以强制执行统一的代码规范。
规范化指将一系列预设规范接入工程各个阶段,通过各项指标标准化开发者的工作流程,引导开发者在团队协作中往更好的方向发展。
1.1.4. 自动化
自动化是工程化的重要特征,包括自动化构建、自动化测试、自动化部署等。比如使用Jenkins或GitHub Actions实现持续集成和持续部署,使用Jest进行自动化测试等。自动化能够大大提升开发效率,减少人为错误。
自动化指将一系列繁琐重复的工作流程交由程序根据预设脚本自动处理,整个工作流程无需人工参与,以解放开发者双手让其更专注业务需求的开发。
1.2. 为什么要前端工程化
- 维护性。首先是项目复杂度的提升。现代前端项目已经不再是简单的页面开发,而是演变成了复杂的工程。代码量成倍增长,多人协作成为常态,如何保证代码质量和项目可维护性成为一大挑战。
- 团队技术栈统一。技术栈的繁荣发展,前端技术发展非常快,新的框架和工具层出不穷。如何在众多技术中做出选择,如何保证团队技术栈的统一,这些都需要工程化的思维来解决。
- 性能要求。用户对网站的性能要求越来越高,如何优化资源加载、如何提升运行时性能,这些都需要在工程化层面进行思考和解决。
核心要解决的还是降本、增效。
2. 前端模块化
2.1. 模块化的意义
模块化是指将一个复杂的系统分解为多个模块以方便编码。对于前端来说,模块化可以带来以下好处:
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
- 按需加载
2.2. 模块化的演进
2.2.1. 全局函数时代(早期)
这是最原始的方式,直接定义全局函数:
function foo() {
// ...
}
function bar() {
// ...
}
问题:
- 全局变量污染
- 命名冲突
- 依赖关系不明确
2.2.2. 命名空间模式(namespace)
为了解决全局变量污染的问题,开始使用命名空间:
var MyApp = {
foo: function() {
// ...
},
bar: function() {
// ...
}
};
MyApp.foo();
问题:
- 依赖关系不明确
- 没有私有空间
2.2.3. IIFE(立即执行函数)模式
使用闭包实现模块化:
var Module = (function(){
var private = 'private variable';
return {
publicMethod: function(){
console.log(private);
}
}
})();
特点:
- 实现了私有空间
- 避免了全局污染
- 依赖关系仍不明确
2.2.4. CommonJS规范
Node.js采用的模块化规范,使用require进行同步加载:
// math.js
module.exports = {
add: function(a, b) {
return a + b;
}
};
// main.js
var math = require('./math');
math.add(2, 3);
特点:
- 同步加载
- 模块可以多次加载,但只会在第一次运行时运行一次
- 模块加载会缓存结果
- 主要用于服务器端
2.2.5. AMD规范(Asynchronous Module Definition)
专门用于浏览器端的异步加载模块规范,最著名的实现是RequireJS:
define(['jquery', 'underscore'], function($, _) {
return {
method: function() {
// 方法实现
}
};
});
require(['module1', 'module2'], function(m1, m2) {
// 使用模块
});
特点:
- 异步加载
- 依赖前置
- 专门用于浏览器端
2.2.6. CMD规范(Common Module Definition)
SeaJS推广的规范,类似AMD但有所不同:
define(function(require, exports, module) {
var $ = require('jquery');
module.exports = {
method: function() {
// 方法实现
}
};
});
特点:
- 异步加载
- 依赖就近
- 更接近CommonJS写法
2.2.7. UMD(Universal Module Definition)
统一模块定义,兼容AMD和CommonJS:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// 浏览器全局变量
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
return {};
}));
2.2.8. ES6 Module
现代JavaScript的标准模块化方案:
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// main.js
import { add, subtract } from './math';
特点:
- 静态分析,编译时确定依赖关系
- 声明式语法
- 支持循环依赖
- 输出值的引用
2.3. 现代模块化实践建议
- 优先使用ES6 Module
- 静态分析有利于tree shaking
- 语法简洁清晰
- 浏览器原生支持
- 构建工具配合
- 使用Webpack、Rollup、Vite等构建工具
- 处理模块依赖
- 代码分割和懒加载
- 动态导入
const module = await import('./module.js');
- 考虑兼容性
- 需要兼容旧浏览器时使用Babel转译
- 可以考虑UMD格式发布npm包
2.4. 总结
JavaScript模块化经历了从无到有、从简单到复杂的演变过程:
- 全局函数 → 命名空间 → IIFE
- CommonJS → AMD → CMD
- UMD → ES6 Module
每个阶段都解决了特定的问题,最终在ES6 Module中达到了相对完美的状态。选择合适的模块化方案需要考虑项目需求、浏览器兼容性等因素。
3. 前端构建
3.1. 什么是前端构建?
前端构建是将源代码转换成可以在浏览器端运行的静态资源的过程。它主要解决以下问题:
- 代码转换:TypeScript编译、SCSS转CSS等
- 文件优化:压缩JavaScript、CSS、HTML代码,压缩合并图片等
- 代码分割:提取多个页面的公共代码,提取首屏不需要执行部分的代码让其异步加载
- 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件
- 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器
- 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过
3.2. 构建工具发展历程
3.2.1. Grunt/Gulp时代
- 特点:基于任务流的构建工具
- 工作原理:定义一系列任务,按照顺序执行
// Gulp示例
gulp.task('css', function () {
return gulp.src('src/styles/*.css')
.pipe(minifyCSS())
.pipe(gulp.dest('build/styles'));
});
3.2.2. Webpack时代
- 特点:一切皆模块,完整的模块化方案
- 核心概念:Entry、Output、Loader、Plugin
// webpack.config.js基本配置
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}
3.2.3. Vite/Snowpack/RsBuild时代
- 特点:基于ES Module的开发服务器
- 优势:开发环境零构建,按需编译
- 生产环境:使用Rollup打包
3.3. 主流构建工具对比
3.3.1. Webpack
优势:
- 功能强大,生态完善
- 配置灵活,可定制性强
- 适合复杂大型项目
劣势:
- 配置复杂
- 构建速度相对较慢
- 学习成本高
3.3.2. Vite
优势:
- 开发环境启动快
- 配置简单
- HMR速度快
劣势:
- 生态相对Webpack少
- 部分场景兼容性问题
针对vue3的场景,目前支持很完善,应该首选
3.3.3. Rollup
优势:
- 打包结果更加清晰
- Tree Shaking效果好
- 适合库的打包
劣势:
- 插件生态不如Webpack
- 功能相对较少
3.3.4. RsBuild
优势:
- 主打性能
劣势:
- 生态差
- 插件少
3.4. 构建工具核心原理
3.4.1. 模块化处理
// 简化版的模块化原理
function require(moduleId) {
const module = {
exports: {}
};
// 模块代码放在这个函数里运行
((module, exports) => {
// 模块的真实代码
})(module, module.exports);
return module.exports;
}
3.4.2. 2. 依赖图谱
构建工具会从入口文件开始,分析整个项目的依赖关系,形成依赖图谱:
{
"src/index.js": {
deps: ["./app.js", "./router.js"],
code: "实际代码"
},
"src/app.js": {
deps: ["./components/header.js"],
code: "实际代码"
}
}
3.4.3. 3. 核心步骤
- 读取入口文件
- 分析依赖
- 转换代码
- 生成bundle
- 输出文件
3.5. 最佳实践建议
3.5.1. 开发环境优化
- 使用webpack-dev-server热更新
- 合理使用sourceMap
- 避免使用压缩
3.5.2. 2. 生产环境优化
- 开启代码压缩
- 启用Tree Shaking
- 合理分包
- 使用CDN加速
- 开启Gzip压缩
3.5.3. 3. 通用优化策略
- 缓存优化
- 并行构建
- 构建结果分析
// webpack配置示例
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
}
3.6. 构建工具选择建议
- 中小型项目
- 推荐使用Vite、Create-React-Scripts、Umi
- 配置简单,开发体验好
- 大型项目
- 推荐使用Vite@6、Umi
- 稳定性好,生态完善
- 组件库开发
- 推荐使用Dumi、Vite
4. 前端规范
4.1. 代码规范
基于eslint、tslint、stylelint进行统一约束。
4.1.1. 举例:JavaScript/TypeScript 编码规范
4.1.1.1. 命名规范
- 变量命名:使用驼峰命名法(camelCase)
// 良好的命名
let userName = 'John';
let firstName = 'Mike';
// 避免的命名
let user_name = 'John';
let firstname = 'Mike';
- 常量命名:使用大写字母和下划线
const MAX_COUNT = 100;
const API_BASE_URL = 'https://api.example.com';
- 类名命名:使用帕斯卡命名法(PascalCase)
class UserProfile {
constructor() {
// ...
}
}
4.1.1.2. 代码格式化
- 使用工具:ESLint + Prettier
- 缩进:2空格或4空格(团队统一)
- 语句结尾:统一使用分号
- 最大行长:80-120字符
- 空行:关键代码块之间保留一个空行
- ...
4.1.1.3. 1.3 注释规范
/**
* 函数描述
* @param {string} param1 - 参数1的描述
* @param {number} param2 - 参数2的描述
* @returns {boolean} 返回值描述
*/
function exampleFunction(param1, param2) {
// 实现逻辑
}
4.1.2. 举例:CSS 编码规范
4.1.2.1. 2.1 命名规范
- 使用BEM命名方法论:Block__Element--Modifier
.block {}
.block__element {}
.block--modifier {}
/* 示例 */
.card {}
.card__title {}
.card--featured {}
4.1.2.2. 式书写顺序
- 定位属性:position、top、right、z-index、display、float等
- 自身属性:width、height、padding、border、margin等
- 文字样式:font、line-height、text-align等
- 视觉效果:background、color、opacity、box-shadow等
- ...
4.1.3. 举例:HTML 编码规范
- 标签必须闭合
- 属性使用双引号
- 语义化标签的正确使用
<!-- 推荐 -->
<article>
<h1>标题</h1>
<section>
<h2>子标题</h2>
<p>正文内容</p>
</section>
</article>
4.2. 协作规范
4.2.1. Git 工作流规范
4.2.1.1. 1.1 分支管理
- 主分支:master/main
- 开发分支:develop
- 功能分支:feature/*
- 修复分支:hotfix/*
- 发布分支:release/*
gitGraph
commit
branch develop
checkout develop
commit
branch feature/login
checkout feature/login
commit
commit
checkout develop
merge feature/login
branch release/1.0
checkout release/1.0
commit
checkout main
merge release/1.0
4.2.1.2. 提交信息规范
类型(type):
- feat: 新功能
- fix: 修复bug
- docs: 文档更新
- style: 代码格式调整
- refactor: 重构
- test: 测试相关
- chore: 构建过程或辅助工具的变动
4.2.2. 2. 项目文件结构规范
src/
├── assets/ # 静态资源
├── components/ # 组件
├── hooks/ # 自定义hooks
├── layouts/ # 布局组件
├── pages/ # 页面
├── services/ # API服务
├── stores/ # 状态管理
├── styles/ # 样式文件
├── types/ # TS类型定义
└── utils/ # 工具函数
4.2.3. 代码审查规范
4.2.3.1. 3.1 Review 检查清单
- 代码是否符合编码规范
- 业务逻辑是否正确
- 是否有潜在的性能问题
- 是否有安全隐患
- 测试覆盖是否完整
- 文档是否更新
4.3. 自动化规范工具
4.3.1. 提交前检查
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"src/**/*.{css,less,scss}": [
"stylelint --fix"
]
}
}
4.3.2. 编辑器配置
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
}
}
4.4. 最佳实践建议
- 定期进行代码规范培训
- 建立规范文档中心
- 使用自动化工具保证规范执行
- 定期进行规范回顾和优化
- 新人入职时进行规范培训
5. 前端自动化测试
5.1. 自动化测试概述
5.1.1. 什么是自动化测试?
自动化测试是指通过编写测试脚本,自动验证应用程序功能是否符合预期的过程。它能够:
- 提高开发效率
- 保证代码质量
- 降低回归测试成本
- 提供代码文档作用
5.1.2. 为什么需要自动化测试?
- 提高效率:自动执行重复性测试任务
- 及早发现问题:在开发阶段就能发现潜在问题
- 回归测试保障:确保新代码不会破坏现有功能
- 重构信心:有测试保障,重构代码更有信心
5.2. 测试类型
5.2.1. 1. 单元测试 (Unit Testing)
- 测试最小可测试单元(通常是函数/方法)
- 特点:执行快、隔离性强
- 覆盖范围:70-80%
- 工具:Jest、Mocha、Jasmine
示例代码:
// 被测试的函数
function add(a, b) {
return a + b;
}
// Jest测试用例
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
5.2.2. 2. 集成测试 (Integration Testing)
- 测试多个模块间的交互
- 特点:覆盖组件间通信
- 覆盖范围:20-30%
- 工具:React Testing Library、Enzyme
示例代码:
// React组件测试
test('renders user profile correctly', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText(mockUser.name)).toBeInTheDocument();
expect(screen.getByText(mockUser.email)).toBeInTheDocument();
});
5.2.3. 3. E2E测试 (End-to-End Testing)
- 测试整个应用流程
- 特点:最接近用户操作
- 覆盖范围:10%左右
- 工具:Cypress、Selenium、Puppeteer
5.3. 常用测试工具
5.3.1. Jest
- Facebook开发的测试框架
- 特点:
- 零配置
- 快照测试
- 并行执行
- 覆盖率报告
5.3.2. Cypress
- 现代化E2E测试工具
- 特点:
- 实时重载
- 时间旅行
- 自动等待
- 调试友好
5.4. 测试最佳实践
5.4.1. 测试原则
- 测试行为而非实现
- 避免测试内部状态
- 使用真实场景数据
- 保持测试简单
5.4.2. 测试规范
describe('Component: UserProfile', () => {
// 每个测试前的准备工作
beforeEach(() => {
// 设置测试环境
});
// 具体测试用例
it('should display user name', () => {
// 测试代码
});
// 测试用例分组
describe('when user is logged in', () => {
it('should show logout button', () => {
// 测试代码
});
});
});
5.4.3. 代码覆盖率目标
- 语句覆盖率:80%以上
- 分支覆盖率:70%以上
- 函数覆盖率:90%以上
5.5. 实践建议
- 循序渐进
- 从简单的单元测试开始
- 逐步引入集成测试
- 最后添加E2E测试
- 持续维护
- 及时更新测试用例
- 定期检查测试覆盖率
- 重构测试代码
- 团队协作
- 建立测试规范
- 代码审查包含测试
- 分享测试经验
5.6. 常见问题解决
- 异步测试处理
test('async operation', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
- 模拟API请求
jest.mock('axios');
test('mocking API', () => {
axios.get.mockResolvedValue({ data: { id: 1 } });
// 测试代码
});
- 测试事件处理
test('button click', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick} />);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});
6. CI/CD
CI/CD 是一种通过自动化流程来频繁向客户交付应用的方法。
- CI(持续集成):开发人员频繁地将代码集成到主干分支,通过自动化构建和测试来验证代码。
- CD(持续交付/部署):将验证后的代码自动部署到不同环境。
6.1. 常见的 CI/CD 工具
6.1.1. CI 工具:
Jenkins
GitLab CI
GitHub Actions
Travis CI
Circle CI
TeamCicty
6.1.2. 部署工具
Docker
Kubernetes
PM2
Nginx
7. 前端监控
7.1. 什么是前端监控
前端监控是一种对前端应用进行全方位监控的技术手段,通过收集各类数据来分析和诊断系统的健康状况,从而提升用户体验和系统质量。
7.2. 监控的核心维度
7.2.1. 性能监控
- 页面加载性能 (Performance)
- First Paint (FP)
- First Contentful Paint (FCP)
- Largest Contentful Paint (LCP)
- First Input Delay (FID)
- Time to Interactive (TTI)
- Cumulative Layout Shift (CLS)
- 资源加载性能
- 资源加载时间
- 资源加载成功率
- CDN 性能
- API 性能
- 接口响应时间
- 接口成功率
- 接口错误率
7.2.2. 错误监控
- JS 运行时错误
- 语法错误
- 运行时异常
- Promise 异常
- 资源加载错误
- 图片加载失败
- JS/CSS 文件加载失败
- API 错误
- 网络请求错误
- 接口返回异常
7.2.3. 用户行为监控
- PV/UV 统计
- 用户点击行为
- 页面浏览深度
- 用户停留时间
- 用户路径分析
- 用户设备信息
7.2.4. 业务监控
- 业务转化率
- 功能使用率
- 用户操作步骤
- 关键业务指标
7.3. 监控实现方案
7.3.1. 性能监控实现
// Performance API 使用示例
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}`);
}
});
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
7.3.2. 错误监控实现
// 全局错误捕获
window.onerror = function(msg, url, line, col, error) {
console.log({
msg,
url,
line,
col,
error
});
return true;
};
// Promise 错误捕获
window.addEventListener('unhandledrejection', function(event) {
console.log('Promise Error:', event.reason);
});
7.3.3. 用户行为监控实现
// 页面 PV 统计
window.addEventListener('load', () => {
sendBeacon('/analytics', {
type: 'PV',
page: location.pathname
});
});
// 用户点击行为
document.addEventListener('click', (event) => {
sendBeacon('/analytics', {
type: 'click',
target: event.target.tagName,
position: {
x: event.clientX,
y: event.clientY
}
});
});
7.4. 数据处理流程
- 数据采集
- 埋点收集
- 自动化采集
- 性能 API 采集
- 数据上报
- 实时上报
- 批量上报
- 离线存储
- 数据存储
- 时序数据库
- 分布式存储
- 数据分片
- 数据分析
- 实时分析
- 离线分析
- 数据挖掘
- 告警处理
- 阈值告警
- 智能告警
- 告警分级
7.5. 监控平台架构
graph TD
A[数据采集层] --> B[数据传输层]
B --> C[数据处理层]
C --> D[数据存储层]
D --> E[数据分析层]
E --> F[可视化层]
E --> G[告警系统]
7.6. 最佳实践
- 监控接入
- 选择合适的监控 SDK
- 合理的采样率设置
- 监控代码位置
- 性能优化
- 控制上报数据大小
- 使用 requestIdleCallback
- 批量上报策略
- 告警配置
- 合理的告警阈值
- 告警级别划分
- 告警收敛策略
- 数据安全
- 数据脱敏
- 传输加密
- 访问权限控制
7.7. 常见监控平台
- 开源方案
- sentry
- zipkin
- prometheus
- 商业方案
- 阿里云 ARMS
- 腾讯云 RUM
8. 前端工程化最佳实践
8.1. 1. 项目脚手架
8.1.1. 基于 Create-React-App 的企业级配置
# 项目初始化
npx create-react-app my-app --template typescript
cd my-app
# 添加必要依赖
npm install @craco/craco -D
npm install antd @ant-design/icons
npm install axios
npm install classnames
npm install react-router-dom
8.1.2. 目录结构规范
src/
├── assets/ # 静态资源
├── components/ # 通用组件
├── constants/ # 常量定义
├── hooks/ # 自定义 Hooks
├── layouts/ # 布局组件
├── pages/ # 页面组件
├── services/ # API 服务
├── stores/ # 状态管理
├── styles/ # 样式文件
├── types/ # TS 类型定义
└── utils/ # 工具函数
8.2. 规范化
8.2.1. 2.1 ESLint 配置
// .eslintrc.js
module.exports = {
extends: [
'react-app',
'react-app/jest',
'plugin:@typescript-eslint/recommended',
'prettier'
],
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off'
}
}
8.2.2. Prettier 配置
// .prettierrc.js
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
endOfLine: 'auto'
}
8.2.3. Git Hooks
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
8.3. 3. 构建工具
8.3.1. Webpack 优化配置
// craco.config.js
const CracoLessPlugin = require('craco-less');
const SimpleProgressWebpackPlugin = require('simple-progress-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
webpack: {
configure: (webpackConfig) => {
// 生产环境才进行压缩
if (process.env.NODE_ENV === 'production') {
webpackConfig.devtool = false;
// 配置代码分割
webpackConfig.optimization = {
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 244000,
minChunks: 1,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
};
}
return webpackConfig;
},
plugins: [
new SimpleProgressWebpackPlugin(),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
}),
],
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
javascriptEnabled: true,
},
},
},
},
],
};
8.4. 自动化测试
8.4.1. Jest + React Testing Library
// src/components/Button/__tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../Button';
describe('Button Component', () => {
it('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
8.5. CI/CD
8.5.1. GitHub Actions 配置
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
8.6. 性能优化
8.6.1. React 性能优化实践
// 使用 React.memo 优化函数组件
const MemoizedComponent = React.memo(({ value }: Props) => {
return <div>{value}</div>
}, (prevProps, nextProps) => {
return prevProps.value === nextProps.value;
});
// 使用 useMemo 缓存计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 使用 useCallback 缓存回调函数
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
8.6.2. 图片懒加载
// src/components/LazyImage/index.tsx
import { useEffect, useRef, useState } from 'react';
interface Props {
src: string;
alt: string;
}
const LazyImage = ({ src, alt }: Props) => {
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{
threshold: 0.1
}
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
<img
ref={imgRef}
src={isInView ? src : ''}
alt={alt}
loading="lazy"
/>
);
};
export default LazyImage;
8.7. 研发效能
8.7.1. 组件文档工具 - Storybook
// src/components/Button/Button.stories.tsx
import { Story, Meta } from '@storybook/react';
import Button, { ButtonProps } from './Button';
export default {
title: 'Components/Button',
component: Button,
} as Meta;
const Template: Story<ButtonProps> = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
variant: 'primary',
children: 'Primary Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: 'secondary',
children: 'Secondary Button',
};
8.7.2. 统一的错误处理
// src/utils/request.ts
import axios from 'axios';
import { message } from 'antd';
const request = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
});
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
switch (error.response.status) {
case 401:
// 处理未授权
break;
case 403:
// 处理禁止访问
break;
case 404:
// 处理未找到
break;
case 500:
// 处理服务器错误
break;
default:
message.error('网络错误,请重试');
}
}
return Promise.reject(error);
}
);
export default request;
以上就是一些前端工程化的最佳实践案例。这些配置和实践能够帮助团队:
- 提高代码质量
- 规范开发流程
- 提升开发效率
- 保证产品性能
- 提高团队协作效率
选择和实施这些实践时要根据团队具体情况来调整,不必一次性全部采用,可以循序渐进地引入。