Skip to content

🚀 最佳实践

本指南汇总了使用ERP组件库开发高质量应用的最佳实践,帮助您构建可维护、高性能的ERP系统。

📋 目录

🏗️ 项目结构

推荐的目录结构

src/
├── components/          # 业务组件
│   ├── common/         # 通用业务组件
│   ├── layout/         # 布局组件
│   └── business/       # 具体业务组件
├── views/              # 页面组件
│   ├── dashboard/      # 仪表盘
│   ├── user/          # 用户管理
│   └── order/         # 订单管理
├── composables/        # 组合式函数
│   ├── useApi.ts      # API相关
│   ├── useAuth.ts     # 认证相关
│   └── useTable.ts    # 表格相关
├── stores/             # 状态管理
│   ├── user.ts        # 用户状态
│   ├── app.ts         # 应用状态
│   └── modules/       # 业务模块状态
├── utils/              # 工具函数
│   ├── request.ts     # 请求封装
│   ├── format.ts      # 格式化工具
│   └── validate.ts    # 验证工具
├── types/              # 类型定义
│   ├── api.ts         # API类型
│   ├── common.ts      # 通用类型
│   └── business.ts    # 业务类型
└── assets/             # 静态资源
    ├── images/        # 图片
    ├── icons/         # 图标
    └── styles/        # 样式

组件组织原则

typescript
// ✅ 推荐:按功能模块组织
components/
├── UserManagement/
│   ├── UserList.vue
│   ├── UserForm.vue
│   ├── UserDetail.vue
│   └── index.ts
└── OrderManagement/
    ├── OrderList.vue
    ├── OrderForm.vue
    └── index.ts

// ❌ 不推荐:按组件类型组织
components/
├── forms/
├── tables/
├── modals/
└── buttons/

🧩 组件使用

1. 组件导入策略

typescript
// ✅ 推荐:按需导入
import { FkInput, FkButton, FkTable } from '@erp/common'

// ✅ 推荐:使用自动导入
// vite.config.ts
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Components({
      resolvers: [
        (componentName) => {
          if (componentName.startsWith('Fk')) {
            return { name: componentName, from: '@erp/common' }
          }
        }
      ]
    })
  ]
})

// ❌ 不推荐:全量导入
import * as FkComponents from '@erp/common'

2. 配置化开发

vue
<template>
  <!-- ✅ 推荐:使用配置化组件 -->
  <FkForm 
    v-model="formData"
    :config="formConfig"
    @submit="handleSubmit"
  />
  
  <!-- ❌ 不推荐:手动构建表单 -->
  <form @submit="handleSubmit">
    <FkInput v-model="formData.name" label="姓名" />
    <FkInput v-model="formData.email" label="邮箱" />
    <FkSelect v-model="formData.role" label="角色" :options="roleOptions" />
  </form>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FkFormConfig } from '@erp/common'

// 配置化表单
const formConfig = computed<FkFormConfig>(() => ({
  labelWidth: '100px',
  items: [
    {
      type: 'input',
      prop: 'name',
      label: '姓名',
      rules: [{ required: true, message: '请输入姓名' }]
    },
    {
      type: 'input',
      prop: 'email',
      label: '邮箱',
      rules: [
        { required: true, message: '请输入邮箱' },
        { type: 'email', message: '邮箱格式不正确' }
      ]
    },
    {
      type: 'select',
      prop: 'role',
      label: '角色',
      options: roleOptions.value
    }
  ]
}))
</script>

3. 组件封装原则

vue
<!-- ✅ 推荐:单一职责的组件 -->
<template>
  <div class="user-search-form">
    <FkSearchForm 
      v-model="searchData"
      :config="searchConfig"
      @search="handleSearch"
      @reset="handleReset"
    />
  </div>
</template>

<script setup lang="ts">
interface Props {
  loading?: boolean
}

interface Emits {
  search: [data: UserSearchData]
  reset: []
}

const props = withDefaults(defineProps<Props>(), {
  loading: false
})

const emit = defineEmits<Emits>()

// 搜索配置
const searchConfig = {
  items: [
    { type: 'input', prop: 'name', label: '用户名' },
    { type: 'select', prop: 'status', label: '状态', options: statusOptions }
  ]
}

const handleSearch = (data: UserSearchData) => {
  emit('search', data)
}

const handleReset = () => {
  emit('reset')
}
</script>

⚡ 性能优化

1. 虚拟滚动

vue
<template>
  <!-- ✅ 大数据量使用虚拟滚动 -->
  <FkTable 
    :columns="columns"
    :data="tableData"
    :virtual="tableData.length > 1000"
    :item-height="50"
  />
</template>

<script setup lang="ts">
// 大数据量处理
const tableData = ref<UserData[]>([])
const loading = ref(false)

// 分页加载
const pagination = reactive({
  current: 1,
  pageSize: 50,
  total: 0
})

