阶段 6:前端工程体系 - 企业级落地

阶段 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

相关推荐
KaMeidebaby2 小时前
卡梅德生物技术快报|多肽库筛选技术构建药物递送功能肽库:流程、算法与质控体
前端·数据库·其他·百度·新浪微博
李剑一2 小时前
字节一面,考察的够全面的啊!面试官:请分阶段解释一下从输入URL到页面渲染整个链路中的关键环节和可观测点
前端
xChive2 小时前
前端请求取消:用 AbortController 从 fetch 到 axios
前端·vue.js·axios·fetch·abortcontroller
Python大数据分析@2 小时前
HTML会代替Markdown吗?为什么?
前端·html
一棵树73512 小时前
js总结介绍
前端·javascript·html
jiayong232 小时前
前端面试题库 - 工程化与性能优化篇
前端·面试·性能优化
暗冰ཏོ2 小时前
2026前端开发资源大全:工具、文档、框架、学习路线与实战指南
前端·前端开发工具·前端编程工具·前端资源·前端开发文档
踩着两条虫2 小时前
AI 低代码引擎可视化设计器交互机制实战
前端·vue.js·人工智能·低代码·架构
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_2:(连接样式表与选择器的实战艺术)
java·前端·css·ui·html·媒体