Vue 3 企业级工程化实践:从 Monorepo 到全栈 DevOps

约 27 分钟阅读

引言:企业级前端工程化的必要性

在企业级项目开发中,单纯的技术实现已经无法满足复杂的业务需求。我们需要建立完整的工程化体系来保证项目的可维护性、可扩展性和团队协作效率。

本文将以 Vue 3 为核心,构建一套完整的企业级工程化解决方案,从代码组织到部署上线的全流程实践。

1. Monorepo 架构设计

1.1 项目结构规划

bash
enterprise-vue-monorepo/
├── apps/                          # 应用程序
│   ├── admin-dashboard/           # 管理后台
│   ├── customer-portal/           # 客户门户
│   ├── mobile-app/               # 移动端应用
│   └── marketing-site/           # 营销网站
├── packages/                     # 共享包
│   ├── ui-components/            # UI组件库
│   ├── shared-utils/             # 工具函数库
│   ├── api-client/               # API客户端
│   ├── design-tokens/            # 设计令牌
│   └── eslint-config/            # ESLint配置
├── tools/                        # 开发工具
│   ├── build-scripts/            # 构建脚本
│   ├── testing-utils/            # 测试工具
│   └── dev-server/               # 开发服务器
├── docs/                         # 文档
│   ├── component-docs/           # 组件文档
│   ├── api-docs/                 # API文档
│   └── design-system/            # 设计系统
├── .github/                      # GitHub配置
│   ├── workflows/                # CI/CD工作流
│   └── templates/                # Issue/PR模板
├── scripts/                      # 项目脚本
├── package.json                  # 根包配置
├── pnpm-workspace.yaml          # PNPM工作空间配置
├── turbo.json                   # Turborepo配置
└── nx.json                      # Nx配置

1.2 Workspace 配置

yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'
  - 'docs/*'
json
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "storybook-static/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.{ts,tsx,vue}", "tests/**/*.{ts,tsx,vue}"]
    },
    "lint": {
      "inputs": ["src/**/*.{ts,tsx,vue}", "*.{js,ts,json}"]
    },
    "type-check": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.{ts,tsx,vue}", "*.{ts,json}"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  },
  "globalDependencies": [
    "package.json",
    "pnpm-lock.yaml",
    "turbo.json",
    ".env",
    ".env.local"
  ]
}

1.3 根目录配置

json
// package.json
{
  "name": "enterprise-vue-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build": "turbo run build",
    "build:apps": "turbo run build --filter='./apps/*'",
    "build:packages": "turbo run build --filter='./packages/*'",
    "dev": "turbo run dev",
    "dev:admin": "turbo run dev --filter=admin-dashboard",
    "dev:docs": "turbo run dev --filter=component-docs",
    "test": "turbo run test",
    "test:unit": "turbo run test:unit",
    "test:e2e": "turbo run test:e2e",
    "lint": "turbo run lint",
    "lint:fix": "turbo run lint:fix",
    "type-check": "turbo run type-check",
    "clean": "turbo run clean && rm -rf node_modules",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "release": "changeset publish",
    "prepare": "husky install"
  },
  "devDependencies": {
    "@changesets/cli": "^2.26.0",
    "@commitlint/cli": "^17.4.4",
    "@commitlint/config-conventional": "^17.4.4",
    "husky": "^8.0.3",
    "lint-staged": "^13.1.2",
    "turbo": "^1.8.3",
    "typescript": "^4.9.5"
  },
  "packageManager": "pnpm@8.0.0"
}

2. 组件库开发体系

2.1 设计系统基础

