前端工程化实践
前端工程化是将软件工程的方法应用于前端开发,通过规范化、自动化、模块化等手段,提高开发效率、代码质量和可维护性。随着前端项目的规模和复杂度不断增加,工程化已经成为前端团队的必修课。在这篇文章中,我将分享前端工程化的核心实践,帮助你构建现代化的前端开发工作流。
项目结构规范
良好的项目结构是工程化的基础,它决定了代码的组织方式和可维护性。
推荐的目录结构
project/
├── public/ # 静态资源
│ ├── index.html
│ └── favicon.ico
├── src/
│ ├── assets/ # 需要编译的资源
│ │ ├── images/
│ │ └── styles/
│ ├── components/ # 通用组件
│ │ ├── common/
│ │ └── business/
│ ├── hooks/ # 自定义Hooks
│ ├── services/ # API服务
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ └── App.tsx
├── tests/ # 测试文件
├── docs/ # 文档
├── scripts/ # 构建脚本
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── tsconfig.json # TypeScript配置
├── package.json
└── README.md
命名规范
- 文件名:小写字母,多个单词用连字符连接(user-profile.tsx)
- 组件名:大驼峰命名(UserProfile)
- 变量/函数:小驼峰命名(getUserInfo)
- 常量:全大写下划线分隔(MAX_RETRY_COUNT)
- CSS类名:BEM或CSS Modules
代码质量保障
ESLint配置
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier' // 放在最后,覆盖冲突规则
],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
};
Prettier配置
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true
}
Git Hooks
// package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,md}": [
"prettier --write"
]
}
}
// .husky/pre-commit
npm run lint-staged
模块化设计
组件设计原则
- 单一职责:每个组件只做一件事
- 可复用:通用组件不包含业务逻辑
- 可测试:组件易于单元测试
- Props清晰:使用TypeScript定义Props类型
// 好的组件设计
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export const Button: React.FC = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <Spinner /> : children}
</button>
);
};
API层封装
// services/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: process.env.API_URL,
timeout: 10000
});
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// 跳转登录
}
return Promise.reject(error);
}
);
export default api;
// services/user.ts
import api from './api';
export const userService = {
getUsers: () => api.get('/users'),
getUser: (id: string) => api.get(`/users/${id}`),
createUser: (data: UserData) => api.post('/users', data),
updateUser: (id: string, data: Partial) => api.put(`/users/${id}`, data),
deleteUser: (id: string) => api.delete(`/users/${id}`)
};
自动化测试
单元测试
// utils/format.test.ts
import { formatCurrency, formatDate } from './format';
describe('formatCurrency', () => {
it('should format number as currency', () => {
expect(formatCurrency(1234.56)).toBe('¥1,234.56');
});
it('should handle zero', () => {
expect(formatCurrency(0)).toBe('¥0.00');
});
});
// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('should render correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should call onClick when clicked', () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(onClick).toHaveBeenCalled();
});
});
E2E测试
// e2e/login.spec.ts (Playwright)
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
CI/CD流程
GitHub Actions配置
# .github/workflows/ci.yml
name: CI/CD
name>
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
CI/CD流程
GitHub Actions配置
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run build
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- run: aws s3 sync ./dist s3://my-bucket --delete
文档管理
代码注释规范
/**
* 格式化金额
* @param amount - 金额数值
* @param currency - 货币符号,默认为人民币
* @returns 格式化后的金额字符串
* @example
* formatCurrency(1234.56) // '¥1,234.56'
* formatCurrency(1234.56, '$') // '$1,234.56'
*/
export function formatCurrency(amount: number, currency = '¥'): string {
return currency + amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
README规范
# 项目名称
简短的项目描述
## 快速开始
### 安装
\`\`\`bash
npm install
\`\`\`
### 开发
\`\`\`bash
npm run dev
\`\`\`
### 构建
\`\`\`bash
npm run build
\`\`\`
## 项目结构
说明目录结构...
## 技术栈
- React 18
- TypeScript
- Vite
## 贡献指南
如何贡献代码...
## License
MIT
总结
前端工程化不是一蹴而就的,而是需要在实践中不断优化迭代。从项目结构开始,逐步建立代码规范、自动化流程、测试体系、CI/CD流程,最终形成完整的工程化体系。
记住,工程化的目的是提高效率和质量,而不是增加负担。选择适合团队规模和项目特点的方案,保持简单实用,才是最佳实践。