Appearance
现代前端工程化实践
随着前端项目复杂度的不断提升,工程化已成为现代前端开发的核心。本文将深入探讨前端工程化的各个方面,帮助你构建高效、可维护的前端项目。
1. 项目架构设计
目录结构规范
project/
├── src/
│ ├── components/ # 通用组件
│ │ ├── ui/ # 基础 UI 组件
│ │ └── business/ # 业务组件
│ ├── pages/ # 页面组件
│ ├── hooks/ # 自定义 Hooks
│ ├── utils/ # 工具函数
│ ├── services/ # API 服务
│ ├── stores/ # 状态管理
│ ├── types/ # TypeScript 类型定义
│ ├── assets/ # 静态资源
│ └── styles/ # 样式文件
├── public/ # 公共资源
├── tests/ # 测试文件
├── docs/ # 项目文档
├── scripts/ # 构建脚本
└── config/ # 配置文件
模块化设计原则
typescript
// 单一职责原则
export class UserService {
async getUser(id: string): Promise<User> {
return this.apiClient.get(`/users/${id}`)
}
async updateUser(id: string, data: Partial<User>): Promise<User> {
return this.apiClient.put(`/users/${id}`, data)
}
}
// 依赖注入
interface ApiClient {
get<T>(url: string): Promise<T>
post<T>(url: string, data: any): Promise<T>
put<T>(url: string, data: any): Promise<T>
delete<T>(url: string): Promise<T>
}
class HttpClient implements ApiClient {
constructor(private baseURL: string) {}
async get<T>(url: string): Promise<T> {
const response = await fetch(`${this.baseURL}${url}`)
return response.json()
}
// 其他方法实现...
}
2. 构建工具配置
Vite 配置优化
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
dts: true,
}),
Components({
resolvers: [ElementPlusResolver()],
dts: true,
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@types': resolve(__dirname, 'src/types'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
},
},
},
sourcemap: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})
Webpack 配置优化
javascript
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true,
},
},
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
}
3. 代码质量保障
ESLint 配置
javascript
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'prettier',
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'vue/multi-word-component-names': 'off',
'vue/component-definition-name-casing': ['error', 'PascalCase'],
},
}
Prettier 配置
json
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
Husky + lint-staged
json
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"stylelint --fix",
"prettier --write"
]
}
}
4. 测试策略
单元测试
typescript
// utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatDate, formatCurrency } from './format'
describe('format utils', () => {
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2024-01-15')
expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15')
})
it('should handle invalid date', () => {
expect(formatDate(null)).toBe('')
})
})
describe('formatCurrency', () => {
it('should format currency with default locale', () => {
expect(formatCurrency(1234.56)).toBe('¥1,234.56')
})
it('should format currency with custom locale', () => {
expect(formatCurrency(1234.56, 'en-US')).toBe('$1,234.56')
})
})
})
组件测试
typescript
// components/UserCard.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import UserCard from './UserCard.vue'
describe('UserCard', () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg'
}
it('should render user information correctly', () => {
const wrapper = mount(UserCard, {
props: { user: mockUser }
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
expect(wrapper.find('img').attributes('src')).toBe(mockUser.avatar)
})
it('should emit edit event when edit button is clicked', async () => {
const wrapper = mount(UserCard, {
props: { user: mockUser }
})
await wrapper.find('[data-testid="edit-button"]').trigger('click')
expect(wrapper.emitted('edit')).toBeTruthy()
expect(wrapper.emitted('edit')?.[0]).toEqual([mockUser])
})
})
E2E 测试
typescript
// e2e/user-management.spec.ts
import { test, expect } from '@playwright/test'
test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/users')
})
test('should display user list', async ({ page }) => {
await expect(page.locator('[data-testid="user-list"]')).toBeVisible()
await expect(page.locator('.user-card')).toHaveCount(10)
})
test('should create new user', async ({ page }) => {
await page.click('[data-testid="add-user-button"]')
await page.fill('[data-testid="name-input"]', 'New User')
await page.fill('[data-testid="email-input"]', 'newuser@example.com')
await page.click('[data-testid="save-button"]')
await expect(page.locator('text=User created successfully')).toBeVisible()
})
})
5. 性能监控
构建分析
javascript
// scripts/analyze.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const webpack = require('webpack')
const config = require('../webpack.config.js')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
openAnalyzer: true,
})
)
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
console.error('Build failed')
return
}
console.log('Build completed successfully')
})
运行时性能监控
typescript
// utils/performance.ts
class PerformanceMonitor {
private static instance: PerformanceMonitor
static getInstance(): PerformanceMonitor {
if (!this.instance) {
this.instance = new PerformanceMonitor()
}
return this.instance
}
measurePageLoad(): void {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
const metrics = {
FCP: this.getFCP(),
LCP: this.getLCP(),
FID: this.getFID(),
CLS: this.getCLS(),
TTFB: navigation.responseStart - navigation.requestStart,
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.navigationStart,
loadComplete: navigation.loadEventEnd - navigation.navigationStart,
}
this.sendMetrics(metrics)
})
}
private getFCP(): number {
const entries = performance.getEntriesByName('first-contentful-paint')
return entries.length > 0 ? entries[0].startTime : 0
}
private getLCP(): Promise<number> {
return new Promise(resolve => {
new PerformanceObserver(list => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
resolve(lastEntry.startTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
})
}
private sendMetrics(metrics: Record<string, number>): void {
// 发送到监控服务
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics),
})
}
}
// 使用
PerformanceMonitor.getInstance().measurePageLoad()
6. CI/CD 流程
GitHub Actions 配置
yaml
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Deploy to production
run: |
# 部署脚本
echo "Deploying to production..."
Docker 配置
dockerfile
# Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
7. 最佳实践总结
开发规范
- 代码规范:统一的代码风格和命名规范
- 组件设计:单一职责、可复用、可测试
- 状态管理:合理的状态划分和数据流
- 性能优化:懒加载、代码分割、缓存策略
工程化工具链
- 构建工具:Vite/Webpack + 插件生态
- 代码质量:ESLint + Prettier + TypeScript
- 测试框架:Vitest + Testing Library + Playwright
- CI/CD:GitHub Actions + Docker + 自动化部署
监控与维护
- 性能监控:Core Web Vitals + 自定义指标
- 错误监控:Sentry + 日志收集
- 依赖管理:定期更新 + 安全扫描
- 文档维护:API 文档 + 组件文档
总结
现代前端工程化是一个系统性工程,需要从项目架构、构建工具、代码质量、测试策略、性能监控、CI/CD 等多个维度进行考虑。通过建立完善的工程化体系,我们可以:
- 提高开发效率和代码质量
- 降低项目维护成本
- 确保产品的稳定性和性能
- 支持团队协作和知识传承
工程化不是一蹴而就的,需要根据项目实际情况逐步完善和优化。