typescript
// packages/design-tokens/src/index.ts
export const designTokens = {
  colors: {
    primary: {
      50: '#eff6ff',
      100: '#dbeafe', 
      200: '#bfdbfe',
      300: '#93c5fd',
      400: '#60a5fa',
      500: '#3b82f6',
      600: '#2563eb',
      700: '#1d4ed8',
      800: '#1e40af',
      900: '#1e3a8a'
    },
    semantic: {
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
      info: '#3b82f6'
    },
    neutral: {
      0: '#ffffff',
      50: '#f9fafb',
      100: '#f3f4f6',
      200: '#e5e7eb',
      300: '#d1d5db',
      400: '#9ca3af',
      500: '#6b7280',
      600: '#4b5563',
      700: '#374151',
      800: '#1f2937',
      900: '#111827',
      1000: '#000000'
    }
  },
  
  typography: {
    fontFamily: {
      sans: ['Inter', 'system-ui', 'sans-serif'],
      mono: ['JetBrains Mono', 'monospace']
    },
    fontSize: {
      xs: ['0.75rem', { lineHeight: '1rem' }],
      sm: ['0.875rem', { lineHeight: '1.25rem' }],
      base: ['1rem', { lineHeight: '1.5rem' }],
      lg: ['1.125rem', { lineHeight: '1.75rem' }],
      xl: ['1.25rem', { lineHeight: '1.75rem' }],
      '2xl': ['1.5rem', { lineHeight: '2rem' }],
      '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
      '4xl': ['2.25rem', { lineHeight: '2.5rem' }]
    },
    fontWeight: {
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700'
    }
  },
  
  spacing: {
    0: '0',
    1: '0.25rem',
    2: '0.5rem', 
    3: '0.75rem',
    4: '1rem',
    6: '1.5rem',
    8: '2rem',
    12: '3rem',
    16: '4rem',
    20: '5rem',
    24: '6rem'
  },
  
  borderRadius: {
    none: '0',
    sm: '0.125rem',
    base: '0.25rem',
    md: '0.375rem',
    lg: '0.5rem',
    xl: '0.75rem',
    '2xl': '1rem',
    full: '9999px'
  },
  
  shadows: {
    sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
    base: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
    md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
    lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
    xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'
  },
  
  breakpoints: {
    sm: '640px',
    md: '768px', 
    lg: '1024px',
    xl: '1280px',
    '2xl': '1536px'
  },
  
  zIndex: {
    hide: -1,
    auto: 'auto',
    base: 0,
    docked: 10,
    dropdown: 1000,
    sticky: 1100,
    banner: 1200,
    overlay: 1300,
    modal: 1400,
    popover: 1500,
    skipLink: 1600,
    toast: 1700,
    tooltip: 1800
  }
} as const

export type DesignTokens = typeof designTokens
export type ColorPalette = keyof typeof designTokens.colors
export type ColorShade = keyof typeof designTokens.colors.primary

2.2 组件库架构

typescript
// packages/ui-components/src/components/Button/Button.vue
<script setup lang="ts">
import { computed, type PropType } from 'vue'
import { designTokens } from '@enterprise/design-tokens'
import type { ComponentSize, ComponentVariant } from '../../types'

interface ButtonProps {
  variant?: ComponentVariant
  size?: ComponentSize
  disabled?: boolean
  loading?: boolean
  block?: boolean
  icon?: string
  iconPosition?: 'left' | 'right'
  type?: 'button' | 'submit' | 'reset'
}

const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false,
  loading: false,
  block: false,
  iconPosition: 'left',
  type: 'button'
})

const emit = defineEmits<{
  click: [event: MouseEvent]
}>()

// 计算样式类
const buttonClasses = computed(() => [
  'btn',
  `btn--${props.variant}`,
  `btn--${props.size}`,
  {
    'btn--disabled': props.disabled,
    'btn--loading': props.loading,
    'btn--block': props.block,
    'btn--icon-only': !$slots.default && props.icon
  }
])

const handleClick = (event: MouseEvent) => {
  if (props.disabled || props.loading) return
  emit('click', event)
}
</script>

<template>
  <button
    :class="buttonClasses"
    :type="type"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <!-- 加载状态 -->
    <span v-if="loading" class="btn__spinner">
      <svg class="animate-spin" viewBox="0 0 24 24">
        <circle
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          stroke-width="4"
          fill="none"
          stroke-dasharray="32"
          stroke-dashoffset="32"
        />
      </svg>
    </span>
    
    <!-- 左侧图标 -->
    <span 
      v-if="icon && iconPosition === 'left' && !loading"
      class="btn__icon btn__icon--left"
    >
      <component :is="icon" />
    </span>
    
    <!-- 按钮内容 -->
    <span v-if="$slots.default" class="btn__content">
      <slot />
    </span>
    
    <!-- 右侧图标 -->
    <span
      v-if="icon && iconPosition === 'right' && !loading" 
      class="btn__icon btn__icon--right"
    >
      <component :is="icon" />
    </span>
  </button>
</template>

<style scoped>
.btn {
  /* 基础样式 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border: 1px solid transparent;
  border-radius: v-bind('designTokens.borderRadius.md');
  font-family: v-bind('designTokens.typography.fontFamily.sans');
  font-weight: v-bind('designTokens.typography.fontWeight.medium');
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: all 0.2s ease;
  outline: none;
  position: relative;
  overflow: hidden;
}

.btn:focus-visible {
  outline: 2px solid v-bind('designTokens.colors.primary[500]');
  outline-offset: 2px;
}

/* 尺寸变体 */
.btn--small {
  padding: v-bind('designTokens.spacing[2]') v-bind('designTokens.spacing[3]');
  font-size: v-bind('designTokens.typography.fontSize.sm[0]');
  line-height: v-bind('designTokens.typography.fontSize.sm[1].lineHeight');
}

.btn--medium {
  padding: v-bind('designTokens.spacing[3]') v-bind('designTokens.spacing[4]');
  font-size: v-bind('designTokens.typography.fontSize.base[0]');
  line-height: v-bind('designTokens.typography.fontSize.base[1].lineHeight');
}

.btn--large {
  padding: v-bind('designTokens.spacing[4]') v-bind('designTokens.spacing[6]');
  font-size: v-bind('designTokens.typography.fontSize.lg[0]');
  line-height: v-bind('designTokens.typography.fontSize.lg[1].lineHeight');
}

/* 颜色变体 */
.btn--primary {
  background-color: v-bind('designTokens.colors.primary[600]');
  color: white;
  border-color: v-bind('designTokens.colors.primary[600]');
}

.btn--primary:hover:not(.btn--disabled) {
  background-color: v-bind('designTokens.colors.primary[700]');
  border-color: v-bind('designTokens.colors.primary[700]');
}

.btn--secondary {
  background-color: v-bind('designTokens.colors.neutral[100]');
  color: v-bind('designTokens.colors.neutral[900]');
  border-color: v-bind('designTokens.colors.neutral[300]');
}

.btn--secondary:hover:not(.btn--disabled) {
  background-color: v-bind('designTokens.colors.neutral[200]');
  border-color: v-bind('designTokens.colors.neutral[400]');
}

.btn--outline {
  background-color: transparent;
  color: v-bind('designTokens.colors.primary[600]');
  border-color: v-bind('designTokens.colors.primary[600]');
}

.btn--outline:hover:not(.btn--disabled) {
  background-color: v-bind('designTokens.colors.primary[50]');
}

.btn--ghost {
  background-color: transparent;
  color: v-bind('designTokens.colors.primary[600]');
  border-color: transparent;
}

.btn--ghost:hover:not(.btn--disabled) {
  background-color: v-bind('designTokens.colors.primary[50]');
}

/* 状态 */
.btn--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--loading {
  cursor: wait;
}

.btn--block {
  width: 100%;
}

.btn--icon-only {
  aspect-ratio: 1;
}

/* 子元素 */
.btn__spinner {
  width: 1em;
  height: 1em;
}

.btn__icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}

.btn__content {
  flex: 1;
}

/* 动画 */
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.animate-spin {
  animation: spin 1s linear infinite;
}
</style>

2.3 组件库构建配置

typescript
// packages/ui-components/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      include: ['src/**/*'],
      exclude: ['src/**/*.stories.ts', 'src/**/*.test.ts'],
      outDir: 'dist/types'
    })
  ],
  
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'EnterpriseUIComponents',
      fileName: (format) => `index.${format}.js`,
      formats: ['es', 'cjs', 'umd']
    },
    
    rollupOptions: {
      external: ['vue', '@enterprise/design-tokens'],
      output: {
        globals: {
          vue: 'Vue',
          '@enterprise/design-tokens': 'DesignTokens'
        },
        exports: 'named'
      }
    },
    
    cssCodeSplit: true,
    sourcemap: true
  },
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

3. 代码质量保证体系

3.1 ESLint 配置包

typescript
// packages/eslint-config/index.js
module.exports = {
  extends: [
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  
  rules: {
    // Vue 规则
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/component-definition-name-casing': ['error', 'PascalCase'],
    'vue/custom-event-name-casing': ['error', 'camelCase'],
    'vue/define-emits-declaration': ['error', 'type-based'],
    'vue/define-macros-order': ['error', {
      order: ['defineProps', 'defineEmits', 'defineExpose']
    }],
    'vue/define-props-declaration': ['error', 'type-based'],
    'vue/html-button-has-type': 'error',
    'vue/html-self-closing': ['error', {
      html: { void: 'always', normal: 'always', component: 'always' },
      svg: 'always',
      math: 'always'
    }],
    'vue/no-root-v-if': 'error',
    'vue/no-undef-components': 'error',
    'vue/no-undef-properties': 'error',
    'vue/no-unused-refs': 'error',
    'vue/no-useless-v-bind': 'error',
    'vue/padding-line-between-blocks': 'error',
    'vue/prefer-separate-static-class': 'error',
    'vue/prefer-true-attribute-shorthand': 'error',
    'vue/require-macro-variable-name': 'error',
    'vue/require-typed-ref': 'error',
    'vue/v-for-delimiter-style': ['error', 'in'],
    
    // TypeScript 规则
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { 
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_'
    }],
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/consistent-type-imports': ['error', {
      prefer: 'type-imports',
      disallowTypeAnnotations: false
    }],
    '@typescript-eslint/no-import-type-side-effects': 'error',
    
    // 通用规则
    'prefer-const': 'error',
    'no-var': 'error',
    'object-shorthand': 'error',
    'prefer-template': 'error',
    'template-curly-spacing': 'error',
    'arrow-spacing': 'error',
    'comma-dangle': ['error', 'never'],
    'comma-spacing': 'error',
    'comma-style': 'error',
    'curly': ['error', 'multi-line'],
    'dot-location': ['error', 'property'],
    'eol-last': 'error',
    'func-call-spacing': 'error',
    'key-spacing': 'error',
    'keyword-spacing': 'error',
    'lines-between-class-members': ['error', 'always', { 
      exceptAfterSingleLine: true 
    }],
    'multiline-ternary': ['error', 'always-multiline'],
    'no-console': 'warn',
    'no-debugger': 'error',
    'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
    'no-trailing-spaces': 'error',
    'object-curly-spacing': ['error', 'always'],
    'padded-blocks': ['error', 'never'],
    'quotes': ['error', 'single', { avoidEscape: true }],
    'semi': ['error', 'never'],
    'space-before-blocks': 'error',
    'space-before-function-paren': ['error', {
      anonymous: 'always',
      named: 'never',
      asyncArrow: 'always'
    }],
    'space-in-parens': 'error',
    'space-infix-ops': 'error',
    'space-unary-ops': 'error',
    'spaced-comment': 'error'
  },
  
  overrides: [
    {
      files: ['**/*.test.{js,ts,vue}', '**/*.spec.{js,ts,vue}'],
      env: {
        jest: true,
        'vitest-globals/env': true
      },
      extends: [
        'plugin:testing-library/vue',
        'plugin:vitest-globals/recommended'
      ],
      rules: {
        '@typescript-eslint/no-explicit-any': 'off'
      }
    },
    {
      files: ['**/*.stories.{js,ts}'],
      rules: {
        '@typescript-eslint/no-explicit-any': 'off',
        'vue/one-component-per-file': 'off'
      }
    }
  ]
}

