🚀 最佳实践
本指南汇总了使用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)
})
}
}📚 总结
遵循这些最佳实践可以帮助您:
- 🏗️ 构建可维护的代码架构
- ⚡ 提升应用性能和用户体验
- 🛡️ 增强应用的稳定性和可靠性
- 🧪 建立完善的测试体系
- 🚀 优化部署和运维流程
记住,最佳实践不是一成不变的规则,而是经过验证的指导原则。根据项目的具体需求和团队情况,灵活应用这些实践,持续改进和优化您的开发流程。
如需更多帮助,请查看: