前端工程化详细落地实践方案
目录
- 1. 前言
- 2. 工程化概述
- 3. 项目初始化与脚手架
- 4. 代码规范与质量控制
- 5. 构建与打包优化
- 6. 自动化测试
- 7. CI/CD 持续集成与部署
- 8. 监控与性能优化
- 9. 微前端架构
- 10. 工程化最佳实践
- 11. 国际化与本地化
- 12. 无障碍设计与实现
- 13. 实际案例分析
- 14. 参考资源
1. 前言
随着前端技术的快速发展,项目规模和复杂度不断增加,前端工程化已成为现代前端开发的必然选择。本文档旨在提供一套完整的前端工程化落地实践方案,帮助团队提高开发效率、保证代码质量、优化用户体验。
2. 工程化概述
2.1 什么是前端工程化
前端工程化是指将前端开发流程规范化、标准化,利用各种工具和技术手段提高开发效率和代码质量的一系列方法和实践。
2.2 工程化的核心目标
- 提高开发效率:通过自动化工具减少重复工作
- 保证代码质量:通过规范和工具确保代码一致性和可维护性
- 优化用户体验:通过构建优化提升应用性能
- 降低维护成本:通过模块化和组件化提高代码复用性
2.3 工程化的主要内容
- 项目脚手架
- 代码规范
- 构建工具
- 自动化测试
- 持续集成/持续部署
- 性能监控与优化
3. 项目初始化与脚手架
3.1 技术栈选型
3.1.1 主流前端框架
React:适合大型应用,生态丰富,组件化优秀
- 优势:虚拟DOM高效渲染、单向数据流、组件复用性强、大型社区支持
- 适用场景:大型SPA应用、需要高度定制UI的项目、对性能要求高的应用
- 生态系统:Redux/MobX(状态管理)、React Router(路由)、Next.js(SSR框架)、React Query(数据获取)
- 学习曲线:中等,函数式编程思想需要适应
Vue:易上手,中小型项目首选,文档完善
- 优势:模板语法直观、响应式系统、双向绑定、渐进式框架设计
- 适用场景:中小型应用、快速原型开发、需要平缓学习曲线的团队
- 生态系统:Vuex/Pinia(状态管理)、Vue Router(路由)、Nuxt.js(SSR框架)、Vueuse(工具集)
- 学习曲线:低,HTML增强型模板更易理解
Angular:完整框架,适合企业级应用
- 优势:完整解决方案、TypeScript原生支持、依赖注入、RxJS集成
- 适用场景:企业级应用、大型团队协作、需要严格架构的项目
- 生态系统:NgRx(状态管理)、Angular Material(UI库)、Angular Universal(SSR)
- 学习曲线:高,概念较多,需要理解依赖注入等模式
Svelte:编译时框架,运行时零依赖
- 优势:编译时优化、无虚拟DOM、体积小、性能高
- 适用场景:性能敏感应用、嵌入式组件、小型应用
- 生态系统:Svelte Kit(应用框架)、Svelte Store(状态管理)
- 学习曲线:低,接近原生JavaScript
3.1.2 构建工具
Webpack:功能全面,生态丰富,适合复杂项目
- 优势:高度可配置、插件丰富、处理各种资源类型、代码分割
- 劣势:配置复杂、大型项目构建速度慢
- 配置示例:
javascript// webpack.config.js 基础配置示例 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', clean: true, }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } } }, { 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', }), ], optimization: { moduleIds: 'deterministic', runtimeChunk: 'single', splitChunks: { cacheGroups: { vendor: { test: /[\\]node_modules[\\]/, name: 'vendors', chunks: 'all', }, }, }, }, };
Vite:基于 ESM,开发环境启动快,热更新迅速
- 优势:开发服务器启动即时、按需编译、原生ESM支持、热更新极快
- 劣势:生产构建依赖Rollup、兼容性需要额外配置
- 配置示例:
javascript// vite.config.js 基础配置示例 import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import legacy from '@vitejs/plugin-legacy'; import { visualizer } from 'rollup-plugin-visualizer'; export default defineConfig(({ mode }) => { const isProd = mode === 'production'; return { plugins: [ react(), isProd && legacy({ targets: ['defaults', 'not IE 11'], }), isProd && visualizer({ open: true, gzipSize: true, }), ], resolve: { alias: { '@': '/src', }, }, css: { modules: { localsConvention: 'camelCaseOnly', }, preprocessorOptions: { scss: { additionalData: `@import "@/styles/variables.scss";`, }, }, }, build: { target: 'es2015', cssCodeSplit: true, sourcemap: !isProd, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], utils: ['lodash-es', 'axios'], }, }, }, }, server: { port: 3000, open: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, }; });
Rollup:适合库开发,打包结果清晰简洁
- 优势:Tree-shaking优秀、输出格式多样(ESM/CJS/UMD)、代码简洁
- 劣势:插件相对较少、不适合复杂应用构建
- 配置示例:
javascript// rollup.config.js 基础配置示例 import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import { terser } from 'rollup-plugin-terser'; import dts from 'rollup-plugin-dts'; import pkg from './package.json'; export default [ { input: 'src/index.ts', output: [ { file: pkg.main, format: 'cjs', sourcemap: true, }, { file: pkg.module, format: 'esm', sourcemap: true, }, { file: pkg.unpkg, format: 'umd', name: 'MyLibrary', plugins: [terser()], globals: { react: 'React', }, }, ], external: ['react', 'react-dom'], plugins: [ resolve(), commonjs(), typescript({ tsconfig: './tsconfig.json' }), ], }, { input: 'dist/types/index.d.ts', output: [{ file: 'dist/index.d.ts', format: 'esm' }], plugins: [dts()], }, ];
Turbopack:Next.js 团队开发的新一代构建工具,性能优异
- 优势:Rust编写、增量计算、极速构建、内存高效
- 劣势:仍在开发中、生态不完善、文档有限
- 使用示例:
javascript// next.config.js 中启用 Turbopack module.exports = { experimental: { turbo: true, }, };
3.1.3 CSS 解决方案
CSS Modules:局部作用域,避免样式冲突
- 优势:自动作用域隔离、类名哈希化、静态分析
- 使用示例:
jsx// Button.module.css .button { padding: 8px 16px; border-radius: 4px; font-weight: 500; } .primary { background-color: #1890ff; color: white; } // Button.jsx import styles from './Button.module.css'; function Button({ children, type = 'primary' }) { return ( <button className={`${styles.button} ${styles[type]}`}> {children} </button> ); }
Sass/Less:CSS 预处理器,提供变量、嵌套等特性
- 优势:变量支持、嵌套规则、混合(mixins)、函数、条件语句
- 使用示例:
scss// variables.scss $primary-color: #1890ff; $border-radius: 4px; $font-family: 'Roboto', sans-serif; // mixins.scss @mixin flex-center { display: flex; justify-content: center; align-items: center; } @mixin responsive($breakpoint) { @if $breakpoint == 'mobile' { @media (max-width: 767px) { @content; } } @else if $breakpoint == 'tablet' { @media (min-width: 768px) and (max-width: 1023px) { @content; } } @else if $breakpoint == 'desktop' { @media (min-width: 1024px) { @content; } } } // button.scss @import 'variables'; @import 'mixins'; .button { padding: 8px 16px; border-radius: $border-radius; font-family: $font-family; font-weight: 500; cursor: pointer; transition: all 0.3s ease; &.primary { background-color: $primary-color; color: white; &:hover { background-color: darken($primary-color, 10%); } } @include responsive('mobile') { padding: 6px 12px; font-size: 14px; } }
Tailwind CSS:原子化 CSS,开发效率高
- 优势:无需编写CSS、快速原型设计、响应式设计简单、主题定制
- 使用示例:
jsxfunction Button({ children, variant = 'primary' }) { const baseClasses = 'px-4 py-2 rounded font-medium transition-colors'; const variantClasses = { primary: 'bg-blue-500 text-white hover:bg-blue-600', secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300', danger: 'bg-red-500 text-white hover:bg-red-600', }; return ( <button className={`${baseClasses} ${variantClasses[variant]}`}> {children} </button> ); }
tailwind.config.js 配置示例:
javascriptmodule.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: { colors: { primary: { 50: '#e6f7ff', 100: '#bae7ff', 500: '#1890ff', 600: '#096dd9', }, brand: '#f5222d', }, spacing: { '4.5': '1.125rem', }, fontFamily: { sans: ['Inter var', 'sans-serif'], }, }, }, plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/aspect-ratio'), ], };
Styled-components:CSS in JS 方案,组件化样式
- 优势:动态样式生成、主题支持、自动前缀、无类名冲突
- 使用示例:
jsximport styled, { ThemeProvider } from 'styled-components'; // 创建主题 const theme = { colors: { primary: '#1890ff', secondary: '#f5f5f5', danger: '#ff4d4f', }, fontSizes: { small: '0.875rem', medium: '1rem', large: '1.25rem', }, breakpoints: { mobile: '576px', tablet: '768px', desktop: '1024px', }, }; // 创建样式组件 const Button = styled.button` padding: 8px 16px; border-radius: 4px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; /* 基于props的动态样式 */ background-color: ${props => props.theme.colors[props.variant || 'primary']}; color: ${props => props.variant === 'secondary' ? '#333' : 'white'}; &:hover { opacity: 0.9; } /* 响应式样式 */ @media (max-width: ${props => props.theme.breakpoints.mobile}) { padding: 6px 12px; font-size: ${props => props.theme.fontSizes.small}; } `; // 使用样式组件 function App() { return ( <ThemeProvider theme={theme}> <div> <Button>默认按钮</Button> <Button variant="secondary">次要按钮</Button> <Button variant="danger">危险按钮</Button> </div> </ThemeProvider> ); }
3.2 脚手架工具
3.2.1 官方脚手架
- Create React App:React 官方脚手架
- Vue CLI / Create Vue:Vue 官方脚手架
- Angular CLI:Angular 官方脚手架
3.2.2 高级脚手架
- Next.js:React 服务端渲染框架
- Nuxt.js:Vue 服务端渲染框架
- Umi:阿里巴巴企业级 React 应用框架
3.2.3 自定义脚手架
对于大型团队,可以基于现有脚手架进行二次开发,定制符合团队需求的脚手架:
# 示例:基于 CRA 模板创建自定义脚手架
npx create-react-app my-app --template company-template
3.3 项目目录结构规范
├── public/ # 静态资源
├── src/ # 源代码
│ ├── api/ # API 请求
│ ├── assets/ # 项目资源文件
│ ├── components/ # 公共组件
│ │ ├── common/ # 通用组件
│ │ └── business/ # 业务组件
│ ├── hooks/ # 自定义 Hooks
│ ├── layouts/ # 布局组件
│ ├── pages/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── styles/ # 全局样式
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ ├── App.tsx # 应用入口组件
│ └── main.tsx # 应用入口文件
├── tests/ # 测试文件
├── .editorconfig # 编辑器配置
├── .eslintrc.js # ESLint 配置
├── .gitignore # Git 忽略文件
├── .prettierrc # Prettier 配置
├── jest.config.js # Jest 配置
├── package.json # 项目依赖
├── README.md # 项目说明
├── tsconfig.json # TypeScript 配置
└── vite.config.ts # Vite 配置
4. 代码规范与质量控制
4.1 代码规范工具
4.1.1 ESLint 配置
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// 自定义规则
},
settings: {
react: {
version: 'detect',
},
},
};
4.1.2 Prettier 配置
// .prettierrc
{
"semi": true,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
}
4.1.3 StyleLint 配置
// .stylelintrc.js
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
plugins: ['stylelint-order'],
rules: {
'order/properties-alphabetical-order': true,
// 自定义规则
},
};
4.2 Git 规范
4.2.1 Git Flow 工作流
- master:主分支,存放稳定版本
- develop:开发分支,最新开发版本
- feature/xxx:功能分支,开发新功能
- hotfix/xxx:修复分支,修复线上 bug
- release/xxx:发布分支,准备发布版本
4.2.2 Commit 规范
使用 Commitizen 和 Commitlint 规范提交信息:
npm install -D commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional
配置 package.json:
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
创建 commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 bug
'docs', // 文档
'style', // 代码格式
'refactor', // 重构
'perf', // 性能优化
'test', // 测试
'chore', // 构建过程或辅助工具变动
'revert', // 回退
'build' // 打包
]
],
'subject-case': [0] // 标题大小写不做校验
}
};
4.2.3 Husky 和 lint-staged
配置 Git 钩子,在提交前自动执行代码检查:
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
配置 package.json:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,less,scss}": [
"stylelint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
4.3 代码评审流程
- 提交 PR/MR:开发者完成功能后提交合并请求
- 自动化检查:触发 CI 流程,运行测试和代码检查
- 人工评审:至少一名团队成员进行代码评审
- 修改完善:根据评审意见修改代码
- 合并代码:评审通过后合并到目标分支
5. 构建与打包优化
5.1 Webpack 优化配置
5.1.1 基础优化
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
// ...
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 生产环境下移除 console
},
},
extractComments: false, // 不提取注释
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\\\]node_modules[\\\\]/,
name: 'vendors',
priority: -10,
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
5.1.2 缓存优化
module.exports = {
// ...
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
},
cache: {
type: 'filesystem', // 使用文件系统缓存
},
};
5.1.3 体积优化
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
// ...
plugins: [
// 分析打包体积
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE ? 'server' : 'disabled',
}),
// Gzip 压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 只有大小大于 10kb 的资源会被处理
minRatio: 0.8, // 只有压缩率小于 0.8 的资源才会被处理
}),
],
};
5.2 Vite 优化配置
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
import { visualizer } from 'rollup-plugin-visualizer';
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
react(),
// 兼容旧浏览器
legacy({
targets: ['defaults', 'not IE 11'],
}),
// 打包分析
visualizer({
open: process.env.ANALYZE === 'true',
}),
// Gzip 压缩
viteCompression({
threshold: 10240, // 只有大小大于 10kb 的资源会被处理
}),
],
build: {
target: 'es2015',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
},
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
// 其他依赖包
},
},
},
},
});
5.3 资源优化策略
5.3.1 图片优化
- 使用 WebP 格式
- 使用响应式图片
- 图片懒加载
- 使用 CDN 加速
// 图片懒加载示例 (React)
import { useEffect, useRef, useState } from 'react';
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
imgRef.current.src = src;
setIsLoaded(true);
observer.disconnect();
}
});
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
observer.disconnect();
};
}, [src]);
return (
<img
ref={imgRef}
alt={alt}
className={`lazy-image ${isLoaded ? 'loaded' : ''}`}
data-src={src}
/>
);
}
5.3.2 字体优化
- 使用
font-display: swap
- 预加载关键字体
- 使用系统字体降级
/* 字体优化示例 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap; /* 文本先以后备字体显示,字体加载完成后再替换 */
font-weight: normal;
font-style: normal;
}
/* 系统字体降级 */
body {
font-family: 'CustomFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
6. 自动化测试
6.1 测试策略与测试金字塔
前端测试应遵循测试金字塔原则,从底到顶依次为:
单元测试:测试最小可测试单元,如函数、组件
- 数量最多,执行最快
- 覆盖率要求高(通常 70-80%)
- 主要工具:Jest、Vitest、Mocha
集成测试:测试多个单元协同工作
- 数量适中
- 覆盖关键业务流程
- 主要工具:React Testing Library、Vue Testing Library、Cypress Component Testing
端到端测试:模拟真实用户行为的测试
- 数量最少,执行最慢
- 覆盖核心用户旅程
- 主要工具:Cypress、Playwright、Selenium
测试策略制定要点
- 测试范围:确定哪些代码需要测试,优先级如何
- 测试类型:单元测试、集成测试、E2E测试的比例
- 测试工具:选择适合项目的测试框架和工具
- 测试流程:测试编写、执行、维护的流程
- 测试指标:覆盖率目标、通过率要求
6.2 单元测试
6.2.1 Jest 配置
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/reportWebVitals.ts',
'!src/setupTests.ts',
'!**/node_modules/**',
'!**/vendor/**',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
// 测试匹配模式
testMatch: ['**/__tests__/**/*.test.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
// 测试超时时间
testTimeout: 10000,
// 并行执行测试
maxWorkers: '50%',
// 测试覆盖率报告格式
coverageReporters: ['text', 'lcov', 'clover', 'html'],
};
setupTests.ts 配置示例:
// src/setupTests.ts
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';
import { server } from './mocks/server';
// 配置 Testing Library
configure({
testIdAttribute: 'data-testid',
});
// 设置 MSW 服务器
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// 模拟 window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// 模拟 IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor(callback) {
this.callback = callback;
}
observe() { return null; }
unobserve() { return null; }
disconnect() { return null; }
};
#### 6.1.2 React 组件测试示例
```typescript
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component', () => {
test('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('applies custom className', () => {
render(<Button className="custom-class">Click me</Button>);
expect(screen.getByText('Click me')).toHaveClass('custom-class');
});
});
6.3 集成测试
使用 Cypress 进行集成测试:
// cypress/integration/login.spec.js
describe('Login Page', () => {
beforeEach(() => {
cy.visit('/login');
});
it('should display login form', () => {
cy.get('form').should('be.visible');
cy.get('input[name="username"]').should('be.visible');
cy.get('input[name="password"]').should('be.visible');
cy.get('button[type="submit"]').should('be.visible');
});
it('should show error for invalid credentials', () => {
cy.get('input[name="username"]').type('invalid_user');
cy.get('input[name="password"]').type('invalid_password');
cy.get('button[type="submit"]').click();
cy.get('.error-message').should('be.visible');
});
it('should redirect to dashboard after successful login', () => {
cy.get('input[name="username"]').type('valid_user');
cy.get('input[name="password"]').type('valid_password');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
});
6.4 E2E 测试
使用 Playwright 进行 E2E 测试:
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/login');
await page.fill('input[name="username"]', 'test_user');
await page.fill('input[name="password"]', 'test_password');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
});
test('complete checkout process', async ({ page }) => {
// 添加商品到购物车
await page.goto('/products');
await page.click('.product-card:first-child .add-to-cart');
await page.click('.cart-icon');
// 检查购物车
await expect(page.locator('.cart-item')).toHaveCount(1);
await page.click('.checkout-button');
// 填写结账信息
await page.fill('input[name="address"]', '123 Test St');
await page.fill('input[name="city"]', 'Test City');
await page.fill('input[name="zip"]', '12345');
await page.click('.payment-method:first-child');
await page.click('.submit-order');
// 验证订单确认
await page.waitForURL('**/order-confirmation');
await expect(page.locator('.order-number')).toBeVisible();
await expect(page.locator('.success-message')).toBeVisible();
});
});
7. CI/CD 持续集成与部署
7.1 CI/CD 策略设计
7.1.1 CI/CD 流程设计
一个完整的前端 CI/CD 流程通常包含以下阶段:
- 代码提交:开发者提交代码到版本控制系统
- 静态检查:运行 ESLint、StyleLint、TypeScript 类型检查等
- 单元测试:运行单元测试并生成覆盖率报告
- 构建:构建生产环境代码
- 集成测试:运行集成测试
- 部署预览:部署到预览环境(如 Vercel Preview)
- E2E 测试:在预览环境运行端到端测试
- 部署生产:部署到生产环境
- 监控:部署后监控应用性能和错误
7.1.2 环境策略
典型的多环境策略:
- 开发环境(Development):开发人员本地环境
- 测试环境(Testing):自动化测试环境
- 预发布环境(Staging):与生产环境配置相同,用于最终验证
- 生产环境(Production):面向最终用户的环境
7.1.3 分支策略与环境映射
分支类型 | 环境 | 部署策略 | 自动化测试 |
---|---|---|---|
Feature | 开发 | 手动/PR预览 | 单元测试 |
Develop | 测试 | 自动部署 | 单元测试+集成测试 |
Release | 预发布 | 自动部署 | 全套测试 |
Main/Master | 生产 | 手动确认部署 | 冒烟测试 |
Hotfix | 生产 | 紧急部署 | 关键路径测试 |
7.2 GitHub Actions 配置
7.2.1 完整 CI/CD 工作流配置
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop, 'release/*']
pull_request:
branches: [main, develop, 'release/*']
# 支持手动触发工作流
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'staging'
type: choice
options:
- staging
- production
# 环境变量设置
env:
NODE_VERSION: '16'
CACHE_KEY: node-modules-${{ hashFiles('**/package-lock.json') }}
jobs:
# 安装依赖
install:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Cache dependencies
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ env.CACHE_KEY }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
# 代码质量检查
lint:
needs: install
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Restore dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ env.CACHE_KEY }}
- name: Lint code
run: npm run lint
- name: Type check
run: npm run type-check
# 单元测试
test:
needs: install
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Restore dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ env.CACHE_KEY }}
- name: Run tests
run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# 构建应用
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Restore dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ env.CACHE_KEY }}
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: dist
retention-days: 7
# 部署到预发布环境
deploy-staging:
if: github.ref == 'refs/heads/develop' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging')
needs: build
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.your-app.com
concurrency:
group: staging_environment
cancel-in-progress: true
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: dist
- name: Setup Firebase CLI
run: npm install -g firebase-tools
- name: Deploy to Firebase Staging
run: firebase deploy --only hosting:staging --token ${{ secrets.FIREBASE_TOKEN }}
- name: Notify Slack on Success
if: success()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: deployments
SLACK_COLOR: good
SLACK_TITLE: Staging Deployment Successful
SLACK_MESSAGE: 'App deployed to staging environment :rocket:'
# 部署到生产环境
deploy-production:
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production')
needs: build
runs-on: ubuntu-latest
environment:
name: production
url: https://your-app.com
# 需要手动批准生产部署
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: dist
- name: Setup Firebase CLI
run: npm install -g firebase-tools
- name: Deploy to Firebase Production
run: firebase deploy --only hosting:production --token ${{ secrets.FIREBASE_TOKEN }}
- name: Create GitHub Release
if: github.ref == 'refs/heads/main'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release v${{ github.run_number }}
draft: false
prerelease: false
- name: Notify Slack on Success
if: success()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: deployments
SLACK_COLOR: good
SLACK_TITLE: Production Deployment Successful
SLACK_MESSAGE: 'App deployed to production environment :rocket:'
7.2.2 GitHub Actions 最佳实践
- 缓存依赖:使用
actions/cache
缓存 node_modules 以加速构建 - 并行作业:将独立任务拆分为并行作业
- 环境保护:为生产环境设置审批流程
- 并发控制:使用
concurrency
避免同时部署到同一环境 - 失败通知:配置失败时的通知机制
- 版本标记:自动为成功的部署创建版本标签
- 环境变量管理:使用 GitHub Secrets 存储敏感信息
### 7.3 GitLab CI/CD
#### 7.3.0 自动打包部署配置流程
**完整的自动化部署流程配置**
GitLab实现代码push提交后自动打包部署需要以下几个步骤:
**1. 项目结构准备**
your-project/ ├── .gitlab-ci.yml # CI/CD 配置文件 ├── Dockerfile # Docker 镜像构建文件 ├── docker-compose.yml # 本地开发环境 ├── deploy/ # 部署脚本目录 │ ├── staging.sh # 测试环境部署脚本 │ └── production.sh # 生产环境部署脚本 ├── package.json # 项目依赖 └── src/ # 源代码
**2. GitLab CI/CD 变量配置**
在 GitLab 项目设置 → CI/CD → Variables 中配置以下变量:
```bash
# 服务器连接信息
SSH_PRIVATE_KEY # SSH 私钥(Protected, Masked)
DEPLOY_SERVER_HOST # 部署服务器地址
DEPLOY_SERVER_USER # 服务器用户名
# Docker Registry 信息
CI_REGISTRY_USER # GitLab Registry 用户名
CI_REGISTRY_PASSWORD # GitLab Registry 密码
# 部署环境配置
STAGING_SERVER_HOST # 测试环境服务器
PRODUCTION_SERVER_HOST # 生产环境服务器
# 应用配置
APP_NAME # 应用名称
DOMAIN_NAME # 域名
SSL_CERT_PATH # SSL 证书路径
# 通知配置
SLACK_WEBHOOK_URL # Slack 通知地址(Protected, Masked)
WECHAT_WEBHOOK_URL # 企业微信通知地址(Protected, Masked)
3. Dockerfile 配置
# 多阶段构建 Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production && npm cache clean --force
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产环境镜像
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制 nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动命令
CMD ["nginx", "-g", "daemon off;"]
4. 完整的 .gitlab-ci.yml 自动部署配置
# 自动打包部署配置
stages:
- install
- test
- build
- deploy
- notify
variables:
NODE_VERSION: "18"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
# 依赖安装
install:
stage: install
image: node:18-alpine
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths:
- node_modules/
expire_in: 1 hour
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
# 代码测试
test:
stage: test
image: node:18-alpine
script:
- npm run test:ci
- npm run lint
dependencies:
- install
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
# 构建应用
build:
stage: build
image: node:18-alpine
script:
- npm run build
- echo "Build completed at $(date)" > dist/build-info.txt
artifacts:
paths:
- dist/
expire_in: 1 week
dependencies:
- install
only:
- main
- develop
- /^release\/.*$/
# 构建 Docker 镜像
build_docker:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
dependencies:
- build
only:
- main
- develop
# 部署到测试环境(develop 分支自动部署)
deploy_staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client curl
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $STAGING_SERVER_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "Deploying to staging environment..."
# 连接服务器并部署
- |
ssh $DEPLOY_SERVER_USER@$STAGING_SERVER_HOST << 'EOF'
# 停止现有容器
docker stop $APP_NAME-staging || true
docker rm $APP_NAME-staging || true
# 拉取最新镜像
docker pull $CI_REGISTRY_IMAGE:latest
# 启动新容器
docker run -d \
--name $APP_NAME-staging \
--restart unless-stopped \
-p 3000:80 \
-e NODE_ENV=staging \
$CI_REGISTRY_IMAGE:latest
# 健康检查
sleep 10
curl -f http://localhost:3000/health || exit 1
echo "Staging deployment completed successfully"
EOF
environment:
name: staging
url: https://staging.$DOMAIN_NAME
dependencies:
- build_docker
only:
- develop
when: on_success
# 部署到生产环境(main 分支手动部署)
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client curl
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PRODUCTION_SERVER_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "Deploying to production environment..."
- echo "⚠️ Production deployment - Please confirm!"
# 生产环境部署脚本
- |
ssh $DEPLOY_SERVER_USER@$PRODUCTION_SERVER_HOST << 'EOF'
# 备份当前版本
docker tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:backup-$(date +%Y%m%d-%H%M%S) || true
# 停止现有容器
docker stop $APP_NAME-prod || true
docker rm $APP_NAME-prod || true
# 拉取最新镜像
docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 启动新容器
docker run -d \
--name $APP_NAME-prod \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-v /etc/ssl/certs:/etc/ssl/certs:ro \
-e NODE_ENV=production \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 健康检查
sleep 15
curl -f https://$DOMAIN_NAME/health || exit 1
echo "Production deployment completed successfully"
EOF
environment:
name: production
url: https://$DOMAIN_NAME
dependencies:
- build_docker
only:
- main
when: manual # 生产环境需要手动确认
allow_failure: false
# 部署通知
notify_success:
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
# 发送成功通知
curl -X POST -H 'Content-type: application/json' \
--data '{
"text": "✅ 部署成功",
"attachments": [{
"color": "good",
"fields": [
{"title": "项目", "value": "'$CI_PROJECT_NAME'", "short": true},
{"title": "分支", "value": "'$CI_COMMIT_REF_NAME'", "short": true},
{"title": "提交", "value": "'$CI_COMMIT_SHA'", "short": true},
{"title": "环境", "value": "'$CI_ENVIRONMENT_NAME'", "short": true},
{"title": "URL", "value": "'$CI_ENVIRONMENT_URL'", "short": false}
]
}]
}' $SLACK_WEBHOOK_URL
when: on_success
only:
- main
- develop
notify_failure:
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
# 发送失败通知
curl -X POST -H 'Content-type: application/json' \
--data '{
"text": "❌ 部署失败",
"attachments": [{
"color": "danger",
"fields": [
{"title": "项目", "value": "'$CI_PROJECT_NAME'", "short": true},
{"title": "分支", "value": "'$CI_COMMIT_REF_NAME'", "short": true},
{"title": "流水线", "value": "'$CI_PIPELINE_URL'", "short": false}
]
}]
}' $SLACK_WEBHOOK_URL
when: on_failure
only:
- main
- develop
5. 服务器环境准备
# 在部署服务器上安装 Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# 启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker
# 添加用户到 docker 组
sudo usermod -aG docker $USER
# 配置 SSH 密钥认证
# 将 GitLab Runner 的公钥添加到服务器的 ~/.ssh/authorized_keys
6. Nginx 反向代理配置
# /etc/nginx/sites-available/your-app
server {
listen 80;
server_name your-domain.com;
# 重定向到 HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL 配置
ssl_certificate /etc/ssl/certs/your-domain.crt;
ssl_certificate_key /etc/ssl/private/your-domain.key;
# 反向代理到 Docker 容器
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康检查端点
location /health {
proxy_pass http://localhost:3000/health;
}
}
7. 部署流程说明
- 代码提交触发:开发者 push 代码到 GitLab
- 自动构建:GitLab CI/CD 自动触发流水线
- 质量检查:运行测试、代码检查
- 应用构建:构建前端应用
- 镜像构建:构建 Docker 镜像并推送到 Registry
- 自动部署:
develop
分支 → 自动部署到测试环境main
分支 → 手动确认后部署到生产环境
- 健康检查:验证部署是否成功
- 通知反馈:发送部署结果通知
8. 回滚策略
# 回滚作业
rollback_production:
stage: deploy
image: alpine:latest
script:
- echo "Rolling back to previous version..."
- |
ssh $DEPLOY_SERVER_USER@$PRODUCTION_SERVER_HOST << 'EOF'
# 获取备份镜像
BACKUP_IMAGE=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep backup | head -1)
# 停止当前容器
docker stop $APP_NAME-prod
docker rm $APP_NAME-prod
# 启动备份版本
docker run -d \
--name $APP_NAME-prod \
--restart unless-stopped \
-p 80:80 \
$BACKUP_IMAGE
EOF
when: manual
only:
- main
7.3.1 基础配置
# .gitlab-ci.yml
# GitLab CI/CD 配置文件
# 定义流水线阶段
stages:
- install
- quality
- test
- build
- security
- deploy
- notify
# 全局变量定义
variables:
# Node.js 版本
NODE_VERSION: "16"
# 缓存策略
CACHE_FALLBACK_KEY: "default-node-modules"
# Docker 镜像
NODE_IMAGE: "node:16-alpine"
# 应用名称
APP_NAME: "frontend-app"
# 部署环境
STAGING_URL: "https://staging.your-app.com"
PRODUCTION_URL: "https://your-app.com"
# 缓存配置
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
# 全局前置脚本
before_script:
- echo "Starting CI/CD pipeline for commit $CI_COMMIT_SHA"
- echo "Branch: $CI_COMMIT_REF_NAME"
- echo "Pipeline ID: $CI_PIPELINE_ID"
# 依赖安装阶段
install_dependencies:
stage: install
image: $NODE_IMAGE
script:
- echo "Installing dependencies..."
- npm ci --cache .npm --prefer-offline
- echo "Dependencies installed successfully"
artifacts:
paths:
- node_modules/
expire_in: 1 hour
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
only:
- merge_requests
- main
- develop
- /^release\/.*$/
- /^hotfix\/.*$/
# 代码质量检查阶段
code_quality:
stage: quality
image: $NODE_IMAGE
script:
- echo "Running code quality checks..."
# ESLint 检查
- npm run lint -- --format junit --output-file reports/eslint-report.xml
# TypeScript 类型检查
- npm run type-check
# Prettier 格式检查
- npm run format:check
- echo "Code quality checks completed"
artifacts:
reports:
junit: reports/eslint-report.xml
paths:
- reports/
expire_in: 1 week
dependencies:
- install_dependencies
only:
- merge_requests
- main
- develop
- /^release\/.*$/
- /^hotfix\/.*$/
# 样式检查
style_check:
stage: quality
image: $NODE_IMAGE
script:
- echo "Running style checks..."
- npm run stylelint
- echo "Style checks completed"
dependencies:
- install_dependencies
only:
- merge_requests
- main
- develop
- /^release\/.*$/
allow_failure: true
# 单元测试阶段
unit_tests:
stage: test
image: $NODE_IMAGE
script:
- echo "Running unit tests..."
- npm run test:coverage -- --watchAll=false --ci --testResultsProcessor=jest-junit
- echo "Unit tests completed"
artifacts:
reports:
junit: reports/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
dependencies:
- install_dependencies
only:
- merge_requests
- main
- develop
- /^release\/.*$/
- /^hotfix\/.*$/
# 集成测试
integration_tests:
stage: test
image: $NODE_IMAGE
services:
- name: postgres:13
alias: postgres
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db"
script:
- echo "Running integration tests..."
- npm run test:integration
- echo "Integration tests completed"
dependencies:
- install_dependencies
only:
- main
- develop
- /^release\/.*$/
allow_failure: true
# 构建阶段
build_application:
stage: build
image: $NODE_IMAGE
script:
- echo "Building application..."
- npm run build
- echo "Build completed successfully"
# 生成构建信息
- echo "{\"version\":\"$CI_COMMIT_SHA\",\"build_time\":\"$(date -Iseconds)\",\"branch\":\"$CI_COMMIT_REF_NAME\"}" > dist/build-info.json
artifacts:
paths:
- dist/
expire_in: 1 week
dependencies:
- install_dependencies
only:
- merge_requests
- main
- develop
- /^release\/.*$/
- /^hotfix\/.*$/
# 构建 Docker 镜像
build_docker_image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_DRIVER: overlay2
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- echo "Building Docker image..."
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
- echo "Docker image built and pushed successfully"
dependencies:
- build_application
only:
- main
- develop
- /^release\/.*$/
# 安全扫描
security_scan:
stage: security
image: $NODE_IMAGE
script:
- echo "Running security scans..."
# npm audit 安全检查
- npm audit --audit-level moderate
# 使用 snyk 进行安全扫描(需要配置 SNYK_TOKEN)
- if [ -n "$SNYK_TOKEN" ]; then
npm install -g snyk;
snyk test --severity-threshold=high;
fi
- echo "Security scans completed"
dependencies:
- install_dependencies
only:
- main
- develop
- /^release\/.*$/
allow_failure: true
# 部署到开发环境
deploy_development:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- echo "Deploying to development environment..."
# 这里可以添加具体的部署脚本
- curl -X POST "$DEVELOPMENT_WEBHOOK_URL" -H "Content-Type: application/json" -d '{"ref":"'$CI_COMMIT_SHA'"}'
- echo "Deployed to development environment"
environment:
name: development
url: https://dev.your-app.com
dependencies:
- build_application
only:
- develop
when: on_success
# 部署到测试环境
deploy_staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- echo "Deploying to staging environment..."
# 使用 Firebase CLI 部署
- apk add --no-cache nodejs npm
- npm install -g firebase-tools
- firebase use --token $FIREBASE_TOKEN your-project-id
- firebase deploy --only hosting:staging --token $FIREBASE_TOKEN
- echo "Deployed to staging environment successfully"
environment:
name: staging
url: $STAGING_URL
deployment_tier: staging
dependencies:
- build_application
only:
- main
- /^release\/.*$/
when: on_success
# 部署到生产环境
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl nodejs npm
script:
- echo "Deploying to production environment..."
- npm install -g firebase-tools
- firebase use --token $FIREBASE_TOKEN your-project-id
- firebase deploy --only hosting:production --token $FIREBASE_TOKEN
- echo "Deployed to production environment successfully"
environment:
name: production
url: $PRODUCTION_URL
deployment_tier: production
dependencies:
- build_application
only:
- main
when: manual
allow_failure: false
# Kubernetes 部署示例
deploy_k8s_staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- echo "Deploying to Kubernetes staging..."
- kubectl config use-context staging
- kubectl set image deployment/$APP_NAME $APP_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n staging
- kubectl rollout status deployment/$APP_NAME -n staging
- echo "Kubernetes staging deployment completed"
environment:
name: k8s-staging
url: https://k8s-staging.your-app.com
dependencies:
- build_docker_image
only:
- develop
when: on_success
deploy_k8s_production:
stage: deploy
image: bitnami/kubectl:latest
script:
- echo "Deploying to Kubernetes production..."
- kubectl config use-context production
- kubectl set image deployment/$APP_NAME $APP_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/$APP_NAME -n production
- echo "Kubernetes production deployment completed"
environment:
name: k8s-production
url: https://k8s.your-app.com
dependencies:
- build_docker_image
only:
- main
when: manual
# 通知阶段
notify_success:
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- echo "Sending success notification..."
# Slack 通知
- |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"✅ Pipeline succeeded for '"$CI_PROJECT_NAME"' - '"$CI_COMMIT_REF_NAME"' ('"$CI_COMMIT_SHA"')"}' \
$SLACK_WEBHOOK_URL
# 企业微信通知
- |
if [ -n "$WECHAT_WEBHOOK_URL" ]; then
curl -X POST -H 'Content-Type: application/json' \
--data '{"msgtype":"text","text":{"content":"✅ '"$CI_PROJECT_NAME"' 部署成功\n分支: '"$CI_COMMIT_REF_NAME"'\n提交: '"$CI_COMMIT_SHA"'"}}' \
$WECHAT_WEBHOOK_URL
fi
- echo "Success notification sent"
when: on_success
only:
- main
- develop
notify_failure:
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- echo "Sending failure notification..."
- |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"❌ Pipeline failed for '"$CI_PROJECT_NAME"' - '"$CI_COMMIT_REF_NAME"' ('"$CI_COMMIT_SHA"')"}' \
$SLACK_WEBHOOK_URL
- echo "Failure notification sent"
when: on_failure
only:
- main
- develop
7.3.2 高级配置特性
1. 动态环境配置
# 动态环境部署
.deploy_template: &deploy_template
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl nodejs npm
- npm install -g firebase-tools
script:
- firebase use --token $FIREBASE_TOKEN your-project-id
- firebase deploy --only hosting:$ENVIRONMENT_NAME --token $FIREBASE_TOKEN
environment:
name: $ENVIRONMENT_NAME
url: https://$ENVIRONMENT_NAME.your-app.com
deploy_feature_branch:
<<: *deploy_template
variables:
ENVIRONMENT_NAME: "feature-$CI_COMMIT_REF_SLUG"
only:
- /^feature\/.*$/
when: manual
environment:
on_stop: cleanup_feature_branch
cleanup_feature_branch:
stage: deploy
image: alpine:latest
script:
- echo "Cleaning up feature branch environment..."
# 清理临时环境的脚本
environment:
name: "feature-$CI_COMMIT_REF_SLUG"
action: stop
when: manual
only:
- /^feature\/.*$/
2. 条件执行和规则
# 使用 rules 替代 only/except
build_application:
stage: build
image: $NODE_IMAGE
script:
- npm run build
rules:
# 主分支和发布分支总是构建
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^release\/.*$/
# MR 时也构建
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# 手动触发时构建
- if: $CI_PIPELINE_SOURCE == "web"
# 标签推送时构建
- if: $CI_COMMIT_TAG
# 仅在文件变更时执行
frontend_tests:
stage: test
script:
- npm run test:frontend
rules:
- changes:
- "src/**/*"
- "package.json"
- "package-lock.json"
when: always
- when: never
3. 并行作业和矩阵构建
# 并行测试不同 Node.js 版本
.test_template: &test_template
stage: test
script:
- npm ci
- npm run test
test_node_16:
<<: *test_template
image: node:16-alpine
test_node_18:
<<: *test_template
image: node:18-alpine
test_node_20:
<<: *test_template
image: node:20-alpine
# 并行构建不同环境
build:
stage: build
parallel:
matrix:
- ENVIRONMENT: [development, staging, production]
script:
- npm run build:$ENVIRONMENT
artifacts:
paths:
- dist-$ENVIRONMENT/
4. 缓存优化策略
# 多级缓存配置
cache:
- key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push
- key:
files:
- package-lock.json
- webpack.config.js
paths:
- .webpack-cache/
policy: pull-push
# 分布式缓存
variables:
CACHE_COMPRESSION_LEVEL: "fastest"
CACHE_REQUEST_TIMEOUT: 5
7.3.3 GitLab CI/CD 最佳实践
1. 性能优化策略
# 优化的缓存配置
variables:
# 启用缓存压缩
CACHE_COMPRESSION_LEVEL: "fastest"
# 设置缓存超时
CACHE_REQUEST_TIMEOUT: 5
# 使用本地缓存
FF_USE_FASTZIP: "true"
# 多层缓存策略
cache:
# 依赖缓存
- key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
# 构建缓存
- key:
files:
- webpack.config.js
- tsconfig.json
paths:
- .webpack-cache/
- node_modules/.cache/
policy: pull-push
# 并行作业优化
test_parallel:
stage: test
parallel: 4
script:
- npm run test -- --maxWorkers=1 --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
reports:
junit: reports/junit-$CI_NODE_INDEX.xml
2. 安全最佳实践
# 安全变量配置示例
variables:
# 公开变量
NODE_VERSION: "18"
APP_NAME: "frontend-app"
# 在 GitLab UI 中配置的保护变量:
# - FIREBASE_TOKEN (Protected, Masked)
# - SLACK_WEBHOOK_URL (Protected, Masked)
# - DOCKER_REGISTRY_PASSWORD (Protected, Masked)
# - SNYK_TOKEN (Protected, Masked)
# 安全扫描作业
security_audit:
stage: security
image: node:18-alpine
script:
# 依赖安全检查
- npm audit --audit-level moderate --json > audit-report.json || true
# 许可证检查
- npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause;ISC' --excludePrivatePackages
# 代码安全扫描
- if [ -n "$SNYK_TOKEN" ]; then
npm install -g snyk;
snyk auth $SNYK_TOKEN;
snyk test --severity-threshold=high --json > snyk-report.json || true;
snyk monitor;
fi
artifacts:
reports:
# 安全报告
dependency_scanning: audit-report.json
paths:
- audit-report.json
- snyk-report.json
expire_in: 1 week
allow_failure: true
# SAST 静态代码分析
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/License-Scanning.gitlab-ci.yml
# 容器安全扫描
container_scanning:
stage: security
image: docker:stable
services:
- docker:stable-dind
script:
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
-v $(pwd):/tmp aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
dependencies:
- build_docker_image
allow_failure: true
3. 环境管理策略
# 环境配置模板
.deploy_template: &deploy_template
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- echo "Deploying to $ENVIRONMENT_NAME environment"
- echo "Application version: $CI_COMMIT_SHA"
- echo "Deployment time: $(date -Iseconds)"
# 健康检查
- |
for i in {1..30}; do
if curl -f "$HEALTH_CHECK_URL/health"; then
echo "Health check passed"
break
fi
echo "Waiting for application to be ready... ($i/30)"
sleep 10
done
after_script:
# 部署后验证
- curl -f "$HEALTH_CHECK_URL/api/version" | jq .
# 开发环境部署
deploy_development:
<<: *deploy_template
variables:
ENVIRONMENT_NAME: "development"
HEALTH_CHECK_URL: "https://dev.your-app.com"
environment:
name: development
url: https://dev.your-app.com
deployment_tier: development
auto_stop_in: 1 week
rules:
- if: $CI_COMMIT_BRANCH == "develop"
when: on_success
# 预发布环境部署
deploy_staging:
<<: *deploy_template
variables:
ENVIRONMENT_NAME: "staging"
HEALTH_CHECK_URL: "https://staging.your-app.com"
environment:
name: staging
url: https://staging.your-app.com
deployment_tier: staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: on_success
- if: $CI_COMMIT_BRANCH =~ /^release\/.*$/
when: on_success
# 生产环境部署(需要手动确认)
deploy_production:
<<: *deploy_template
variables:
ENVIRONMENT_NAME: "production"
HEALTH_CHECK_URL: "https://your-app.com"
environment:
name: production
url: https://your-app.com
deployment_tier: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
allow_failure: false
before_script:
- echo "⚠️ Production deployment requires manual approval"
- echo "Deploying commit: $CI_COMMIT_SHA"
- echo "Deployed by: $GITLAB_USER_NAME"
4. 监控和通知系统
# 部署监控作业
monitor_deployment:
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- echo "Monitoring deployment health..."
# 性能监控
- |
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' "$PRODUCTION_URL")
echo "Response time: ${RESPONSE_TIME}s"
if (( $(echo "$RESPONSE_TIME > 2.0" | bc -l) )); then
echo "⚠️ High response time detected: ${RESPONSE_TIME}s"
exit 1
fi
# 错误率检查
- |
ERROR_RATE=$(curl -s "$MONITORING_API/error-rate" | jq -r '.rate')
echo "Error rate: ${ERROR_RATE}%"
if (( $(echo "$ERROR_RATE > 5.0" | bc -l) )); then
echo "❌ High error rate detected: ${ERROR_RATE}%"
exit 1
fi
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: delayed
start_in: 5 minutes
# 智能通知系统
.notification_template: ¬ification_template
stage: notify
image: alpine:latest
before_script:
- apk add --no-cache curl jq
notify_deployment_success:
<<: *notification_template
script:
- |
# 构建通知消息
MESSAGE=$(cat <<EOF
{
"text": "✅ 部署成功",
"attachments": [
{
"color": "good",
"fields": [
{"title": "项目", "value": "$CI_PROJECT_NAME", "short": true},
{"title": "分支", "value": "$CI_COMMIT_REF_NAME", "short": true},
{"title": "提交", "value": "$CI_COMMIT_SHA", "short": true},
{"title": "部署者", "value": "$GITLAB_USER_NAME", "short": true},
{"title": "环境", "value": "$CI_ENVIRONMENT_NAME", "short": true},
{"title": "时间", "value": "$(date -Iseconds)", "short": true}
],
"actions": [
{
"type": "button",
"text": "查看应用",
"url": "$CI_ENVIRONMENT_URL"
},
{
"type": "button",
"text": "查看流水线",
"url": "$CI_PIPELINE_URL"
}
]
}
]
}
EOF
)
# 发送 Slack 通知
- curl -X POST -H 'Content-type: application/json' --data "$MESSAGE" "$SLACK_WEBHOOK_URL"
# 发送企业微信通知
- |
WECHAT_MESSAGE=$(cat <<EOF
{
"msgtype": "markdown",
"markdown": {
"content": "## ✅ 部署成功\n\n**项目**: $CI_PROJECT_NAME\n**分支**: $CI_COMMIT_REF_NAME\n**提交**: $CI_COMMIT_SHA\n**环境**: $CI_ENVIRONMENT_NAME\n**部署者**: $GITLAB_USER_NAME\n\n[查看应用]($CI_ENVIRONMENT_URL) | [查看流水线]($CI_PIPELINE_URL)"
}
}
EOF
)
- if [ -n "$WECHAT_WEBHOOK_URL" ]; then
curl -X POST -H 'Content-Type: application/json' --data "$WECHAT_MESSAGE" "$WECHAT_WEBHOOK_URL";
fi
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_JOB_STATUS == "success"
when: on_success
notify_deployment_failure:
<<: *notification_template
script:
- |
# 获取失败的作业信息
FAILED_JOBS=$(curl -s --header "PRIVATE-TOKEN: $CI_API_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs?scope[]=failed" | \
jq -r '.[].name' | tr '\n' ', ' | sed 's/,$//')
MESSAGE=$(cat <<EOF
{
"text": "❌ 部署失败",
"attachments": [
{
"color": "danger",
"fields": [
{"title": "项目", "value": "$CI_PROJECT_NAME", "short": true},
{"title": "分支", "value": "$CI_COMMIT_REF_NAME", "short": true},
{"title": "失败作业", "value": "$FAILED_JOBS", "short": false},
{"title": "流水线", "value": "$CI_PIPELINE_URL", "short": false}
]
}
]
}
EOF
)
- curl -X POST -H 'Content-type: application/json' --data "$MESSAGE" "$SLACK_WEBHOOK_URL"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: on_failure
5. 流水线优化技巧
# 智能触发规则
workflow:
rules:
# 跳过重复的流水线
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"
when: never
# 仅在有意义的变更时运行
- if: $CI_COMMIT_MESSAGE =~ /\[skip ci\]/
when: never
# 正常情况下运行
- when: always
# 条件作业执行
build_docs:
stage: build
script:
- npm run build:docs
rules:
- changes:
- "docs/**/*"
- "*.md"
when: always
- when: never
# 智能重试策略
.retry_config: &retry_config
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- scheduler_failure
test_flaky:
<<: *retry_config
stage: test
script:
- npm run test:e2e
allow_failure: true
# 资源限制
variables:
KUBERNETES_CPU_REQUEST: "100m"
KUBERNETES_CPU_LIMIT: "500m"
KUBERNETES_MEMORY_REQUEST: "128Mi"
KUBERNETES_MEMORY_LIMIT: "512Mi"
6. 调试和故障排除
# 调试模式
debug_pipeline:
stage: test
script:
- echo "=== Environment Variables ==="
- env | grep CI_ | sort
- echo "=== System Information ==="
- uname -a
- echo "=== Node.js Information ==="
- node --version
- npm --version
- echo "=== Package Information ==="
- npm list --depth=0
rules:
- if: $CI_COMMIT_MESSAGE =~ /\[debug\]/
when: always
- when: never
# 失败时保留制品
test_with_artifacts:
stage: test
script:
- npm run test 2>&1 | tee test-output.log
artifacts:
when: always
paths:
- test-output.log
- coverage/
- screenshots/
expire_in: 1 week
after_script:
- echo "Test completed with exit code: $?"
7. 性能监控集成
# 性能基准测试
performance_test:
stage: test
image: node:18-alpine
script:
- npm install -g lighthouse-ci
- npm run build
- npm run serve &
- sleep 10
# Lighthouse CI 性能测试
- lhci autorun --upload.target=temporary-public-storage
artifacts:
reports:
performance: lighthouse-report.json
paths:
- .lighthouseci/
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# 包大小分析
bundle_analysis:
stage: test
script:
- npm run build
- npm run analyze
# 检查包大小是否超过限制
- |
BUNDLE_SIZE=$(du -sb dist/ | cut -f1)
MAX_SIZE=5242880 # 5MB
if [ $BUNDLE_SIZE -gt $MAX_SIZE ]; then
echo "❌ Bundle size ($BUNDLE_SIZE bytes) exceeds limit ($MAX_SIZE bytes)"
exit 1
fi
echo "✅ Bundle size: $BUNDLE_SIZE bytes"
artifacts:
paths:
- dist/
- bundle-analysis.html
8. 多环境配置管理
# 环境变量模板
.env_template: &env_template
variables:
NODE_ENV: $ENVIRONMENT_NAME
API_BASE_URL: $API_BASE_URL
CDN_URL: $CDN_URL
SENTRY_DSN: $SENTRY_DSN
# 开发环境变量
.dev_env: &dev_env
<<: *env_template
variables:
ENVIRONMENT_NAME: "development"
API_BASE_URL: "https://api-dev.your-app.com"
CDN_URL: "https://cdn-dev.your-app.com"
# 生产环境变量
.prod_env: &prod_env
<<: *env_template
variables:
ENVIRONMENT_NAME: "production"
API_BASE_URL: "https://api.your-app.com"
CDN_URL: "https://cdn.your-app.com"
# 使用环境变量
build_dev:
<<: *dev_env
stage: build
script:
- npm run build
rules:
- if: $CI_COMMIT_BRANCH == "develop"
build_prod:
<<: *prod_env
stage: build
script:
- npm run build
rules:
- if: $CI_COMMIT_BRANCH == "main"
9. 最佳实践总结
- 流水线设计:保持流水线简洁、快速、可靠
- 缓存策略:合理使用缓存减少构建时间
- 并行执行:利用并行作业提高效率
- 安全第一:保护敏感信息,集成安全扫描
- 环境隔离:严格区分不同环境的配置
- 监控告警:及时发现和处理问题
- 文档维护:保持 CI/CD 配置的文档更新
- 版本控制:对 CI/CD 配置进行版本管理
- 测试覆盖:确保充分的测试覆盖率
- 回滚策略:制定清晰的回滚计划
4. 维护性
- 模板复用:使用 YAML 锚点和模板
- 配置分离:将配置与代码分离
- 文档完善:维护详细的 CI/CD 文档
- 版本控制:对 CI/CD 配置进行版本控制
7.4 Docker 部署配置
# Dockerfile
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 启用 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 缓存静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
# SPA 路由处理
location / {
try_files $uri $uri/ /index.html;
}
}
8. 监控与性能优化
8.1 前端监控系统
8.1.1 性能监控
使用 Web Vitals 监控核心性能指标:
// 性能监控示例
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
});
// 使用 Beacon API 发送数据
navigator.sendBeacon('/analytics', body);
}
// 监控所有指标
getCLS(sendToAnalytics); // 累积布局偏移
getFID(sendToAnalytics); // 首次输入延迟
getLCP(sendToAnalytics); // 最大内容绘制
getFCP(sendToAnalytics); // 首次内容绘制
getTTFB(sendToAnalytics); // 首字节时间
8.1.2 错误监控
// 错误监控示例
class ErrorMonitor {
constructor() {
this.init();
}
init() {
// 捕获全局错误
window.addEventListener('error', (event) => {
this.reportError({
type: 'js',
message: event.message,
source: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
return true;
}, true);
// 捕获 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
return true;
});
// 捕获资源加载错误
document.addEventListener('error', (event) => {
if (event.target && (event.target.tagName === 'IMG' || event.target.tagName === 'SCRIPT' || event.target.tagName === 'LINK')) {
this.reportError({
type: 'resource',
message: `Failed to load ${event.target.tagName}`,
source: event.target.src || event.target.href,
});
}
}, true);
}
reportError(error) {
// 发送错误到服务器
navigator.sendBeacon('/error-logging', JSON.stringify({
...error,
url: location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
}));
}
}
// 初始化错误监控
new ErrorMonitor();
8.2 性能优化策略
8.2.1 代码分割
// React 代码分割示例
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Loading from './components/Loading';
// 懒加载路由组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Router>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
}
8.2.2 预加载关键资源
// 预加载示例
const prefetchLinks = [
'/api/critical-data.json',
'/images/hero.webp',
];
// 在空闲时预加载资源
if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => {
prefetchLinks.forEach(url => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
});
});
} else {
// 降级处理
setTimeout(() => {
prefetchLinks.forEach(url => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
});
}, 1000);
}
8.2.3 虚拟列表
// 虚拟列表示例 (React)
import { useState, useEffect, useRef } from 'react';
function VirtualList({ items, itemHeight, windowHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);
useEffect(() => {
const handleScroll = () => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop);
}
};
const container = containerRef.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}
}, []);
const totalHeight = items.length * itemHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight));
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + windowHeight) / itemHeight)
);
const visibleItems = [];
for (let i = startIndex; i <= endIndex; i++) {
visibleItems.push(
<div
key={i}
style={{
position: 'absolute',
top: `${i * itemHeight}px`,
height: `${itemHeight}px`,
width: '100%',
}}
>
{items[i]}
</div>
);
}
return (
<div
ref={containerRef}
style={{
height: `${windowHeight}px`,
overflow: 'auto',
position: 'relative',
}}
>
<div style={{ height: `${totalHeight}px` }}>{visibleItems}</div>
</div>
);
}
9. 微前端架构
9.1 微前端概述
9.1.1 什么是微前端
微前端是一种前端架构模式,它将前端应用分解为更小、更易于管理的部分,并使这些部分可以独立开发、测试和部署,同时对用户来说仍然是一个统一的产品。
9.1.2 微前端的核心价值
- 技术栈无关:允许不同团队使用不同的技术栈开发
- 独立开发部署:各团队可以独立开发、测试和部署自己的应用
- 增量升级:可以逐步升级旧系统,而不是一次性重写
- 团队自治:支持多团队并行开发,减少协作成本
- 用户体验一致:对最终用户呈现统一的体验
9.1.3 微前端架构模式
- 基座模式:一个主应用(基座)负责集成和管理子应用
- 去中心化模式:没有明确的主应用,各应用通过约定或路由协议集成
- 混合模式:结合基座模式和去中心化模式的特点
9.1.4 微前端实现方式
- 路由分发:基于 URL 路由将请求分发到不同的前端应用
- iframe 隔离:使用 iframe 加载子应用,提供天然的 JS 和 CSS 隔离
- Web Components:使用 Web Components 封装子应用
- JavaScript 集成:在运行时动态加载子应用的 JavaScript
- 构建时集成:在构建阶段将多个应用合并为一个
9.2 微前端框架选型
框架 | 优势 | 劣势 | 适用场景 | 技术特点 |
---|---|---|---|---|
single-spa | 灵活性高、轻量级、社区活跃 | 样式隔离和通信机制需自行实现 | 需要高度定制化的场景 | 基于路由的生命周期管理 |
qiankun | 开箱即用、完善的隔离方案、中文社区支持 | 基于 single-spa,略显重量级 | 企业级应用、国内团队 | JS 沙箱、CSS 隔离、预加载 |
Module Federation | Webpack 5 原生支持、共享依赖、构建时集成 | 仅支持 Webpack 5、学习曲线陡 | 已使用 Webpack 的项目、组件共享场景 | 运行时模块共享、依赖共享 |
Micro-app | 基于 Web Components、简单易用、性能好 | 相对较新、生态不如其他成熟 | 追求简单实现、需要 DOM 隔离 | Shadow DOM 隔离、无依赖框架 |
Piral | 插件化架构、开发体验好 | 国内资料少 | 需要插件化扩展的应用 | 基于 TypeScript、模块化设计 |
9.3 微前端架构设计
9.3.1 应用拆分策略
- 按业务领域拆分:根据业务功能划分子应用
- 按团队拆分:根据团队职责划分子应用
- 按更新频率拆分:将频繁变化的部分与稳定部分分离
9.3.2 通信机制设计
- 基于 Props 通信:主应用通过 props 向子应用传递数据和回调
- 基于事件总线:使用发布-订阅模式实现应用间通信
- 基于共享状态:使用 Redux、MobX 等状态管理工具共享状态
- 基于 URL:通过 URL 参数传递简单数据
- 基于本地存储:使用 localStorage、sessionStorage 共享数据
// 事件总线实现示例
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(callback => {
callback(...args);
});
}
}
// 创建全局事件总线
window.microAppEventBus = window.microAppEventBus || new EventBus();
// 在子应用中使用
const eventBus = window.microAppEventBus;
// 订阅事件
const unsubscribe = eventBus.on('userLoggedIn', (user) => {
console.log('User logged in:', user);
updateUserInfo(user);
});
// 发布事件
eventBus.emit('cartUpdated', { items: 3, total: 150 });
// 取消订阅
unsubscribe();
9.3.3 样式隔离方案
- BEM 命名约定:使用 Block-Element-Modifier 命名规范避免冲突
- CSS Modules:在构建时生成唯一的类名
- CSS-in-JS:使用 styled-components 等库实现样式隔离
- Shadow DOM:利用 Web Components 的 Shadow DOM 实现完全隔离
- 动态前缀:运行时为 CSS 选择器添加应用特定前缀
// qiankun 中的样式隔离配置
registerMicroApps([
{
name: 'app1',
entry: '//localhost:3001',
container: '#app1-container',
activeRule: '/app1',
props: { data: 'shared data' },
// 样式隔离配置
sandbox: {
// 严格样式隔离
strictStyleIsolation: false,
// 使用选择器前缀实现样式隔离
experimentalStyleIsolation: true,
},
},
]);
9.4 qiankun 微前端实现
9.4.1 主应用配置
// main.js - 主应用入口
import { registerMicroApps, start, initGlobalState } from 'qiankun';
// 初始化全局状态,用于应用间通信
const actions = initGlobalState({
user: null,
theme: 'light',
permissions: [],
});
// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变更:', prev, state);
});
// 注册子应用
registerMicroApps(
[
{
name: 'react-app',
entry: '//localhost:3001',
container: '#react-app-container',
activeRule: '/react-app',
props: {
// 传递给子应用的数据
mainStore: actions,
// 传递给子应用的方法
onMessage: (data) => console.log('来自React子应用的消息:', data),
},
},
{
name: 'vue-app',
entry: '//localhost:3002',
container: '#vue-app-container',
activeRule: '/vue-app',
props: {
mainStore: actions,
onMessage: (data) => console.log('来自Vue子应用的消息:', data),
},
},
{
name: 'angular-app',
entry: '//localhost:3003',
container: '#angular-app-container',
activeRule: '/angular-app',
props: {
mainStore: actions,
onMessage: (data) => console.log('来自Angular子应用的消息:', data),
},
},
],
{
// 生命周期钩子
beforeLoad: [app => console.log(`${app.name} 加载前`)],
beforeMount: [app => console.log(`${app.name} 挂载前`)],
afterMount: [app => console.log(`${app.name} 挂载后`)],
beforeUnmount: [app => console.log(`${app.name} 卸载前`)],
afterUnmount: [app => console.log(`${app.name} 卸载后`)],
}
);
// 启动 qiankun
start({
prefetch: 'all', // 预加载所有子应用
sandbox: {
strictStyleIsolation: false, // 严格的样式隔离(使用 Shadow DOM)
experimentalStyleIsolation: true, // 实验性样式隔离(添加前缀)
// 指定部分全局对象不被代理
patchers: [
(globalContext) => {
// 不代理 window.AMap 对象(例如高德地图)
Object.defineProperty(globalContext, 'AMap', {
enumerable: true,
configurable: true,
get: () => window.AMap,
});
},
],
},
singular: true, // 单实例模式,同一时间只会展示一个子应用
});
// 手动加载子应用(可选)
// import { loadMicroApp } from 'qiankun';
// const microApp = loadMicroApp({
// name: 'app1',
// entry: '//localhost:3001',
// container: '#manual-container',
// });
9.4.2 React 子应用配置
// src/index.js - React 子应用入口
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// 子应用生命周期钩子
let mainStore = null;
// 渲染函数
function render(props) {
const { container, mainStore: store, onMessage } = props || {};
// 保存主应用传递的状态管理器
if (store) {
mainStore = store;
}
// 获取渲染容器
const rootElement = container ? container.querySelector('#root') : document.querySelector('#root');
ReactDOM.render(
<App
mainStore={mainStore}
onMessage={onMessage}
/>,
rootElement
);
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
// 子应用生命周期钩子 - 初始化
export async function bootstrap() {
console.log('React 子应用初始化');
}
// 子应用生命周期钩子 - 挂载
export async function mount(props) {
console.log('React 子应用挂载', props);
render(props);
// 监听全局状态变化
props.mainStore?.onGlobalStateChange((state, prev) => {
console.log('全局状态变化:', prev, state);
// 处理状态变化
});
}
// 子应用生命周期钩子 - 卸载
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container ? container.querySelector('#root') : document.querySelector('#root')
);
}
// 可选:子应用更新钩子
export async function update(props) {
console.log('React 子应用更新', props);
render(props);
}
// 配置 webpack 的 publicPath
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 webpack publicPath,防止资源加载出错
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
9.4.3 子应用配置文件
// .rescriptsrc.js - React 子应用配置
module.exports = {
webpack: (config) => {
config.output.library = 'reactApp';
config.output.libraryTarget = 'umd';
config.output.publicPath = 'http://localhost:3001/';
return config;
},
devServer: (_) => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
// vue.config.js - Vue 子应用配置
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
port: 3002,
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
9.5 Module Federation 实现
9.5.1 主应用配置
// webpack.config.js - 主应用
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
static: path.join(__dirname, 'dist'),
port: 3000,
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
// 模块联邦插件配置
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
// 远程应用配置
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
exposes: {
// 暴露给其他应用的模块
'./Header': './src/components/Header',
'./AuthService': './src/services/auth',
},
shared: {
// 共享依赖配置
react: {
singleton: true, // 确保只加载一个 React 实例
requiredVersion: '^17.0.0', // 指定版本要求
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.0.0',
},
'@material-ui/core': {
singleton: true,
},
// 共享工具库
lodash: {
singleton: true,
},
// 共享状态管理库
'zustand': {
singleton: true,
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
9.5.2 子应用配置
// webpack.config.js - 子应用
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
static: path.join(__dirname, 'dist'),
port: 3001,
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
remotes: {
// 引用主应用暴露的模块
host: 'host@http://localhost:3000/remoteEntry.js',
},
exposes: {
// 暴露给其他应用的组件或模块
'./Button': './src/components/Button',
'./ProductList': './src/components/ProductList',
'./ProductService': './src/services/product',
},
shared: {
// 共享依赖配置,与主应用保持一致
react: {
singleton: true,
requiredVersion: '^17.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.0.0',
},
'@material-ui/core': {
singleton: true,
},
lodash: {
singleton: true,
},
'zustand': {
singleton: true,
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
9.5.3 使用远程模块
// src/App.js - 主应用中使用远程模块
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// 懒加载远程模块
const RemoteButton = lazy(() => import('app1/Button'));
const RemoteProductList = lazy(() => import('app1/ProductList'));
// 直接导入远程服务
import ProductService from 'app1/ProductService';
function App() {
const [products, setProducts] = React.useState([]);
React.useEffect(() => {
// 使用远程服务获取数据
ProductService.getProducts().then(setProducts);
}, []);
return (
<BrowserRouter>
<div>
<h1>主应用</h1>
<nav>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/products">产品列表</Link></li>
</ul>
</nav>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<div>
<h2>欢迎使用微前端应用</h2>
{/* 使用远程组件 */}
<RemoteButton>来自子应用的按钮</RemoteButton>
</div>} />
<Route path="/products" element={<RemoteProductList products={products} />} />
</Routes>
</Suspense>
</div>
</BrowserRouter>
);
}
export default App;
9.6 微前端最佳实践
9.6.1 性能优化
- 预加载策略:根据用户行为预测并预加载子应用
- 代码分割:将公共依赖提取为共享模块
- 缓存策略:合理设置缓存策略,减少重复加载
- 懒加载:按需加载子应用和组件
9.6.2 安全考量
- CSP 策略:设置内容安全策略,防止 XSS 攻击
- 沙箱隔离:确保子应用之间的 JavaScript 隔离
- 权限控制:限制子应用对主应用 API 的访问
- 数据验证:验证应用间通信的数据
9.6.3 开发规范
- 统一依赖管理:使用 pnpm workspace 或 lerna 管理多包项目
- 接口契约:定义清晰的应用间通信接口
- 版本控制:使用语义化版本管理子应用
- 文档规范:维护完善的架构和 API 文档
9.6.4 常见问题解决方案
- 路由冲突:使用路由前缀或命名空间隔离
- 样式冲突:采用 CSS Modules 或 CSS-in-JS
- 全局状态管理:使用发布-订阅模式或共享状态库
- 鉴权处理:统一鉴权逻辑,共享登录状态
## 10. 工程化最佳实践
### 10.1 项目文档化
#### 10.1.1 核心文档体系
- **README.md**:项目概述、安装步骤、使用说明、技术栈、项目结构
- **CONTRIBUTING.md**:贡献指南、开发流程、提交规范、代码审查流程
- **CHANGELOG.md**:版本变更记录、新特性、修复问题、破坏性变更
- **LICENSE**:开源许可证信息
- **架构文档**:系统架构图、模块关系、数据流
- **API文档**:接口说明、参数定义、返回值说明
#### 10.1.2 Storybook 组件文档
Storybook 是一个用于开发和展示UI组件的工具,它提供了一个独立的环境来构建和测试组件。
```javascript
// Button.stories.js
import React from 'react';
import Button from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'select', options: ['primary', 'secondary', 'danger'] },
description: '按钮的样式变体',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
size: {
control: { type: 'select', options: ['small', 'medium', 'large'] },
description: '按钮的大小',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'medium' },
},
},
disabled: {
control: 'boolean',
description: '是否禁用按钮',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
},
onClick: { action: 'clicked', description: '点击按钮时触发的回调函数' },
},
parameters: {
docs: {
description: {
component: '通用按钮组件,支持多种样式变体和大小。',
},
},
componentSubtitle: '高度可定制的按钮组件',
design: {
type: 'figma',
url: 'https://www.figma.com/file/...',
},
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
variant: 'primary',
children: 'Primary Button',
};
Primary.parameters = {
docs: {
description: {
story: '主要按钮,用于页面中的主要操作。',
},
},
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: 'secondary',
children: 'Secondary Button',
};
Secondary.parameters = {
docs: {
description: {
story: '次要按钮,用于页面中的次要操作。',
},
},
};
export const Danger = Template.bind({});
Danger.args = {
variant: 'danger',
children: 'Danger Button',
};
Danger.parameters = {
docs: {
description: {
story: '危险按钮,用于表示危险或不可逆的操作。',
},
},
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
children: 'Disabled Button',
};
Disabled.parameters = {
docs: {
description: {
story: '禁用状态的按钮。',
},
},
};
10.1.3 文档即代码
使用 VuePress 或 Docusaurus 构建项目文档站点:
// docusaurus.config.js
module.exports = {
title: '前端工程化实践',
tagline: '现代前端工程化最佳实践',
url: 'https://your-docusaurus-site.example.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
organizationName: 'your-org',
projectName: 'frontend-engineering',
themeConfig: {
navbar: {
title: '前端工程化',
logo: {
alt: 'Logo',
src: 'img/logo.svg',
},
items: [
{to: 'docs/intro', label: '指南', position: 'left'},
{to: 'docs/architecture', label: '架构', position: 'left'},
{to: 'docs/components', label: '组件', position: 'left'},
{to: 'blog', label: '博客', position: 'left'},
{href: 'https://github.com/your-org/frontend-engineering', label: 'GitHub', position: 'right'},
],
},
footer: {
style: 'dark',
links: [
{
title: '文档',
items: [
{label: '指南', to: 'docs/intro'},
{label: 'API', to: 'docs/api'},
],
},
{
title: '社区',
items: [
{label: 'Stack Overflow', href: 'https://stackoverflow.com/questions/tagged/your-project'},
{label: 'Discord', href: 'https://discord.gg/your-project'},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Your Project. Built with Docusaurus.`,
},
},
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/your-org/frontend-engineering/edit/main/website/',
},
blog: {
showReadingTime: true,
editUrl: 'https://github.com/your-org/frontend-engineering/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
},
],
],
};
10.2 API 文档生成
10.2.1 TypeScript API 文档
使用 TypeDoc 生成 TypeScript API 文档:
// typedoc.json
{
"entryPoints": ["src/index.ts"],
"out": "docs/api",
"name": "项目 API 文档",
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": true,
"theme": "default",
"categorizeByGroup": true,
"categoryOrder": ["Models", "Services", "Components", "*"],
"readme": "none",
"sort": ["alphabetical"],
"validation": {
"invalidLink": true,
"notExported": true
},
"visibilityFilters": {
"protected": false,
"private": false,
"inherited": true,
"external": false
},
"plugin": [
"typedoc-plugin-markdown",
"typedoc-plugin-missing-exports"
]
}
10.2.2 RESTful API 文档
使用 Swagger/OpenAPI 生成 RESTful API 文档:
// swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '前端项目 API',
version: '1.0.0',
description: '前端项目 RESTful API 文档',
contact: {
name: '开发团队',
email: 'dev@example.com',
},
},
servers: [
{
url: 'http://localhost:3000/api',
description: '开发服务器',
},
{
url: 'https://api.example.com',
description: '生产服务器',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [{
bearerAuth: [],
}],
},
apis: ['./src/routes/*.js', './src/models/*.js'],
};
const specs = swaggerJsdoc(options);
module.exports = (app) => {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
explorer: true,
customCss: '.swagger-ui .topbar { display: none }',
}));
};
10.3 版本管理与发布
10.3.1 语义化版本
遵循 Semantic Versioning 规范:
- 主版本号:不兼容的 API 修改(MAJOR)
- 次版本号:向下兼容的功能性新增(MINOR)
- 修订号:向下兼容的问题修正(PATCH)
- 预发布版本:alpha、beta、rc 等(如 1.0.0-beta.1)
10.3.2 版本控制最佳实践
版本号递增规则:
- 修复 bug,递增修订号
- 新增功能但不破坏现有功能,递增次版本号
- 不兼容的变更,递增主版本号
版本标签:
- alpha:内部测试版本
- beta:外部测试版本
- rc (Release Candidate):候选发布版本
版本控制工作流:
- 使用 Git 标签标记版本
- 为每个版本创建发布分支
- 使用 CHANGELOG 记录变更
10.3.3 自动化发布
使用 semantic-release 实现自动化发布:
// package.json
{
"name": "frontend-project",
"version": "0.0.0-development",
"scripts": {
"semantic-release": "semantic-release",
"prepare": "husky install"
},
"devDependencies": {
"semantic-release": "^19.0.0",
"@semantic-release/changelog": "^6.0.0",
"@semantic-release/git": "^10.0.0",
"@semantic-release/github": "^8.0.0",
"@semantic-release/npm": "^9.0.0",
"@semantic-release/release-notes-generator": "^10.0.0",
"husky": "^7.0.0",
"@commitlint/cli": "^16.0.0",
"@commitlint/config-conventional": "^16.0.0"
},
"release": {
"branches": ["main", {"name": "beta", "prerelease": true}],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", {
"changelogFile": "CHANGELOG.md"
}],
"@semantic-release/npm",
["@semantic-release/github", {
"assets": ["dist/**/*.js", "dist/**/*.css"]
}],
["@semantic-release/git", {
"assets": ["package.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}]
]
},
"commitlint": {
"extends": ["@commitlint/config-conventional"]
}
}
使用 GitHub Actions 自动发布:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main, beta]
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run tests
run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
10.4 团队协作最佳实践
10.4.1 协作流程
- 明确分工:前端、后端、设计、产品各司其职,明确职责边界
- 统一规范:代码风格、命名约定、文件组织一致,减少沟通成本
- 知识共享:定期技术分享,文档更新,避免知识孤岛
- Code Review:严格执行代码评审流程,提高代码质量
- 持续集成:自动化测试和部署,快速发现问题
- 敏捷开发:迭代式开发,定期回顾,持续改进
10.4.2 协作工具
- 项目管理:Jira、Trello、Asana
- 代码协作:GitHub、GitLab、Bitbucket
- 文档协作:Confluence、Notion、Google Docs
- 沟通工具:Slack、Microsoft Teams、钉钉
- 设计协作:Figma、Sketch、Adobe XD
10.4.3 跨职能协作
前端与设计:
- 建立设计系统和组件库
- 使用 Figma 等工具进行设计交付
- 定期设计评审会议
前端与后端:
- 明确 API 契约和数据格式
- 使用 Mock 服务进行并行开发
- 共同制定接口文档
前端与产品:
- 参与需求讨论和功能设计
- 提供技术可行性评估
- 收集用户反馈并优化体验
10.4.4 远程协作最佳实践
- 异步沟通:使用文档和任务管理工具记录决策和进度
- 定期同步:固定时间的站会和周会
- 透明度:公开项目进度和问题
- 结对编程:使用屏幕共享和协作工具进行远程结对
- 文档优先:详细记录设计决策和实现细节
10.5 代码质量与安全
10.5.1 代码评审
代码评审是保证代码质量的重要环节,应该建立明确的代码评审流程和标准:
评审清单:
- 代码是否符合项目编码规范
- 是否有潜在的性能问题
- 是否有安全漏洞
- 是否有足够的测试覆盖
- 是否有适当的错误处理
- 是否有冗余或重复代码
评审工具:
- GitHub Pull Request
- GitLab Merge Request
- Gerrit
- Crucible
评审流程示例:
# .github/pull_request_template.md
## 描述
请描述此 PR 的目的和变更内容
## 相关 Issue
- 关联的 Issue: #
## 类型
- [ ] 功能新增
- [ ] Bug 修复
- [ ] 性能优化
- [ ] 代码重构
- [ ] 文档更新
- [ ] 测试新增/修改
- [ ] 其他:
## 自测清单
- [ ] 我已添加必要的测试用例
- [ ] 所有测试用例通过
- [ ] 我已在本地验证功能正常
- [ ] 我已检查代码风格符合项目规范
- [ ] 我已更新相关文档
## 截图(如适用)
## 其他信息
10.5.2 静态代码分析
使用静态代码分析工具自动检测代码问题:
- ESLint 配置:
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true,
jest: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:security/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: [
'react',
'@typescript-eslint',
'react-hooks',
'jsx-a11y',
'import',
'security',
'sonarjs',
'prettier',
],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
rules: {
'prettier/prettier': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'sonarjs/cognitive-complexity': 'warn',
'sonarjs/no-duplicate-string': 'warn',
'sonarjs/no-identical-functions': 'warn',
'security/detect-object-injection': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['to'],
},
],
},
overrides: [
{
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
env: {
jest: true,
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
],
};
- SonarQube 集成:
// sonar-project.properties
sonar.projectKey=frontend-project
sonar.projectName=Frontend Project
sonar.projectVersion=1.0.0
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.exclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/node_modules/**,**/coverage/**,**/build/**,**/dist/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8
10.5.3 依赖安全审计
定期检查和更新依赖项,防止安全漏洞:
- npm audit:
# 检查漏洞
npm audit
# 修复漏洞
npm audit fix
# 生成详细报告
npm audit --json > audit-report.json
- 自动化依赖更新:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
versioning-strategy: auto
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
groups:
dev-dependencies:
dependency-type: "development"
update-type: "all"
production-dependencies:
dependency-type: "production"
update-type: "semver-minor"
10.5.4 安全编码实践
- XSS 防护:
// React 中使用 DOMPurify 净化 HTML
import DOMPurify from 'dompurify';
const UserContent = ({ htmlContent }) => {
const sanitizedHTML = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
};
- CSRF 防护:
// API 请求中添加 CSRF 令牌
import axios from 'axios';
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const api = axios.create({
baseURL: '/api',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
withCredentials: true,
});
export default api;
- 安全的本地存储:
// 敏感数据加密存储
import CryptoJS from 'crypto-js';
const SECRET_KEY = process.env.REACT_APP_STORAGE_SECRET_KEY;
export const secureStorage = {
setItem(key, data) {
const encryptedData = CryptoJS.AES.encrypt(
JSON.stringify(data),
SECRET_KEY
).toString();
localStorage.setItem(key, encryptedData);
},
getItem(key) {
const encryptedData = localStorage.getItem(key);
if (!encryptedData) return null;
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, SECRET_KEY);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
return JSON.parse(decryptedData);
} catch (error) {
console.error('Failed to decrypt data:', error);
return null;
}
},
removeItem(key) {
localStorage.removeItem(key);
},
clear() {
localStorage.clear();
},
};
11. 国际化与本地化
11.1 国际化方案选型
11.1.1 主流国际化库对比
库名 | 适用框架 | 优势 | 劣势 | 适用场景 |
---|---|---|---|---|
react-i18next | React | 功能全面、插件丰富、社区活跃 | 配置相对复杂 | 中大型React应用 |
vue-i18n | Vue | 与Vue深度集成、使用简单 | 功能相对简单 | Vue应用 |
formatjs/react-intl | React | 基于ICU消息格式、强大的格式化能力 | 学习曲线较陡 | 需要复杂格式化的应用 |
next-i18next | Next.js | 服务端渲染支持、与Next.js集成 | 依赖react-i18next | Next.js应用 |
nuxt-i18n | Nuxt.js | 服务端渲染支持、与Nuxt.js集成 | 依赖vue-i18n | Nuxt.js应用 |
i18next | 框架无关 | 高度可扩展、框架无关 | 需要额外适配器 | 任何JavaScript应用 |
11.1.2 选型考虑因素
- 项目规模:小型项目可选择轻量级方案,大型项目需要考虑可扩展性
- 技术栈:选择与当前技术栈最匹配的库
- 功能需求:
- 多语言切换
- 复数处理
- 日期/数字/货币格式化
- 动态加载语言包
- RTL支持
- 性能考量:语言包大小、加载策略、渲染性能
- 团队熟悉度:考虑团队对特定库的熟悉程度
11.2 实现国际化
11.2.1 React 国际化实现
使用 react-i18next 实现国际化:
// i18n.js - 基础配置
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend) // 从服务器加载翻译文件
.use(LanguageDetector) // 自动检测用户语言
.use(initReactI18next) // 将i18n实例传递给react-i18next
.init({
fallbackLng: 'zh-CN', // 回退语言
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React已经安全地转义
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', // 语言文件路径
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'], // 检测顺序
lookupQuerystring: 'lng', // URL参数名
lookupCookie: 'i18next', // Cookie名
lookupLocalStorage: 'i18nextLng', // localStorage键名
caches: ['localStorage', 'cookie'], // 缓存用户语言选择
},
ns: ['common', 'home', 'about'], // 命名空间
defaultNS: 'common', // 默认命名空间
});
export default i18n;
高级用法示例:
// 组件中的使用
import React, { useState, useEffect } from 'react';
import { useTranslation, Trans } from 'react-i18next';
function AdvancedI18nComponent() {
const { t, i18n } = useTranslation(['common', 'home']);
const [count, setCount] = useState(0);
// 语言切换处理
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
document.documentElement.lang = lng; // 更新HTML lang属性
document.documentElement.dir = lng === 'ar' ? 'rtl' : 'ltr'; // RTL支持
};
// 复数形式示例
const itemText = t('item', { count }); // 'item_one' 或 'item_other'
// 格式化日期
const formattedDate = t('date', {
date: new Date(),
formatParams: {
date: {
year: 'numeric',
month: 'long',
day: 'numeric'
}
}
});
return (
<div>
<h1>{t('home:welcome.title')}</h1>
{/* 带有HTML和变量的复杂翻译 */}
<Trans
i18nKey="welcomeMessage"
values={{ name: 'John' }}
components={{ bold: <strong />, link: <a href="/docs" /> }}
>
Welcome <bold>{{ name }}</bold>, please visit our <link>documentation</link>.
</Trans>
<p>{itemText}</p>
<p>{formattedDate}</p>
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => changeLanguage('zh-CN')}>中文</button>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ar')}>العربية</button>
</div>
</div>
);
}
语言文件示例:
// locales/en/common.json
{
"item_zero": "No items",
"item_one": "{{count}} item",
"item_other": "{{count}} items",
"welcomeMessage": "Welcome <bold>{{name}}</bold>, please visit our <link>documentation</link>.",
"date": "{{date, datetime}}"
}
// locales/zh-CN/common.json
{
"item_zero": "没有项目",
"item_one": "{{count}} 个项目",
"item_other": "{{count}} 个项目",
"welcomeMessage": "欢迎 <bold>{{name}}</bold>,请访问我们的<link>文档</link>。",
"date": "{{date, datetime}}"
}
11.2.2 Vue 国际化实现
使用 vue-i18n 实现国际化:
// i18n.js - Vue 3 配置
import { createI18n } from 'vue-i18n';
import axios from 'axios';
// 创建i18n实例
const i18n = createI18n({
legacy: false, // 使用Composition API模式
locale: localStorage.getItem('locale') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {}, // 初始为空,后续动态加载
datetimeFormats: {
'zh-CN': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric'
}
},
'en': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
hour12: true
}
}
},
numberFormats: {
'zh-CN': {
currency: {
style: 'currency',
currency: 'CNY',
notation: 'standard'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
},
percent: {
style: 'percent',
useGrouping: false
}
},
'en': {
currency: {
style: 'currency',
currency: 'USD',
notation: 'standard'
},
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
},
percent: {
style: 'percent',
useGrouping: false
}
}
}
});
// 动态加载语言包
export async function loadLanguageAsync(locale) {
// 检查是否已加载
const { global } = i18n;
if (global.locale.value === locale) return Promise.resolve();
// 如果语言包已加载
if (global.availableLocales.includes(locale)) {
global.locale.value = locale;
document.querySelector('html').setAttribute('lang', locale);
localStorage.setItem('locale', locale);
return Promise.resolve();
}
// 加载新语言包
try {
const messages = await axios.get(`/locales/${locale}.json`).then(res => res.data);
global.setLocaleMessage(locale, messages);
global.locale.value = locale;
document.querySelector('html').setAttribute('lang', locale);
localStorage.setItem('locale', locale);
return Promise.resolve();
} catch (error) {
console.error(`Could not load locale: ${locale}`, error);
return Promise.reject(error);
}
}
export default i18n;
使用示例:
<template>
<div>
<h1>{{ $t('welcome.title') }}</h1>
<p v-html="$t('welcome.description', { name: username })"></p>
<!-- 日期格式化 -->
<p>{{ $d(new Date(), 'long') }}</p>
<!-- 数字格式化 -->
<p>{{ $n(1234.56, 'currency') }}</p>
<!-- 复数 -->
<p>{{ $tc('items', itemCount, { count: itemCount }) }}</p>
<div>
<button @click="changeLanguage('zh-CN')">中文</button>
<button @click="changeLanguage('en')">English</button>
<button @click="itemCount++">增加项目</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { loadLanguageAsync } from '../i18n';
const { t, d, n, tc, locale } = useI18n({ useScope: 'global' });
const username = ref('张三');
const itemCount = ref(1);
async function changeLanguage(lang) {
try {
await loadLanguageAsync(lang);
// 根据语言更新用户名示例
username.value = lang === 'zh-CN' ? '张三' : 'John';
// RTL支持
document.dir = lang === 'ar' ? 'rtl' : 'ltr';
} catch (error) {
console.error('Failed to change language:', error);
}
}
</script>
11.3 多语言文本管理
11.3.1 文本组织结构
/locales
/en
common.json # 通用文本
home.json # 首页文本
product.json # 产品页文本
error.json # 错误信息
/zh-CN
common.json
home.json
product.json
error.json
/ja
...
11.3.2 翻译管理工具
- Lokalise:专业翻译管理平台,支持团队协作
- Crowdin:支持众包翻译的平台
- POEditor:简单易用的翻译管理工具
- i18n-manager:开源的本地翻译管理工具
11.3.3 自动提取文本
使用 i18next-scanner 自动提取需要翻译的文本:
// i18next-scanner.config.js
module.exports = {
input: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}',
'!**/node_modules/**',
],
output: './public/locales',
options: {
debug: true,
removeUnusedKeys: true,
sort: true,
func: {
list: ['t', 'i18next.t', 'i18n.t'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
lngs: ['en', 'zh-CN', 'ja'],
ns: ['common', 'home', 'product', 'error'],
defaultLng: 'en',
defaultNs: 'common',
defaultValue: '',
resource: {
loadPath: '{{lng}}/{{ns}}.json',
savePath: '{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n',
},
},
};
11.4 日期、时间和数字格式化
11.4.1 使用 Intl API
// 日期格式化工具函数
export const dateFormatter = {
format(date, options = {}, locale = navigator.language) {
const defaultOptions = { year: 'numeric', month: 'long', day: 'numeric' };
return new Intl.DateTimeFormat(locale, { ...defaultOptions, ...options }).format(date);
},
// 相对时间格式化(如:3天前,2小时后)
formatRelative(date, locale = navigator.language) {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const now = new Date();
const diffInSeconds = Math.floor((date - now) / 1000);
if (Math.abs(diffInSeconds) < 60) {
return rtf.format(diffInSeconds, 'second');
}
const diffInMinutes = Math.floor(diffInSeconds / 60);
if (Math.abs(diffInMinutes) < 60) {
return rtf.format(diffInMinutes, 'minute');
}
const diffInHours = Math.floor(diffInMinutes / 60);
if (Math.abs(diffInHours) < 24) {
return rtf.format(diffInHours, 'hour');
}
const diffInDays = Math.floor(diffInHours / 24);
if (Math.abs(diffInDays) < 30) {
return rtf.format(diffInDays, 'day');
}
const diffInMonths = Math.floor(diffInDays / 30);
if (Math.abs(diffInMonths) < 12) {
return rtf.format(diffInMonths, 'month');
}
const diffInYears = Math.floor(diffInMonths / 12);
return rtf.format(diffInYears, 'year');
}
};
// 数字格式化工具函数
export const numberFormatter = {
// 一般数字格式化
format(number, options = {}, locale = navigator.language) {
return new Intl.NumberFormat(locale, options).format(number);
},
// 货币格式化
formatCurrency(amount, currency = 'CNY', locale = navigator.language) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
currencyDisplay: 'symbol',
}).format(amount);
},
// 百分比格式化
formatPercent(number, digits = 2, locale = navigator.language) {
return new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}).format(number);
},
// 文件大小格式化
formatFileSize(bytes, locale = navigator.language) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${new Intl.NumberFormat(locale, {
maximumFractionDigits: 2
}).format(bytes / Math.pow(k, i))} ${sizes[i]}`;
}
};
11.5 RTL 支持
11.5.1 CSS 方向控制
/* RTL支持的CSS变量 */
:root {
--direction: ltr;
--start: left;
--end: right;
--text-align: left;
}
[dir="rtl"] {
--direction: rtl;
--start: right;
--end: left;
--text-align: right;
}
.container {
direction: var(--direction);
text-align: var(--text-align);
}
.margin-start {
margin-inline-start: 1rem; /* 自动适应LTR和RTL */
}
.padding-end {
padding-inline-end: 1rem; /* 自动适应LTR和RTL */
}
11.5.2 RTL 转换工具
使用 rtlcss 自动转换 CSS:
// postcss.config.js
module.exports = {
plugins: [
require('postcss-rtl')(),
// 其他 PostCSS 插件
],
};
11.5.3 React 中的 RTL 实现
// RTLProvider.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
const RTLContext = createContext({ isRTL: false });
export const useRTL = () => useContext(RTLContext);
export const RTLProvider = ({ children }) => {
const { i18n } = useTranslation();
const [isRTL, setIsRTL] = useState(false);
useEffect(() => {
// RTL语言列表
const rtlLanguages = ['ar', 'he', 'fa', 'ur'];
const shouldBeRTL = rtlLanguages.includes(i18n.language);
setIsRTL(shouldBeRTL);
document.documentElement.dir = shouldBeRTL ? 'rtl' : 'ltr';
document.body.dir = shouldBeRTL ? 'rtl' : 'ltr';
}, [i18n.language]);
return (
<RTLContext.Provider value={{ isRTL }}>
{children}
</RTLContext.Provider>
);
};
// 使用示例
function App() {
const { isRTL } = useRTL();
return (
<div className={isRTL ? 'rtl-container' : 'ltr-container'}>
{/* 应用内容 */}
</div>
);
}
11.6 本地化最佳实践
设计时考虑国际化:
- 预留足够空间容纳不同语言文本
- 避免在图片中使用文本
- 使用图标代替文字时提供替代文本
文本处理:
- 避免字符串拼接,使用模板和参数
- 处理复数形式和性别区分
- 考虑不同语言的排序规则
性能优化:
- 按需加载语言包
- 缓存已加载的语言资源
- 预编译翻译文件减少运行时开销
测试与验证:
- 为每种支持的语言创建测试用例
- 验证布局在不同语言下的显示
- 检查日期、时间、数字和货币格式
维护策略:
- 建立翻译更新流程
- 使用版本控制管理翻译文件
- 定期审核和更新过时的翻译
12. 无障碍设计与实现
12.1 无障碍标准与法规
12.1.1 主要标准
标准 | 描述 | 重要性 |
---|---|---|
WCAG 2.1 | Web内容无障碍指南,定义了A、AA、AAA三个符合级别 | 国际公认标准 |
WAI-ARIA | Web无障碍倡议-无障碍富互联网应用,提供额外的语义信息 | 动态内容必备 |
Section 508 | 美国联邦法规,要求联邦机构的电子信息对残障人士无障碍 | 美国政府项目必须 |
EN 301 549 | 欧盟无障碍标准 | 欧盟市场必须 |
中国无障碍标准 | GB/T 37668-2019 信息技术 互联网内容无障碍可访问性技术要求 | 中国市场参考 |
12.1.2 无障碍合规级别
- WCAG A级:基本无障碍要求,解决主要障碍
- WCAG AA级:标准合规级别,大多数项目的目标
- WCAG AAA级:最高级别,提供全面无障碍体验
12.1.3 无障碍设计原则
- 可感知性:信息和用户界面组件必须以用户可以感知的方式呈现
- 可操作性:用户界面组件和导航必须可操作
- 可理解性:信息和用户界面操作必须可理解
- 健壮性:内容必须足够健壮,能被各种用户代理解释
12.2 无障碍实现基础
12.2.1 语义化 HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无障碍网页示例</title>
</head>
<body>
<!-- 使用正确的页面结构 -->
<header>
<h1>公司名称</h1>
<!-- 跳转链接,方便键盘用户快速导航 -->
<a href="#main-content" class="skip-link">跳到主要内容</a>
<nav aria-label="主导航">
<ul>
<li><a href="/" aria-current="page">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/services">服务</a></li>
<li><a href="/contact">联系我们</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<section aria-labelledby="section-heading">
<h2 id="section-heading">我们的服务</h2>
<p>我们提供高质量的服务...</p>
<!-- 图片使用适当的alt文本 -->
<figure>
<img src="service.jpg" alt="团队成员在工作场景" width="600" height="400">
<figcaption>我们的专业团队提供全方位服务</figcaption>
</figure>
</section>
</main>
<footer>
<p>© 2023 公司名称. 保留所有权利.</p>
<!-- 提供无障碍声明 -->
<a href="/accessibility">无障碍声明</a>
</footer>
</body>
</html>
12.2.2 常见语义化错误与修正
错误做法 | 正确做法 | 原因 |
---|---|---|
<div class="button" onclick="...">点击</div> | <button type="button">点击</button> | 按钮应使用button元素,自带键盘可访问性 |
<div class="heading">标题</div> | <h2>标题</h2> | 使用正确的标题层级 |
<a onclick="showModal()">打开</a> | <button type="button" onclick="showModal()">打开</button> | 没有href的链接应使用按钮 |
<img src="logo.png"> | <img src="logo.png" alt="公司Logo"> | 图片必须有alt属性 |
12.3 ARIA 属性与角色
12.3.1 ARIA 基础属性
属性类型 | 常用属性 | 用途 |
---|---|---|
角色 | role="button", role="tab" | 定义元素的功能角色 |
状态 | aria-expanded, aria-checked | 描述元素当前状态 |
属性 | aria-label, aria-labelledby | 提供额外的描述信息 |
关系 | aria-controls, aria-owns | 建立元素之间的关系 |
12.3.2 常见组件 ARIA 实现
手风琴组件
// React Accordion组件
function Accordion({ title, children }) {
const [expanded, setExpanded] = useState(false);
const headingId = useId();
const contentId = useId();
return (
<div className="accordion">
<h3>
<button
aria-expanded={expanded}
aria-controls={contentId}
id={headingId}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
{title}
<span aria-hidden="true">{expanded ? '▼' : '▶'}</span>
</button>
</h3>
<div
id={contentId}
role="region"
aria-labelledby={headingId}
hidden={!expanded}
>
{children}
</div>
</div>
);
}
模态对话框
// React Modal组件
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
const previousFocus = useRef(null);
useEffect(() => {
if (isOpen) {
// 保存当前焦点
previousFocus.current = document.activeElement;
// 设置焦点到模态框
modalRef.current.focus();
// 阻止背景滚动
document.body.style.overflow = 'hidden';
// 焦点陷阱
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key === 'Tab') {
// 获取所有可聚焦元素
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
// 循环焦点
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
// 恢复焦点
if (previousFocus.current) {
previousFocus.current.focus();
}
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" role="presentation" onClick={onClose}>
<div
className="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
tabIndex={-1}
onClick={(e) => e.stopPropagation()}
>
<header>
<h2 id="modal-title">{title}</h2>
<button
type="button"
className="close-button"
aria-label="关闭对话框"
onClick={onClose}
>
×
</button>
</header>
<div className="modal-content">
{children}
</div>
</div>
</div>
);
}
12.4 键盘导航与焦点管理
12.4.1 键盘导航模式
// 键盘导航示例
function handleKeyDown(event) {
const { key } = event;
switch (key) {
case 'ArrowDown':
// 移动到下一项
event.preventDefault();
focusNextItem();
break;
case 'ArrowUp':
// 移动到上一项
event.preventDefault();
focusPreviousItem();
break;
case 'Enter':
case ' ':
// 选择当前项
event.preventDefault();
selectCurrentItem();
break;
case 'Escape':
// 关闭菜单
event.preventDefault();
closeMenu();
break;
case 'Home':
// 移动到第一项
event.preventDefault();
focusFirstItem();
break;
case 'End':
// 移动到最后一项
event.preventDefault();
focusLastItem();
break;
default:
break;
}
}
12.4.2 焦点管理工具函数
// 焦点管理工具函数
const FocusManager = {
// 保存焦点
saveFocus() {
this.savedFocus = document.activeElement;
},
// 恢复焦点
restoreFocus() {
if (this.savedFocus && this.savedFocus.focus) {
this.savedFocus.focus();
}
},
// 焦点陷阱 - 将焦点限制在容器内
trapFocus(container) {
if (!container) return () => {};
const focusableElements = container.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return () => {};
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
};
12.5 表单无障碍
12.5.1 无障碍表单示例
// React无障碍表单组件
function AccessibleForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
terms: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
// 清除该字段的错误
if (errors[name]) {
setErrors({
...errors,
[name]: ''
});
}
};
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = '请输入您的姓名';
}
if (!formData.email.trim()) {
newErrors.email = '请输入您的电子邮箱';
} else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
newErrors.email = '请输入有效的电子邮箱';
}
if (!formData.terms) {
newErrors.terms = '请同意服务条款';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
// 表单提交逻辑
alert('表单提交成功!');
} else {
// 焦点到第一个错误字段
const firstError = Object.keys(errors)[0];
if (firstError) {
document.getElementById(firstError)?.focus();
}
}
};
return (
<form onSubmit={handleSubmit} noValidate aria-labelledby="form-title">
<h2 id="form-title">联系我们</h2>
<div className="form-group">
<label htmlFor="name" className="required-field">
姓名
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<div id="name-error" className="error-message" role="alert">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email" className="required-field">
电子邮箱
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
/>
<div id="email-hint" className="hint-text">
我们不会向您发送垃圾邮件
</div>
{errors.email && (
<div id="email-error" className="error-message" role="alert">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message">
留言
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="4"
/>
</div>
<div className="form-group checkbox-group">
<input
type="checkbox"
id="terms"
name="terms"
checked={formData.terms}
onChange={handleChange}
aria-invalid={!!errors.terms}
aria-describedby={errors.terms ? 'terms-error' : undefined}
/>
<label htmlFor="terms" className="required-field">
我已阅读并同意<a href="/terms">服务条款</a>
</label>
{errors.terms && (
<div id="terms-error" className="error-message" role="alert">
{errors.terms}
</div>
)}
</div>
<div className="form-actions">
<button type="submit" className="submit-button">
提交
</button>
<button type="reset" className="reset-button">
重置
</button>
</div>
</form>
);
}
12.6 无障碍测试与审核
12.6.1 自动化测试
// Jest + axe-core 测试示例
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import MyComponent from './MyComponent';
expect.extend(toHaveNoViolations);
test('MyComponent 应该没有无障碍违规', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
12.6.2 集成到 CI/CD 流程
# .github/workflows/accessibility.yml
name: 无障碍测试
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
a11y-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 设置 Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: 安装依赖
run: npm ci
- name: 运行无障碍测试
run: npm run test:a11y
- name: 生成无障碍报告
run: npm run a11y:report
- name: 上传无障碍报告
uses: actions/upload-artifact@v2
with:
name: accessibility-report
path: ./a11y-report/
12.6.3 常用无障碍测试工具
工具名称 | 类型 | 用途 |
---|---|---|
axe-core | JavaScript库 | 自动化无障碍测试 |
jest-axe | 测试框架集成 | 在Jest测试中使用axe |
pa11y | 命令行工具 | 自动化无障碍测试和报告 |
Lighthouse | 浏览器工具 | 性能和无障碍审核 |
WAVE | 浏览器扩展 | 视觉化无障碍评估 |
NVDA/VoiceOver | 屏幕阅读器 | 真实用户体验测试 |
12.7 屏幕阅读器兼容性
12.7.1 主流屏幕阅读器支持策略
屏幕阅读器 | 平台 | 测试重点 |
---|---|---|
NVDA | Windows | 导航结构、表单交互、动态内容 |
JAWS | Windows | 复杂组件、表格、PDF内容 |
VoiceOver | macOS/iOS | 移动响应式布局、手势交互 |
TalkBack | Android | 触摸导航、自定义组件 |
12.7.2 屏幕阅读器测试清单
- 页面标题:确保每个页面有描述性标题
- 页面结构:使用正确的标题层级和区域标记
- 键盘导航:测试Tab键顺序是否合理
- 动态内容:测试动态更新内容是否被正确宣布
- 表单交互:测试表单填写和错误处理
- 自定义组件:测试复杂组件如日期选择器、下拉菜单
- 图像和媒体:测试替代文本和媒体控件
- 模态对话框:测试焦点管理和键盘交互
12.8 无障碍最佳实践
12.8.1 颜色与对比度
/* 高对比度颜色变量 */
:root {
--text-color: #222;
--background-color: #fff;
--link-color: #0066cc;
--link-visited: #551a8b;
--error-color: #d32f2f;
--focus-outline: 2px solid #0066cc;
}
/* 高对比度模式 */
@media (prefers-contrast: high) {
:root {
--text-color: #000;
--background-color: #fff;
--link-color: #0000ee;
--link-visited: #551a8b;
--error-color: #cc0000;
--focus-outline: 3px solid #000;
}
}
/* 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--text-color: #f0f0f0;
--background-color: #121212;
--link-color: #90caf9;
--link-visited: #ce93d8;
--error-color: #f44336;
--focus-outline: 2px solid #90caf9;
}
}
/* 应用样式 */
body {
color: var(--text-color);
background-color: var(--background-color);
}
a {
color: var(--link-color);
}
a:visited {
color: var(--link-visited);
}
.error {
color: var(--error-color);
}
:focus {
outline: var(--focus-outline);
outline-offset: 2px;
}
12.8.2 响应式设计与缩放
/* 响应式无障碍设计 */
html {
/* 允许用户缩放 */
touch-action: manipulation;
-webkit-text-size-adjust: 100%;
}
body {
/* 使用相对单位 */
font-size: 100%;
line-height: 1.5;
}
/* 响应式文本大小 */
h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
}
p, li, label, input, button {
font-size: clamp(1rem, 2vw, 1.25rem);
}
/* 触摸目标大小 */
button, .button, a {
min-height: 44px;
min-width: 44px;
padding: 0.5rem 1rem;
}
/* 响应式间距 */
.container {
padding: clamp(1rem, 3vw, 2rem);
}
/* 媒体查询 - 大文本模式 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001s !important;
transition-duration: 0.001s !important;
}
}
12.8.3 无障碍声明页面
// 无障碍声明页面组件
function AccessibilityStatement() {
return (
<div className="accessibility-statement">
<h1>无障碍声明</h1>
<section>
<h2>我们的承诺</h2>
<p>
我们致力于确保我们的网站对所有用户可访问,包括残障人士。我们正在实施《网页内容无障碍指南》2.1版(WCAG 2.1)AA级标准。
</p>
</section>
<section>
<h2>符合性状态</h2>
<p>
根据WCAG 2.1 AA标准,我们的网站部分符合要求。我们正在努力解决不符合的部分。
</p>
</section>
<section>
<h2>无障碍功能</h2>
<ul>
<li>键盘导航支持</li>
<li>屏幕阅读器兼容性</li>
<li>文本缩放和响应式设计</li>
<li>颜色对比度符合标准</li>
<li>替代文本用于非文本内容</li>
</ul>
</section>
<section>
<h2>已知问题</h2>
<ul>
<li>某些旧的PDF文档可能不完全无障碍</li>
<li>部分第三方内容可能不符合无障碍标准</li>
</ul>
</section>
<section>
<h2>反馈和联系信息</h2>
<p>
我们欢迎您对我们网站的无障碍性提供反馈。如果您在使用我们的网站时遇到问题,或者有改进建议,请联系我们:
</p>
<ul>
<li>电子邮件:<a href="mailto:accessibility@example.com">accessibility@example.com</a></li>
<li>电话:<a href="tel:+8610xxxxxxxx">+86 10 XXXX XXXX</a></li>
</ul>
</section>
<section>
<h2>评估方法</h2>
<p>
我们使用以下方法评估我们网站的无障碍性:
</p>
<ul>
<li>自动化测试工具(Axe, Lighthouse)</li>
<li>使用NVDA和VoiceOver等屏幕阅读器进行测试</li>
<li>键盘导航测试</li>
<li>由残障用户进行的用户测试</li>
</ul>
</section>
</div>
);
}
13. 实际案例分析
13.1 大型电商平台重构
13.1.1 背景与挑战
- 历史技术债务:jQuery + 传统 MVC
- 性能问题:首屏加载时间超过 5 秒
- 开发效率低:功能迭代周期长
13.1.2 解决方案
- 技术栈升级:从 jQuery 迁移到 React + TypeScript
- 微前端架构:将应用拆分为多个子应用
- 性能优化:实施代码分割、懒加载、图片优化等策略
- CI/CD 流程:建立自动化测试和部署流程
13.1.3 成果
- 首屏加载时间减少 60%
- 开发效率提升 40%
- 代码质量显著提高,bug 率降低 30%
13.2 中后台管理系统标准化
13.2.1 背景与挑战
- 多个业务线各自开发管理系统
- 重复造轮子,组件不统一
- 用户体验不一致
13.2.2 解决方案
- 组件库开发:基于 Ant Design 封装业务组件库
- 脚手架标准化:统一项目初始化和目录结构
- 权限管理方案:统一的 RBAC 权限模型
- 主题定制系统:支持多品牌定制
13.2.3 成果
- 新系统开发时间缩短 50%
- 用户培训成本降低 40%
- 维护成本显著降低
14. 参考资源
14.1 官方文档
14.2 推荐书籍
- 《深入浅出 React 和 Redux》
- 《Vue.js 设计与实现》
- 《JavaScript 高级程序设计》
- 《深入理解 TypeScript》
- 《前端工程化:体系设计与实践》
- 《Web 性能权威指南》
- 《重构:改善既有代码的设计》
14.3 学习资源
本文档提供了前端工程化的详细落地实践方案,涵盖了从项目初始化到部署监控的全流程。团队可以根据实际情况选择适合的技术栈和工具,逐步实施工程化改造,提高开发效率和代码质量。通过引入国际化、无障碍设计和实际案例分析,帮助团队更全面地理解和应用前端工程化实践。