3.2 TypeScript 配置

json
// packages/tsconfig/base.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@enterprise/*": ["../../packages/*/src"]
    },
    
    "types": ["vite/client", "vitest/globals"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts", 
    "src/**/*.tsx",
    "src/**/*.vue"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}

3.3 Git Hooks 配置

bash
#!/bin/sh
# .husky/pre-commit
. "$(dirname "$0")/_/husky.sh"

# 运行 lint-staged
npx lint-staged

# 类型检查
echo "🔍 Running type check..."
npm run type-check

# 运行测试
echo "🧪 Running tests..."
npm run test:affected
json
// .lintstagedrc.json
{
  "*.{js,ts,vue}": [
    "eslint --fix",
    "prettier --write"
  ],
  "*.{css,scss,vue}": [
    "stylelint --fix",
    "prettier --write"  
  ],
  "*.{json,md,yaml,yml}": [
    "prettier --write"
  ],
  "package.json": [
    "sort-package-json"
  ]
}
javascript
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新功能
        'fix',      // 修复
        'docs',     // 文档
        'style',    // 格式(不影响代码运行的变动)
        'refactor', // 重构
        'perf',     // 性能优化
        'test',     // 增加测试
        'chore',    // 构建过程或辅助工具的变动
        'revert',   // 回滚
        'build',    // 构建系统或外部依赖项的更改
        'ci'        // CI 配置文件和脚本的更改
      ]
    ],
    'subject-max-length': [2, 'always', 72],
    'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
    'header-max-length': [2, 'always', 100]
  }
}

4. 测试策略与自动化

4.1 单元测试配置

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['src/**/*.{test,spec}.{js,ts,vue}'],
    exclude: ['node_modules', 'dist', 'e2e'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.{js,ts,vue}'],
      exclude: [
        'src/**/*.d.ts',
        'src/**/*.test.{js,ts,vue}',
        'src/**/*.spec.{js,ts,vue}',
        'src/**/*.stories.{js,ts}',
        'src/main.ts'
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80
        }
      }
    }
  },
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@enterprise': resolve(__dirname, '../../packages')
    }
  }
})
typescript
// tests/setup.ts
import { vi } from 'vitest'
import { config } from '@vue/test-utils'

// 全局模拟
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn()
  }))
})

// 全局组件注册
config.global.components = {
  // 注册测试中需要的全局组件
}

// 全局插件
config.global.plugins = [
  // 添加测试中需要的插件
]

// 模拟 CSS 模块
vi.mock('*.module.css', () => ({
  default: new Proxy({}, {
    get: (target, prop) => prop
  })
}))

4.2 组件测试示例

typescript
// packages/ui-components/src/components/Button/Button.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'

