阶段 6:前端工程体系 - 企业级落地
工程体系是团队协作的基石。没有规范的工程化,再好的架构也跑不起来。
一、CI/CD 流水线
1.1 完整流水线架构
rust
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 代码提交 │ -> │ CI 检查 │ -> │ 构建打包 │ -> │ 自动部署 │
│ git push │ │ lint/test │ │ build │ │ deploy │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 代码规范检查 │ │ 产物上传 OSS │ │ 灰度/全量发布 │
│ 单元测试 │ │ CDN 刷新 │ │ 健康检查 │
└─────────────┘ └─────────────┘ └─────────────┘
1.2 GitHub Actions 完整配置
yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main]
# 环境变量
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ========== 阶段1: 代码质量检查 ==========
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm run lint
- name: Run Prettier check
run: pnpm run format:check
- name: Run TypeScript check
run: pnpm run type-check
# ========== 阶段2: 测试 ==========
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
- name: Run E2E tests
run: pnpm run test:e2e
# ========== 阶段3: 构建 ==========
build:
name: Build
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
env:
NODE_ENV: production
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
retention-days: 7
- name: Get version
id: version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
# 构建 Docker 镜像
- name: Build Docker image
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} .
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
# 推送到容器镜像仓库
- name: Push Docker image
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# ========== 阶段4: 部署到不同环境 ==========
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v3
with:
name: dist
path: dist/
# 部署到 OSS
- name: Deploy to OSS
uses: manyuanrong/setup-ossutil@v2
with:
endpoint: ${{ secrets.OSS_ENDPOINT }}
access-key-id: ${{ secrets.OSS_ACCESS_KEY_ID }}
access-key-secret: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
- run: ossutil cp -rf ./dist/ oss://staging-bucket/ --update
# 刷新 CDN
- name: Refresh CDN
run: |
curl -X POST "https://cdn.example.com/refresh" \
-H "Authorization: Bearer ${{ secrets.CDN_TOKEN }}" \
-d '{"paths":["/"]}'
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://example.com
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/download-artifact@v3
with:
name: dist
path: dist/
# 灰度发布(先 10%,再全量)
- name: Canary deployment (10%)
run: |
ossutil cp -rf ./dist/ oss://production-bucket/canary/ --update
# 切换 10% 流量到 canary
- name: Wait for canary check
run: sleep 300 # 等待 5 分钟观察
- name: Full deployment
run: |
ossutil cp -rf ./dist/ oss://production-bucket/ --update
# 刷新 CDN 预热
- name: Health check
run: |
curl --retry 5 --retry-delay 10 --fail https://example.com/health
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.version.outputs.VERSION }}
files: |
dist/
generate_release_notes: true
1.3 GitLab CI 配置(企业内网常用)
yaml
# .gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
variables:
PNPM_CACHE_PATH: .pnpm-store
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- ${PNPM_CACHE_PATH}
before_script:
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm config set store-dir ${PNPM_CACHE_PATH}
- pnpm install --frozen-lockfile
# Lint 阶段
lint:
stage: lint
script:
- pnpm run lint
- pnpm run format:check
tags:
- docker
# 测试阶段
test:
stage: test
script:
- pnpm run test:coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# 构建阶段
build:
stage: build
only:
- main
- tags
script:
- pnpm run build
artifacts:
paths:
- dist/
expire_in: 1 week
# 部署到测试环境
deploy_staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- echo "Deploying to staging..."
- scp -r dist/* user@staging-server:/var/www/html/
only:
- main
# 部署到生产环境(手动触发)
deploy_production:
stage: deploy
environment:
name: production
url: https://example.com
script:
- echo "Deploying to production..."
- scp -r dist/* user@prod-server:/var/www/html/
when: manual
only:
- main
- tags
二、代码规范体系
2.1 ESLint + Prettier + Husky 集成
javascript
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier', // 必须放在最后,覆盖冲突规则
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
},
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: true,
node: { paths: ['src'], extensions: ['.js', '.jsx', '.ts', '.tsx'] },
},
},
rules: {
// 代码质量规则
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'warn',
'no-unused-vars': 'off', // 使用 TypeScript 版本
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
// React 规则
'react/react-in-jsx-scope': 'off', // React 17+
'react/prop-types': 'off', // 使用 TypeScript
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import 规则
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-cycle': 'error',
},
overrides: [
{
files: ['*.test.ts', '*.spec.ts', '*.test.tsx'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
],
}
json
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"jsxSingleQuote": false,
"jsxBracketSameLine": false
}
json
// .prettierignore
node_modules/
dist/
build/
coverage/
*.min.js
*.snap
javascript
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 对暂存文件运行 lint-staged
pnpm run lint-staged
# 运行类型检查
pnpm run type-check
json
// package.json
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"type-check": "tsc --noEmit",
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css,scss}": [
"prettier --write"
]
}
}
2.2 Commit 规范(Conventional Commits)
javascript
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // Bug 修复
'docs', // 文档
'style', // 代码格式(不影响功能)
'refactor', // 重构
'perf', // 性能优化
'test', // 测试
'chore', // 构建/工具变动
'ci', // CI 配置
'revert', // 回滚
],
],
'type-case': [2, 'always', 'lower'],
'type-empty': [2, 'never'],
'subject-case': [2, 'always', 'lower'],
'subject-empty': [2, 'never'],
'subject-max-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
},
}
bash
# 提交格式
git commit -m "feat: 添加用户登录功能"
git commit -m "fix(api): 修复用户信息接口超时问题"
git commit -m "perf(virtual-list): 优化大数据量滚动性能"
# 带详情
git commit -m "feat: 添加虚拟列表组件
- 支持动态高度
- 添加滚动预加载
- 优化内存占用
Closes #123"
2.3 Release 流程(semantic-release)
javascript
// .releaserc.js
module.exports = {
branches: ['main', { name: 'beta', prerelease: true }],
plugins: [
[
'@semantic-release/commit-analyzer',
{
preset: 'conventionalcommits',
releaseRules: [
{ type: 'feat', release: 'minor' },
{ type: 'fix', release: 'patch' },
{ type: 'perf', release: 'patch' },
{ type: 'breaking', release: 'major' },
],
},
],
'@semantic-release/release-notes-generator',
'@semantic-release/changelog',
[
'@semantic-release/npm',
{
npmPublish: true,
},
],
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]',
},
],
'@semantic-release/github',
],
}
三、权限系统(RBAC)
3.1 RBAC 核心模型
markdown
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户 │────>│ 角色 │────>│ 权限 │
│ User │ n:m│ Role │ n:m│ Permission│
└──────────┘ └──────────┘ └──────────┘
│ │ │
▼ ▼ ▼
张三/李四 管理员/编辑 读/写/删除
3.2 前端 RBAC 完整实现
typescript
// ========== 类型定义 ==========
interface Permission {
id: string
code: string // 权限代码,如 'user:read', 'order:write'
name: string
type: 'menu' | 'button' | 'api'
}
interface Role {
id: string
name: string
permissions: Permission[]
}
interface User {
id: string
name: string
roles: Role[]
permissions?: Permission[] // 直接分配的权限(可选)
}
// ========== 权限服务 ==========
class PermissionService {
private currentUser: User | null = null
private permissionCache = new Map<string, boolean>()
// 初始化用户权限
async init(userId: string) {
const user = await this.fetchUser(userId)
this.currentUser = user
this.buildPermissionCache()
}
private buildPermissionCache() {
this.permissionCache.clear()
// 收集所有权限(角色权限 + 直接权限)
const allPermissions = new Set<string>()
this.currentUser?.roles.forEach(role => {
role.permissions.forEach(p => allPermissions.add(p.code))
})
this.currentUser?.permissions?.forEach(p => allPermissions.add(p.code))
// 构建缓存
allPermissions.forEach(code => {
this.permissionCache.set(code, true)
})
}
// 权限检查
hasPermission(permissionCode: string): boolean {
// 超级管理员
if (this.currentUser?.roles.some(r => r.name === 'super_admin')) {
return true
}
return this.permissionCache.get(permissionCode) === true
}
// 权限检查(多个条件,满足任意一个即可)
hasAnyPermission(codes: string[]): boolean {
return codes.some(code => this.hasPermission(code))
}
// 权限检查(需要全部满足)
hasAllPermissions(codes: string[]): boolean {
return codes.every(code => this.hasPermission(code))
}
// 获取用户所有权限代码
getPermissions(): string[] {
return Array.from(this.permissionCache.keys())
}
}
// ========== 权限指令(Vue) ==========
// v-permission="'user:read'"
// v-permission="['user:read', 'user:write']" // 满足任意一个
// v-permission:all="['user:read', 'user:write']" // 需要全部满足
const permissionDirective = {
mounted(el: HTMLElement, binding: any, vnode: any) {
const { value, arg } = binding
const permissionService = vnode.context.$permission
if (!value) return
let hasAuth = false
if (arg === 'all') {
hasAuth = permissionService.hasAllPermissions(value)
} else {
hasAuth = permissionService.hasAnyPermission(Array.isArray(value) ? value : [value])
}
if (!hasAuth) {
el.parentNode?.removeChild(el)
}
}
}
// ========== 权限组件(React) ==========
interface PermissionProps {
code: string | string[]
mode?: 'any' | 'all' // any: 任意一个即可, all: 全部需要
children: React.ReactNode
fallback?: React.ReactNode
}
function Permission({ code, mode = 'any', children, fallback = null }: PermissionProps) {
const { hasAnyPermission, hasAllPermissions } = usePermission()
let hasAuth = false
const codes = Array.isArray(code) ? code : [code]
if (mode === 'any') {
hasAuth = hasAnyPermission(codes)
} else {
hasAuth = hasAllPermissions(codes)
}
return hasAuth ? <>{children}</> : <>{fallback}</>
}
// 权限 Hook
function usePermission() {
const permissionService = useContext(PermissionContext)
return {
hasPermission: (code: string) => permissionService.hasPermission(code),
hasAnyPermission: (codes: string[]) => permissionService.hasAnyPermission(codes),
hasAllPermissions: (codes: string[]) => permissionService.hasAllPermissions(codes),
permissions: permissionService.getPermissions(),
}
}
// 使用示例
function UserManagement() {
const { hasPermission } = usePermission()
return (
<div>
<Permission code="user:read">
<UserTable />
</Permission>
<Permission code="user:write" fallback={<DisabledButton />}>
<AddUserButton />
<EditUserButton />
</Permission>
{hasPermission('user:delete') && <DeleteUserButton />}
</div>
)
}
3.3 动态菜单生成
typescript
// 菜单配置
interface MenuItem {
key: string
label: string
icon?: string
path?: string
permission?: string | string[] // 需要的权限
children?: MenuItem[]
}
const allMenus: MenuItem[] = [
{
key: 'dashboard',
label: '仪表盘',
icon: 'Dashboard',
path: '/dashboard',
permission: 'dashboard:view',
},
{
key: 'user',
label: '用户管理',
icon: 'User',
permission: 'user:view',
children: [
{
key: 'user-list',
label: '用户列表',
path: '/user/list',
permission: 'user:read',
},
{
key: 'user-role',
label: '角色管理',
path: '/user/role',
permission: 'role:read',
},
],
},
{
key: 'order',
label: '订单管理',
icon: 'Order',
permission: 'order:view',
children: [
{
key: 'order-list',
label: '订单列表',
path: '/order/list',
permission: 'order:read',
},
{
key: 'order-refund',
label: '退款管理',
path: '/order/refund',
permission: 'order:refund',
},
],
},
]
// 根据权限过滤菜单
function filterMenuByPermission(menus: MenuItem[], permissionService: PermissionService): MenuItem[] {
return menus
.filter(menu => {
if (menu.permission) {
const codes = Array.isArray(menu.permission) ? menu.permission : [menu.permission]
return permissionService.hasAnyPermission(codes)
}
return true
})
.map(menu => ({
...menu,
children: menu.children ? filterMenuByPermission(menu.children, permissionService) : undefined,
}))
.filter(menu => {
// 如果菜单没有子菜单了(但原来有),过滤掉
if (menu.children !== undefined && menu.children.length === 0) {
return false
}
return true
})
}
3.4 路由权限控制
typescript
// 路由守卫
interface RouteMeta {
title: string
permission?: string | string[]
roles?: string[]
}
const router = createRouter({ ... })
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
const permissionService = getPermissionService()
const token = getToken()
// 1. 未登录
if (!token) {
if (to.path !== '/login') {
next(`/login?redirect=${to.path}`)
} else {
next()
}
return
}
// 2. 已登录但没有用户信息,获取用户信息
if (!permissionService.currentUser) {
try {
await permissionService.init(getUserId())
next()
} catch (error) {
// 获取用户信息失败,清除 token 跳转登录
clearToken()
next('/login')
}
return
}
// 3. 检查权限
const meta = to.meta as RouteMeta
if (meta.permission) {
const codes = Array.isArray(meta.permission) ? meta.permission : [meta.permission]
const hasPermission = permissionService.hasAnyPermission(codes)
if (!hasPermission) {
next('/403')
return
}
}
// 4. 检查角色
if (meta.roles) {
const hasRole = permissionService.currentUser.roles.some(r => meta.roles!.includes(r.name))
if (!hasRole) {
next('/403')
return
}
}
next()
})
四、工程体系面试总结
面试高频问题
| 问题 | 回答要点 |
|---|---|
| "如何保证代码质量?" | ESLint + Prettier + Husky + 代码审查 + 单元测试覆盖率门禁 |
| "CI/CD 流程怎么设计?" | lint → test → build → deploy(staging) → manual deploy(prod) |
| "RBAC 和 ABAC 区别?" | RBAC 基于角色,ABAC 基于属性(用户属性、环境属性等),更灵活 |
| "如何处理多环境配置?" | .env 文件 + 构建时注入(不同环境不同 API、不同 feature flag) |
| "前端如何做灰度发布?" | 服务端路由灰度(Nginx)、CDN 动态路由、Feature Flag |