const loadData = async () => {
  loading.value = true
  try {
    const { data, total } = await getUserList({
      page: pagination.current,
      size: pagination.pageSize
    })
    tableData.value = data
    pagination.total = total
  } finally {
    loading.value = false
  }
}
</script>

2. 懒加载和代码分割

typescript
// ✅ 推荐:路由懒加载
const routes = [
  {
    path: '/user',
    component: () => import('@/views/user/UserList.vue')
  },
  {
    path: '/order',
    component: () => import('@/views/order/OrderList.vue')
  }
]

// ✅ 推荐:组件懒加载
const UserDetail = defineAsyncComponent(() => import('./UserDetail.vue'))

// ✅ 推荐:条件加载
const HeavyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

3. 缓存策略

typescript
// ✅ 推荐:使用缓存
import { useMemoize } from '@vueuse/core'

// 计算属性缓存
const expensiveComputed = computed(() => {
  return heavyCalculation(props.data)
})

// 函数结果缓存
const memoizedFunction = useMemoize((input: string) => {
  return expensiveOperation(input)
})

// API缓存
const { data, loading, error } = useApi('/api/users', {
  cache: true,
  cacheTime: 5 * 60 * 1000 // 5分钟缓存
})

📝 代码规范

1. TypeScript最佳实践

typescript
// ✅ 推荐:严格的类型定义
interface UserData {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
  status: 'active' | 'inactive'
  createdAt: string
  updatedAt: string
}

// ✅ 推荐:泛型约束
interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
}

// ✅ 推荐:工具类型
type UserFormData = Pick<UserData, 'name' | 'email' | 'role'>
type UserUpdateData = Partial<UserFormData>

// ❌ 不推荐:使用any
const userData: any = {}

2. 组合式API规范

typescript
// ✅ 推荐:逻辑分离
export function useUserManagement() {
  const users = ref<UserData[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const loadUsers = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await getUserList()
      users.value = response.data
    } catch (err) {
      error.value = err instanceof Error ? err.message : '加载失败'
    } finally {
      loading.value = false
    }
  }
  
  const deleteUser = async (id: number) => {
    try {
      await deleteUserById(id)
      users.value = users.value.filter(user => user.id !== id)
    } catch (err) {
      error.value = '删除失败'
    }
  }
  
  return {
    users: readonly(users),
    loading: readonly(loading),
    error: readonly(error),
    loadUsers,
    deleteUser
  }
}

3. 命名规范

typescript
// ✅ 推荐:清晰的命名
const isUserLoading = ref(false)
const userList = ref<UserData[]>([])
const handleUserSubmit = () => {}
const validateUserForm = () => {}

// ❌ 不推荐:模糊的命名
const flag = ref(false)
const data = ref([])
const handle = () => {}
const check = () => {}

🗃️ 状态管理

1. Pinia最佳实践

typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // 状态
  const currentUser = ref<UserData | null>(null)
  const permissions = ref<string[]>([])
  
  // 计算属性
  const isAdmin = computed(() => currentUser.value?.role === 'admin')
  const hasPermission = computed(() => (permission: string) => {
    return permissions.value.includes(permission)
  })
  
  // 动作
  const login = async (credentials: LoginData) => {
    try {
      const response = await loginApi(credentials)
      currentUser.value = response.data.user
      permissions.value = response.data.permissions
      return response
    } catch (error) {
      throw error
    }
  }
  
  const logout = () => {
    currentUser.value = null
    permissions.value = []
    // 清除本地存储
    localStorage.removeItem('token')
  }
  
  return {
    // 状态
    currentUser: readonly(currentUser),
    permissions: readonly(permissions),
    // 计算属性
    isAdmin,
    hasPermission,
    // 动作
    login,
    logout
  }
})

2. 状态持久化

typescript
// ✅ 推荐:选择性持久化
export const useAppStore = defineStore('app', () => {
  const theme = ref<'light' | 'dark'>('light')
  const language = ref<'zh' | 'en'>('zh')
  const sidebarCollapsed = ref(false)
  
  return {
    theme,
    language,
    sidebarCollapsed
  }
}, {
  persist: {
    key: 'app-settings',
    storage: localStorage,
    paths: ['theme', 'language'] // 只持久化部分状态
  }
})

🛡️ 错误处理

1. 全局错误处理

typescript
// utils/errorHandler.ts
export class ErrorHandler {
  static handle(error: unknown, context?: string) {
    console.error(`[${context}] Error:`, error)
    
    if (error instanceof ApiError) {
      this.handleApiError(error)
    } else if (error instanceof ValidationError) {
      this.handleValidationError(error)
    } else {
      this.handleUnknownError(error)
    }
  }
  
  private static handleApiError(error: ApiError) {
    switch (error.code) {
      case 401:
        // 跳转到登录页
        router.push('/login')
        break
      case 403:
        ElMessage.error('权限不足')
        break
      case 500:
        ElMessage.error('服务器错误,请稍后重试')
        break
      default:
        ElMessage.error(error.message || '请求失败')
    }
  }
}