describe('Button Component', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Click me'
      }
    })
    
    expect(wrapper.text()).toContain('Click me')
    expect(wrapper.classes()).toContain('btn')
    expect(wrapper.classes()).toContain('btn--primary')
    expect(wrapper.classes()).toContain('btn--medium')
  })
  
  it('handles click events', async () => {
    const onClick = vi.fn()
    const wrapper = mount(Button, {
      props: { onClick },
      slots: { default: 'Click me' }
    })
    
    await wrapper.trigger('click')
    expect(onClick).toHaveBeenCalledOnce()
  })
  
  it('prevents click when disabled', async () => {
    const onClick = vi.fn()
    const wrapper = mount(Button, {
      props: { disabled: true, onClick },
      slots: { default: 'Click me' }
    })
    
    await wrapper.trigger('click')
    expect(onClick).not.toHaveBeenCalled()
    expect(wrapper.classes()).toContain('btn--disabled')
  })
  
  it('shows loading state', () => {
    const wrapper = mount(Button, {
      props: { loading: true },
      slots: { default: 'Click me' }
    })
    
    expect(wrapper.classes()).toContain('btn--loading')
    expect(wrapper.find('.btn__spinner').exists()).toBe(true)
  })
  
  it('applies correct variant classes', () => {
    const variants = ['primary', 'secondary', 'outline', 'ghost'] as const
    
    variants.forEach(variant => {
      const wrapper = mount(Button, {
        props: { variant },
        slots: { default: 'Button' }
      })
      
      expect(wrapper.classes()).toContain(`btn--${variant}`)
    })
  })
  
  it('applies correct size classes', () => {
    const sizes = ['small', 'medium', 'large'] as const
    
    sizes.forEach(size => {
      const wrapper = mount(Button, {
        props: { size },
        slots: { default: 'Button' }
      })
      
      expect(wrapper.classes()).toContain(`btn--${size}`)
    })
  })
  
  it('renders with icon', () => {
    const wrapper = mount(Button, {
      props: { icon: 'star-icon' },
      slots: { default: 'Starred' }
    })
    
    expect(wrapper.find('.btn__icon').exists()).toBe(true)
  })
  
  it('has correct accessibility attributes', () => {
    const wrapper = mount(Button, {
      props: { type: 'submit', disabled: true },
      slots: { default: 'Submit' }
    })
    
    expect(wrapper.attributes('type')).toBe('submit')
    expect(wrapper.attributes('disabled')).toBeDefined()
  })
})

4.3 E2E 测试配置

typescript
// e2e/playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure'
  },
  
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] }
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] }
    }
  ],
  
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI
  }
})

5. CI/CD 流水线

5.1 GitHub Actions 工作流

yaml
# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 代码质量检查
  quality-check:
    name: Code Quality Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm type-check

      - name: Check formatting
        run: pnpm prettier --check .

  # 单元测试
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: pnpm test:coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

  # E2E 测试
  e2e-tests:
    name: E2E Tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: pnpm playwright install --with-deps

      - name: Build applications
        run: pnpm build

      - name: Run E2E tests
        run: pnpm test:e2e

      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

  # 构建检查
  build-check:
    name: Build Check
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [admin-dashboard, customer-portal, mobile-app]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build ${{ matrix.app }}
        run: pnpm --filter ${{ matrix.app }} build

      - name: Check bundle size
        run: |
          cd apps/${{ matrix.app }}
          npx bundlesize

  # 安全扫描
  security-scan:
    name: Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  # 依赖审计
  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'

      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Audit dependencies
        run: pnpm audit

      - name: Check for outdated dependencies
        run: pnpm outdated
        continue-on-error: true

5.2 部署流水线

