前端工程化实践
前端工程化是现代 Web 开发的重要组成部分,本教程将带您了解前端工程化的核心概念和最佳实践。
什么是前端工程化?
前端工程化是指将软件工程的方法和实践应用到前端开发中,通过工具、流程和规范来提高开发效率、代码质量和项目可维护性。
核心目标
- 🚀 提高开发效率 - 自动化重复性工作
- 🔧 保证代码质量 - 统一代码规范和最佳实践
- 📦 优化构建产物 - 减少包体积,提升性能
- 🔄 简化部署流程 - 自动化部署和发布
项目初始化
使用脚手架工具
bash
# Vue 项目
npm create vue@latest my-project
cd my-project
npm install
# React 项目
npx create-react-app my-app --template typescript
cd my-app
# Vite 通用项目
npm create vite@latest my-project
cd my-project
npm install
项目结构规划
my-project/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 项目资源文件
│ ├── components/ # 公共组件
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── api/ # API 接口
│ ├── types/ # TypeScript 类型定义
│ └── styles/ # 样式文件
├── tests/ # 测试文件
├── docs/ # 项目文档
├── .github/ # GitHub 配置
└── config/ # 配置文件
代码规范
ESLint 配置
安装 ESLint:
bash
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
配置 .eslintrc.js
:
javascript
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2022: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'@vue/typescript/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
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',
'prefer-const': 'error',
'no-var': 'error'
}
}
Prettier 配置
安装 Prettier:
bash
npm install -D prettier eslint-config-prettier eslint-plugin-prettier
配置 .prettierrc
:
json
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "avoid"
}
Husky + lint-staged
安装 Husky:
bash
npm install -D husky lint-staged
npx husky install
配置 package.json
:
json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"prettier --write"
]
}
}
添加 pre-commit 钩子:
bash
npx husky add .husky/pre-commit "npx lint-staged"
构建优化
Vite 配置优化
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils')
}
},
// 构建优化
build: {
// 分包策略
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils-vendor': ['lodash-es', 'dayjs']
}
}
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// 开发服务器
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
Webpack 优化(如果使用)
javascript
// webpack.config.js
const path = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
// 入口优化
entry: {
app: './src/main.js',
vendor: ['vue', 'vue-router', 'vuex']
},
// 输出优化
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
},
// 优化配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
// 插件
plugins: [
// 分析包大小
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean)
}
样式工程化
CSS 预处理器
安装 Sass:
bash
npm install -D sass
样式架构:
scss
// styles/index.scss
@import './variables';
@import './mixins';
@import './base';
@import './components';
@import './utilities';
// styles/_variables.scss
:root {
// 颜色系统
--color-primary: #409eff;
--color-success: #67c23a;
--color-warning: #e6a23c;
--color-danger: #f56c6c;
// 间距系统
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
// 字体系统
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size-md: 16px;
--font-size-lg: 18px;
--font-size-xl: 20px;
}
// styles/_mixins.scss
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin responsive($breakpoint) {
@if $breakpoint == mobile {
@media (max-width: 767px) { @content; }
}
@if $breakpoint == tablet {
@media (min-width: 768px) and (max-width: 1023px) { @content; }
}
@if $breakpoint == desktop {
@media (min-width: 1024px) { @content; }
}
}
PostCSS 配置
javascript
// postcss.config.js
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
}
}
状态管理
Pinia 最佳实践
typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, LoginCredentials } from '@/types/user'
import { userApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
// State
const currentUser = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
// Getters
const isLoggedIn = computed(() => !!currentUser.value)
const userRole = computed(() => currentUser.value?.role || 'guest')
// Actions
const login = async (credentials: LoginCredentials) => {
loading.value = true
error.value = null
try {
const response = await userApi.login(credentials)
currentUser.value = response.data
// 存储到 localStorage
localStorage.setItem('user', JSON.stringify(response.data))
localStorage.setItem('token', response.token)
return response
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
throw err
} finally {
loading.value = false
}
}
const logout = () => {
currentUser.value = null
localStorage.removeItem('user')
localStorage.removeItem('token')
}
const initializeAuth = () => {
const storedUser = localStorage.getItem('user')
const storedToken = localStorage.getItem('token')
if (storedUser && storedToken) {
currentUser.value = JSON.parse(storedUser)
}
}
return {
// State
currentUser,
loading,
error,
// Getters
isLoggedIn,
userRole,
// Actions
login,
logout,
initializeAuth
}
})
API 管理
统一的 API 客户端
typescript
// api/client.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
class ApiClient {
private instance: AxiosInstance
constructor(baseURL: string) {
this.instance = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
this.setupInterceptors()
}
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
this.instance.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 处理未授权
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.instance.put(url, data, config)
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.delete(url, config)
}
}
export const apiClient = new ApiClient(import.meta.env.VITE_API_BASE_URL)
测试策略
单元测试
安装测试工具:
bash
npm install -D vitest @vue/test-utils jsdom
配置 vitest.config.ts
:
typescript
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.ts']
}
})
测试示例:
typescript
// tests/components/Button.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'
describe('Button', () => {
it('renders properly', () => {
const wrapper = mount(Button, {
props: { type: 'primary' },
slots: { default: 'Click me' }
})
expect(wrapper.text()).toContain('Click me')
expect(wrapper.classes()).toContain('btn-primary')
})
it('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
})
E2E 测试
安装 Playwright:
bash
npm install -D @playwright/test
E2E 测试示例:
typescript
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test'
test('user can login', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="email"]', 'user@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="login-button"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('[data-testid="user-name"]')).toBeVisible()
})
CI/CD 流程
GitHub Actions 配置
yaml
# File: .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 tests
run: npm run test:unit
- name: Run E2E tests
run: npm run test:e2e
- name: Build project
run: npm run build
- name: Upload coverage
uses: codecov/codecov-action@v3
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install and build
run: |
npm ci
npm run build
- name: Deploy to production
run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
性能监控
性能指标收集
typescript
// utils/performance.ts
export class PerformanceMonitor {
static measurePageLoad() {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
const metrics = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
request: navigation.responseStart - navigation.requestStart,
response: navigation.responseEnd - navigation.responseStart,
dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,
load: navigation.loadEventEnd - navigation.loadEventStart,
total: navigation.loadEventEnd - navigation.navigationStart
}
console.log('Performance Metrics:', metrics)
// 发送到监控服务
this.sendMetrics(metrics)
})
}
static measureLCP() {
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime)
this.sendMetric('lcp', lastEntry.startTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
}
static measureFID() {
new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime)
this.sendMetric('fid', entry.processingStart - entry.startTime)
})
}).observe({ entryTypes: ['first-input'] })
}
private static sendMetrics(metrics: Record<string, number>) {
// 发送到监控服务
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics)
})
}
private static sendMetric(name: string, value: number) {
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [name]: value })
})
}
}
// 在应用启动时初始化
PerformanceMonitor.measurePageLoad()
PerformanceMonitor.measureLCP()
PerformanceMonitor.measureFID()
最佳实践总结
项目结构
- 清晰的目录结构
- 合理的文件命名
- 模块化的代码组织
代码质量
- 统一的代码规范
- 完善的类型定义
- 充分的测试覆盖
构建优化
- 合理的分包策略
- 资源压缩和优化
- 缓存策略配置
开发体验
- 热更新和快速构建
- 完善的错误提示
- 便捷的调试工具
部署流程
- 自动化的 CI/CD
- 环境配置管理
- 监控和日志收集
工具推荐
开发工具
- VS Code - 编辑器
- Vue DevTools - Vue 调试工具
- Chrome DevTools - 浏览器调试
构建工具
- Vite - 现代构建工具
- Webpack - 传统构建工具
- Rollup - 库打包工具
质量保证
- ESLint - 代码检查
- Prettier - 代码格式化
- Husky - Git 钩子
测试工具
- Vitest - 单元测试
- Playwright - E2E 测试
- Storybook - 组件测试
前端工程化是一个持续演进的过程,需要根据项目需求和团队情况不断优化和调整。掌握这些核心概念和工具,将大大提升您的开发效率和项目质量!🚀