// 在组件中使用
const handleSubmit = async () => {
  try {
    await submitForm()
  } catch (error) {
    ErrorHandler.handle(error, 'UserForm')
  }
}

2. 组件错误边界

vue
<!-- ErrorBoundary.vue -->
<template>
  <div v-if="error" class="error-boundary">
    <h3>出现了错误</h3>
    <p>{{ error.message }}</p>
    <FkButton @click="retry">重试</FkButton>
  </div>
  <slot v-else />
</template>

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  error.value = err
  return false // 阻止错误继续传播
})

const retry = () => {
  error.value = null
}
</script>

🧪 测试策略

1. 单元测试

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

describe('UserForm', () => {
  it('should validate required fields', async () => {
    const wrapper = mount(UserForm)
    
    // 提交空表单
    await wrapper.find('form').trigger('submit')
    
    // 检查验证错误
    expect(wrapper.find('.error-message').text()).toBe('请输入用户名')
  })
  
  it('should emit submit event with form data', async () => {
    const wrapper = mount(UserForm)
    
    // 填写表单
    await wrapper.find('input[name="name"]').setValue('张三')
    await wrapper.find('input[name="email"]').setValue('zhangsan@example.com')
    
    // 提交表单
    await wrapper.find('form').trigger('submit')
    
    // 检查事件
    expect(wrapper.emitted('submit')).toBeTruthy()
    expect(wrapper.emitted('submit')[0]).toEqual([{
      name: '张三',
      email: 'zhangsan@example.com'
    }])
  })
})

2. 集成测试

typescript
// UserManagement.test.ts
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import UserManagement from '@/views/UserManagement.vue'

describe('UserManagement', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('should load and display users', async () => {
    // Mock API
    vi.mocked(getUserList).mockResolvedValue({
      data: [
        { id: 1, name: '张三', email: 'zhangsan@example.com' }
      ]
    })
    
    const wrapper = mount(UserManagement)
    
    // 等待数据加载
    await wrapper.vm.$nextTick()
    
    // 检查用户列表
    expect(wrapper.find('.user-item').exists()).toBe(true)
    expect(wrapper.find('.user-name').text()).toBe('张三')
  })
})

🚀 部署优化

1. 构建优化

typescript
// vite.config.ts
export default defineConfig({
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          erp: ['@erp/common', '@erp/biz'],
          utils: ['lodash-es', 'dayjs']
        }
      }
    },
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  // 预加载优化
  optimizeDeps: {
    include: ['@erp/common', '@erp/biz', '@erp/icons']
  }
})

2. 缓存策略

nginx
# nginx.conf
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location /api/ {
  add_header Cache-Control "no-cache, no-store, must-revalidate";
}

3. CDN配置

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue', 'vue-router'],
      output: {
        globals: {
          vue: 'Vue',
          'vue-router': 'VueRouter'
        }
      }
    }
  }
})

📊 性能监控

1. 性能指标收集

typescript
// utils/performance.ts
export class PerformanceMonitor {
  static measurePageLoad() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
      const metrics = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        request: navigation.responseStart - navigation.requestStart,
        response: navigation.responseEnd - navigation.responseStart,
        dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,
        load: navigation.loadEventEnd - navigation.loadEventStart
      }
      
      // 发送到监控服务
      this.sendMetrics('page-load', metrics)
    })
  }
  
  static measureComponentRender(componentName: string) {
    const start = performance.now()
    return () => {
      const end = performance.now()
      this.sendMetrics('component-render', {
        component: componentName,
        duration: end - start
      })
    }
  }
}

2. 错误监控

typescript
// utils/errorMonitor.ts
export class ErrorMonitor {
  static init() {
    // 捕获JavaScript错误
    window.addEventListener('error', (event) => {
      this.reportError({
        type: 'javascript',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack
      })
    })
    
    // 捕获Promise错误
    window.addEventListener('unhandledrejection', (event) => {
      this.reportError({
        type: 'promise',
        message: event.reason?.message || 'Unhandled Promise Rejection',
        stack: event.reason?.stack
      })
    })
    
    // 捕获Vue错误
    app.config.errorHandler = (err, instance, info) => {
      this.reportError({
        type: 'vue',
        message: err.message,
        stack: err.stack,
        componentName: instance?.$options.name,
        info
      })
    }
  }
  
  private static reportError(error: ErrorInfo) {
    // 发送到错误监控服务
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(error)
    })
  }
}

📚 总结

遵循这些最佳实践可以帮助您:

  • 🏗️ 构建可维护的代码架构
  • 提升应用性能和用户体验
  • 🛡️ 增强应用的稳定性和可靠性
  • 🧪 建立完善的测试体系
  • 🚀 优化部署和运维流程

记住,最佳实践不是一成不变的规则,而是经过验证的指导原则。根据项目的具体需求和团队情况,灵活应用这些实践,持续改进和优化您的开发流程。


如需更多帮助,请查看:

基于 MIT 许可发布