yaml
# .github/workflows/deploy.yml
name: Deploy Pipeline

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  # 构建和推送 Docker 镜像
  build-and-push:
    name: Build and Push Images
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [admin-dashboard, customer-portal, mobile-app]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.REGISTRY_URL }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.REGISTRY_URL }}/enterprise/${{ matrix.app }}
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=sha,prefix=sha-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: ./apps/${{ matrix.app }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 部署到 Kubernetes
  deploy-to-k8s:
    name: Deploy to Kubernetes
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: 'v1.28.0'

      - name: Configure kubectl
        run: |
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig

      - name: Deploy to staging
        run: |
          export KUBECONFIG=kubeconfig
          export IMAGE_TAG=sha-${{ github.sha }}
          envsubst < k8s/staging/kustomization.yaml | kubectl apply -k -

      - name: Wait for deployment
        run: |
          export KUBECONFIG=kubeconfig
          kubectl rollout status deployment/admin-dashboard -n staging
          kubectl rollout status deployment/customer-portal -n staging

      - name: Run smoke tests
        run: |
          # 运行部署后的烟雾测试
          pnpm test:smoke --env=staging

  # 生产部署(需要手动批准)
  deploy-to-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: deploy-to-k8s
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          export KUBECONFIG=kubeconfig
          export IMAGE_TAG=${{ github.ref_name }}
          envsubst < k8s/production/kustomization.yaml | kubectl apply -k -

      - name: Notify deployment
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          channel: '#deployments'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

6. 多环境管理

6.1 环境配置管理

typescript
// tools/config-manager/src/index.ts
import { z } from 'zod'

// 环境配置schema
const EnvironmentConfigSchema = z.object({
  app: z.object({
    name: z.string(),
    version: z.string(),
    environment: z.enum(['development', 'staging', 'production']),
    debug: z.boolean(),
    port: z.number().int().min(1000).max(65535)
  }),
  
  api: z.object({
    baseUrl: z.string().url(),
    timeout: z.number().int().positive(),
    retries: z.number().int().min(0),
    apiKey: z.string().optional()
  }),
  
  database: z.object({
    host: z.string(),
    port: z.number().int(),
    username: z.string(),
    password: z.string(),
    database: z.string(),
    ssl: z.boolean()
  }),
  
  redis: z.object({
    host: z.string(),
    port: z.number().int(),
    password: z.string().optional(),
    db: z.number().int().min(0)
  }),
  
  security: z.object({
    jwtSecret: z.string().min(32),
    corsOrigins: z.array(z.string()),
    rateLimitWindowMs: z.number().int().positive(),
    rateLimitMaxRequests: z.number().int().positive()
  }),
  
  monitoring: z.object({
    enabled: z.boolean(),
    sentryDsn: z.string().optional(),
    logLevel: z.enum(['debug', 'info', 'warn', 'error']),
    metricsEndpoint: z.string().url().optional()
  }),
  
  features: z.object({
    enableNewDashboard: z.boolean(),
    enableAdvancedAnalytics: z.boolean(),
    enableBetaFeatures: z.boolean()
  })
})

export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>

class ConfigManager {
  private config: EnvironmentConfig | null = null
  
  // 加载配置
  async loadConfig(environment: string): Promise<EnvironmentConfig> {
    try {
      // 从多个来源加载配置
      const baseConfig = await this.loadFromFile(`config/base.json`)
      const envConfig = await this.loadFromFile(`config/${environment}.json`)
      const secretsConfig = await this.loadFromSecrets(environment)
      const envVarsConfig = this.loadFromEnvVars()
      
      // 合并配置(优先级:环境变量 > 密钥 > 环境配置 > 基础配置)
      const mergedConfig = {
        ...baseConfig,
        ...envConfig,
        ...secretsConfig,
        ...envVarsConfig
      }
      
      // 验证配置
      this.config = EnvironmentConfigSchema.parse(mergedConfig)
      
      return this.config
    } catch (error) {
      throw new Error(`Failed to load configuration: ${error.message}`)
    }
  }
  
  // 获取配置
  getConfig(): EnvironmentConfig {
    if (!this.config) {
      throw new Error('Configuration not loaded. Call loadConfig() first.')
    }
    return this.config
  }
  
  // 从文件加载配置
  private async loadFromFile(filePath: string): Promise<any> {
    try {
      const { readFile } = await import('fs/promises')
      const content = await readFile(filePath, 'utf-8')
      return JSON.parse(content)
    } catch (error) {
      console.warn(`Could not load config from ${filePath}:`, error.message)
      return {}
    }
  }
  
  // 从密钥管理系统加载配置
  private async loadFromSecrets(environment: string): Promise<any> {
    try {
      // 这里可以集成 AWS Secrets Manager, Azure Key Vault 等
      // 示例使用环境变量模拟
      const secretsPrefix = `SECRETS_${environment.toUpperCase()}_`
      const secrets: any = {}
      
      Object.keys(process.env).forEach(key => {
        if (key.startsWith(secretsPrefix)) {
          const configKey = key.replace(secretsPrefix, '').toLowerCase()
          secrets[configKey] = process.env[key]
        }
      })
      
      return secrets
    } catch (error) {
      console.warn('Could not load secrets:', error.message)
      return {}
    }
  }
  
  // 从环境变量加载配置
  private loadFromEnvVars(): any {
    const envConfig: any = {}
    
    // 映射环境变量到配置结构
    const envMappings = {
      'APP_NAME': 'app.name',
      'APP_PORT': 'app.port',
      'API_BASE_URL': 'api.baseUrl',
      'API_TIMEOUT': 'api.timeout',
      'DB_HOST': 'database.host',
      'DB_PORT': 'database.port',
      'DB_USERNAME': 'database.username',
      'DB_PASSWORD': 'database.password',
      'REDIS_HOST': 'redis.host',
      'REDIS_PORT': 'redis.port',
      'JWT_SECRET': 'security.jwtSecret',
      'LOG_LEVEL': 'monitoring.logLevel',
      'SENTRY_DSN': 'monitoring.sentryDsn'
    }
    
    Object.entries(envMappings).forEach(([envVar, configPath]) => {
      const value = process.env[envVar]
      if (value !== undefined) {
        this.setNestedProperty(envConfig, configPath, this.parseValue(value))
      }
    })
    
    return envConfig
  }
  
  // 设置嵌套属性
  private setNestedProperty(obj: any, path: string, value: any): void {
    const keys = path.split('.')
    let current = obj
    
    for (let i = 0; i < keys.length - 1; i++) {
      const key = keys[i]
      if (!(key in current)) {
        current[key] = {}
      }
      current = current[key]
    }
    
    current[keys[keys.length - 1]] = value
  }
  
  // 解析值类型
  private parseValue(value: string): any {
    // 尝试解析为数字
    if (/^\d+$/.test(value)) {
      return parseInt(value, 10)
    }
    
    // 尝试解析为布尔值
    if (value === 'true') return true
    if (value === 'false') return false
    
    // 尝试解析为JSON
    if (value.startsWith('{') || value.startsWith('[')) {
      try {
        return JSON.parse(value)
      } catch {
        // 如果解析失败,返回原字符串
      }
    }
    
    return value
  }
}

export const configManager = new ConfigManager()

// Vue 插件
export function createConfigPlugin(environment: string) {
  return {
    async install(app: any) {
      const config = await configManager.loadConfig(environment)
      
      app.config.globalProperties.$config = config
      app.provide('config', config)
    }
  }
}

6.2 Docker 多阶段构建

dockerfile
# apps/admin-dashboard/Dockerfile
# 基础镜像
FROM node:18-alpine as base
WORKDIR /app
RUN npm install -g pnpm

# 依赖安装阶段
FROM base as deps
COPY package.json pnpm-lock.yaml ./
COPY ../../packages/ui-components/package.json ./packages/ui-components/
COPY ../../packages/shared-utils/package.json ./packages/shared-utils/
RUN pnpm install --frozen-lockfile --prod

# 开发依赖安装阶段
FROM base as dev-deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# 构建阶段
FROM dev-deps as builder
COPY . .
COPY --from=deps /app/node_modules ./node_modules

# 设置构建参数
ARG BUILD_ENV=production
ARG API_BASE_URL
ARG SENTRY_DSN
ARG VERSION

ENV NODE_ENV=${BUILD_ENV}
ENV VITE_API_BASE_URL=${API_BASE_URL}
ENV VITE_SENTRY_DSN=${SENTRY_DSN}
ENV VITE_VERSION=${VERSION}

RUN pnpm build

# 生产运行时镜像
FROM nginx:alpine as runtime

# 安装运行时依赖
RUN apk add --no-cache \
    curl \
    jq \
    && rm -rf /var/cache/apk/*

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
COPY nginx.vh.default.conf /etc/nginx/conf.d/default.conf

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1

# 暴露端口
EXPOSE 80

# 启动脚本
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

7. 监控与日志体系

7.1 应用监控配置

typescript
// packages/monitoring/src/index.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
import type { App } from 'vue'

interface MonitoringConfig {
  sentryDsn?: string
  environment: string
  release?: string
  enablePerformanceMonitoring: boolean
  enableUserFeedback: boolean
  sampleRate: number
  tracesSampleRate: number
}

class ApplicationMonitor {
  private config: MonitoringConfig
  private performanceObserver?: PerformanceObserver
  
  constructor(config: MonitoringConfig) {
    this.config = config
  }
  
  // 初始化监控
  init(app: App) {
    this.initSentry(app)
    this.initPerformanceMonitoring()
    this.initErrorBoundary(app)
    this.initUserFeedback()
  }
  
  // 初始化 Sentry
  private initSentry(app: App) {
    if (!this.config.sentryDsn) return
    
    Sentry.init({
      app,
      dsn: this.config.sentryDsn,
      environment: this.config.environment,
      release: this.config.release,
      integrations: [
        new BrowserTracing({
          routingInstrumentation: Sentry.vueRouterInstrumentation(router),
          tracingOrigins: ['localhost', /^\//]
        })
      ],
      sampleRate: this.config.sampleRate,
      tracesSampleRate: this.config.tracesSampleRate,
      
      beforeSend(event, hint) {
        // 过滤敏感信息
        if (event.exception) {
          const error = hint.originalException
          if (error?.message?.includes('password') || 
              error?.message?.includes('token')) {
            return null
          }
        }
        
        return event
      }
    })
  }
  
  // 性能监控
  private initPerformanceMonitoring() {
    if (!this.config.enablePerformanceMonitoring) return
    
    // Web Vitals 监控
    this.observeWebVitals()
    
    // 自定义性能指标
    this.observeCustomMetrics()
    
    // 资源加载监控
    this.observeResourceLoading()
  }
  
  private observeWebVitals() {
    // FCP (First Contentful Paint)
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          this.reportMetric('fcp', entry.startTime)
        }
      }
    }).observe({ entryTypes: ['paint'] })
    
    // LCP (Largest Contentful Paint)
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.reportMetric('lcp', lastEntry.startTime)
    }).observe({ entryTypes: ['largest-contentful-paint'] })
    
    // FID (First Input Delay)
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const fid = entry.processingStart - entry.startTime
        this.reportMetric('fid', fid)
      }
    }).observe({ entryTypes: ['first-input'] })
    
    // CLS (Cumulative Layout Shift)
    let clsValue = 0
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
        }
      }
      this.reportMetric('cls', clsValue)
    }).observe({ entryTypes: ['layout-shift'] })
  }
  
  private observeCustomMetrics() {
    // 组件渲染时间
    this.performanceObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name.startsWith('vue-component:')) {
          this.reportMetric('component-render', entry.duration, {
            component: entry.name.replace('vue-component:', '')
          })
        }
      }
    })
    
    this.performanceObserver.observe({ entryTypes: ['measure'] })
  }
  
  private observeResourceLoading() {
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const resource = entry as PerformanceResourceTiming
        
        this.reportMetric('resource-load', resource.duration, {
          name: resource.name,
          type: this.getResourceType(resource.name),
          size: resource.transferSize || 0
        })
      }
    }).observe({ entryTypes: ['resource'] })
  }
  
  // 错误边界
  private initErrorBoundary(app: App) {
    app.config.errorHandler = (error, instance, info) => {
      console.error('Vue Error:', error, info)
      
      // 上报错误
      Sentry.captureException(error, {
        tags: {
          component: instance?.$options.name || 'Unknown',
          errorInfo: info
        }
      })
    }
    
    // 全局未捕获错误
    window.addEventListener('error', (event) => {
      this.reportError(event.error, {
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      })
    })
    
    // Promise 拒绝
    window.addEventListener('unhandledrejection', (event) => {
      this.reportError(event.reason, {
        type: 'unhandled-promise-rejection'
      })
    })
  }
  
  // 用户反馈
  private initUserFeedback() {
    if (!this.config.enableUserFeedback) return
    
    // 添加用户反馈按钮
    const feedbackButton = document.createElement('button')
    feedbackButton.innerHTML = '反馈'
    feedbackButton.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 9999;
      padding: 10px 15px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    `
    
    feedbackButton.addEventListener('click', () => {
      Sentry.showReportDialog()
    })
    
    document.body.appendChild(feedbackButton)
  }
  
  // 上报指标
  private reportMetric(name: string, value: number, tags: Record<string, any> = {}) {
    // 发送到监控系统
    if (window.gtag) {
      window.gtag('event', name, {
        value: Math.round(value),
        ...tags
      })
    }
    
    // 发送到自定义端点
    this.sendToMetricsEndpoint({
      name,
      value,
      tags,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    })
  }
  
  // 上报错误
  private reportError(error: any, context: Record<string, any> = {}) {
    Sentry.captureException(error, {
      tags: context
    })
  }
  
  private getResourceType(url: string): string {
    if (url.includes('.js')) return 'script'
    if (url.includes('.css')) return 'stylesheet'
    if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image'
    if (url.includes('.woff')) return 'font'
    return 'other'
  }
  
  private async sendToMetricsEndpoint(data: any) {
    try {
      await fetch('/api/metrics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      })
    } catch (error) {
      console.warn('Failed to send metrics:', error)
    }
  }
}

// Vue 插件
export function createMonitoringPlugin(config: MonitoringConfig) {
  return {
    install(app: App) {
      const monitor = new ApplicationMonitor(config)
      monitor.init(app)
      
      app.config.globalProperties.$monitor = monitor
      app.provide('monitor', monitor)
    }
  }
}

总结

通过本文的企业级工程化实践,我们构建了一套完整的 Vue 3 开发体系:

🏗️ 核心架构

  • Monorepo 管理 - 统一代码组织和依赖管理
  • 组件库体系 - 设计系统驱动的可复用组件
  • 配置管理 - 多环境配置和密钥管理
  • 工程化工具 - 代码质量保证和自动化流程

🔧 开发体验

  • 类型安全 - 完整的 TypeScript 支持
  • 代码规范 - ESLint + Prettier + Commitlint
  • 测试策略 - 单元测试 + 集成测试 + E2E测试
  • 热重载 - 快速开发反馈循环

🚀 部署运维

  • 容器化 - Docker 多阶段构建优化
  • CI/CD - 自动化构建、测试、部署流水线
  • 监控告警 - 完整的可观测性体系
  • 安全扫描 - 依赖审计和漏洞检测

💡 最佳实践

  • 渐进式架构 - 支持逐步迁移和扩展
  • 团队协作 - 标准化的开发流程和规范
  • 性能优化 - 构建优化和运行时监控
  • 可维护性 - 清晰的代码组织和文档体系

这套企业级工程化解决方案不仅解决了大型项目的技术挑战,更为团队协作和项目长期维护奠定了坚实基础。通过标准化的工具链和流程,能够显著提升开发效率和代码质量,为企业数字化转型提供强有力的技术支撑。

相关文章