前端工程化实践

前端工程化

前端工程化是将软件工程的方法应用于前端开发,通过规范化、自动化、模块化等手段,提高开发效率、代码质量和可维护性。随着前端项目的规模和复杂度不断增加,工程化已经成为前端团队的必修课。在这篇文章中,我将分享前端工程化的核心实践,帮助你构建现代化的前端开发工作流。

项目结构规范

良好的项目结构是工程化的基础,它决定了代码的组织方式和可维护性。

推荐的目录结构

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流程,最终形成完整的工程化体系。

记住,工程化的目的是提高效率和质量,而不是增加负担。选择适合团队规模和项目特点的方案,保持简单实用,才是最佳实践。