Skip to content

VJSP Vue3 Frame - 开发规范和最佳实践

概述

本文档为VJSP Vue3 Frame项目提供完整的开发规范和最佳实践指南。基于项目实际代码和配置,涵盖项目架构、开发流程、代码规范、性能优化等核心内容,帮助团队保持代码质量和开发效率。

项目架构设计

技术栈架构

VJSP Vue3 Frame采用现代化的前端技术栈:

  • 核心框架: Vue 3 + TypeScript + Vite
  • UI组件库: Element Plus
  • 状态管理: Pinia
  • 路由管理: Vue Router 4
  • 构建工具: Vite
  • 代码规范: ESLint + Prettier
  • 样式方案: Less + Tailwind CSS

目录结构设计

项目采用分层架构设计,确保代码的可维护性和可扩展性:

shell
src/
├── api/           # API接口层
├── assets/        # 静态资源
├── components/    # 公共组件
├── constants/     # 常量定义
├── directives/    # 自定义指令
├── hooks/         # 组合式函数
├── layout/        # 布局组件
├── locales/       # 国际化资源
├── plugins/       # 插件配置
├── router/        # 路由配置
├── stores/        # 状态管理
├── styles/        # 样式文件
├── types/         # 类型定义
├── utils/         # 工具函数
└── views/         # 页面组件

模块化设计原则

  1. 单一职责原则: 每个模块/组件只负责特定功能
  2. 依赖倒置原则: 高层模块不依赖低层模块,都依赖抽象
  3. 开闭原则: 对扩展开放,对修改关闭
  4. 接口隔离原则: 使用多个特定接口比使用单一通用接口更好

开发流程规范

代码提交规范

项目采用约定式提交规范(Conventional Commits):

bash
# 提交格式
<type>[optional scope]: <description>

# 示例
feat(user): 新增用户管理功能
fix(api): 修复登录接口异常
refactor(utils): 重构缓存工具函数
docs(readme): 更新项目文档

提交类型说明:

  • feat: 新功能
  • fix: 修复bug
  • docs: 文档更新
  • style: 代码格式调整
  • refactor: 代码重构
  • test: 测试相关
  • chore: 构建过程或辅助工具变动

分支管理策略

项目采用Git Flow分支管理模型:

shell
main          # 主分支,生产环境代码
├── develop   # 开发分支,集成功能
├── feature/  # 功能分支,新功能开发
├── release/  # 发布分支,版本发布准备
└── hotfix/   # 热修复分支,紧急bug修复

代码审查流程

  1. 功能开发: 在feature分支进行开发
  2. 本地测试: 完成功能后本地测试验证
  3. 提交PR: 创建Pull Request到develop分支
  4. 代码审查: 团队成员进行代码审查
  5. CI/CD: 自动构建和测试
  6. 合并发布: 审查通过后合并发布

代码规范指南

TypeScript规范

类型定义规范

typescript
// 接口定义使用PascalCase
interface UserInfo {
  id: number
  name: string
  email: string
}

// 类型别名使用PascalCase
type ApiResponse<T> = {
  code: number
  data: T
  message: string
}

// 枚举使用PascalCase
enum UserStatus {
  Active = 1,
  Inactive = 0,
}

组件Props规范

typescript
// 使用TypeScript接口定义Props
interface ProductCardProps {
  product: Product
  showActions?: boolean
  onEdit?: (product: Product) => void
  onDelete?: (productId: number) => void
}

// 使用withDefaults提供默认值
const props = withDefaults(defineProps<ProductCardProps>(), {
  showActions: true,
})

Vue 3组合式API规范

组件结构规范

vue
<template>
  <!-- 模板部分 -->
</template>

<script setup lang="ts">
// 导入部分
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'

// 类型定义
interface Product {
  id: string
  name: string
  price: number
}

// 响应式数据
const productList = ref<Product[]>([])
const loading = ref(false)

// 计算属性
const totalPrice = computed(() => {
  return productList.value.reduce((sum, product) => sum + product.price, 0)
})

// 方法定义
const fetchProducts = async () => {
  // 业务逻辑
}

// 生命周期
onMounted(() => {
  fetchProducts()
})

// 暴露给模板
defineExpose({
  productList,
  fetchProducts,
})
</script>

组合式函数规范

typescript
// src/hooks/useProduct.ts
import { ref, computed } from 'vue'
import type { Product } from '@/types/api'

export function useProduct() {
  const products = ref<Product[]>([])
  const loading = ref(false)

  const productCount = computed(() => products.value.length)

  const fetchProducts = async () => {
    loading.value = true
    try {
      // API调用逻辑
    } finally {
      loading.value = false
    }
  }

  return {
    products,
    loading,
    productCount,
    fetchProducts,
  }
}

样式编写规范

CSS命名规范

less
// 使用BEM命名规范
.product-card {
  &__header {
    // 头部样式
  }

  &__body {
    // 主体样式

    &--loading {
      // 加载状态样式
    }
  }

  &__footer {
    // 底部样式
  }
}

// 使用CSS Modules避免样式冲突
.productList {
  :global(.el-table) {
    // 全局样式覆盖
  }
}

主题变量规范

less
// styles/variables.module.less
:root {
  // 颜色变量
  --primary-color: #1890ff;
  --success-color: #52c41a;
  --warning-color: #faad14;
  --error-color: #f5222d;

  // 尺寸变量
  --border-radius-base: 4px;
  --font-size-base: 14px;

  // 间距变量
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
}

API开发规范

接口定义规范

typescript
// src/api/product/types.ts
export interface Product {
  id: number
  name: string
  price: number
  status: number
  createTime: string
}

export interface ProductListParams {
  page?: number
  size?: number
  name?: string
  status?: number
}

export interface CreateProductParams {
  name: string
  price: number
  description?: string
}

API服务封装规范

typescript
// src/api/product/index.ts
import type { Product, ProductListParams, CreateProductParams } from './types'

export class ProductApi {
  private static BASE_URL = '/api/product'

  // 获取产品列表
  static async getList(params?: ProductListParams) {
    return await axios.get<ApiResponse<Product[]>>(this.BASE_URL + '/list', { params })
  }

  // 创建产品
  static async create(data: CreateProductParams) {
    return await axios.post<ApiResponse<number>>(this.BASE_URL + '/create', data)
  }

  // 更新产品
  static async update(id: number, data: Partial<CreateProductParams>) {
    return await axios.put<ApiResponse<void>>(`${this.BASE_URL}/update/${id}`, data)
  }

  // 删除产品
  static async delete(id: number) {
    return await axios.delete<ApiResponse<void>>(`${this.BASE_URL}/delete/${id}`)
  }
}

错误处理规范

typescript
// src/utils/errorHandler.ts
export class ApiError extends Error {
  constructor(
    public code: number,
    public message: string,
    public data?: any
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

export function handleApiError(error: any) {
  if (error.response) {
    // 服务器响应错误
    const { status, data } = error.response
    throw new ApiError(status, data?.message || '请求失败', data)
  } else if (error.request) {
    // 网络错误
    throw new ApiError(-1, '网络连接失败')
  } else {
    // 其他错误
    throw new ApiError(-2, error.message)
  }
}

状态管理规范

Store定义规范

typescript
// src/stores/modules/product.ts
import { defineStore } from 'pinia'
import type { Product } from '@/api/product/types'

export const useProductStore = defineStore('product', () => {
  // 状态定义
  const productList = ref<Product[]>([])
  const currentProduct = ref<Product | null>(null)
  const loading = ref(false)

  // Getter
  const productCount = computed(() => productList.value.length)
  const activeProducts = computed(() => productList.value.filter(product => product.status === 1))

  // Actions
  const fetchProducts = async (params?: any) => {
    loading.value = true
    try {
      const response = await ProductApi.getList(params)
      productList.value = response.data.data
    } finally {
      loading.value = false
    }
  }

  const createProduct = async (data: any) => {
    const response = await ProductApi.create(data)
    await fetchProducts() // 重新获取列表
    return response.data.data
  }

  return {
    // State
    productList,
    currentProduct,
    loading,

    // Getters
    productCount,
    activeProducts,

    // Actions
    fetchProducts,
    createProduct,
  }
})

Store使用规范

vue
<template>
  <div>
    <el-button @click="loadProducts">加载产品</el-button>
    <div v-loading="productStore.loading">
      <p>产品数量: {{ productStore.productCount }}</p>
      <el-table :data="productStore.productList">
        <!-- 表格内容 -->
      </el-table>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useProductStore } from '@/stores/modules/product'

const productStore = useProductStore()

const loadProducts = async () => {
  await productStore.fetchProducts()
}

// 在组件卸载时重置store状态
onUnmounted(() => {
  productStore.$reset()
})
</script>

路由和权限规范

路由配置规范

typescript
// src/router/modules/product.ts
import { Layout } from '@/utils/routerHelper'
import type { AppRouteRecordRaw } from '@/types/router'

const ProductRoute: AppRouteRecordRaw = {
  path: '/product',
  component: Layout,
  redirect: '/product/list',
  name: 'Product',
  meta: {
    title: '产品管理',
    icon: 'shopping',
    permission: 'product:view',
    keepAliveName: 'ProductLayout',
    alwaysShow: true,
  },
  children: [
    {
      path: 'list',
      component: () => import('@/views/product/ProductList.vue'),
      name: 'ProductList',
      meta: {
        title: '产品列表',
        icon: 'list',
        permission: 'product:list',
        keepAliveName: 'ProductList',
        noCache: false,
      },
    },
    {
      path: 'create',
      component: () => import('@/views/product/ProductCreate.vue'),
      name: 'ProductCreate',
      meta: {
        title: '创建产品',
        icon: 'plus',
        permission: 'product:create',
        noCache: true,
      },
    },
  ],
}

export default ProductRoute

权限控制规范

typescript
// 使用权限指令
<template>
  <div>
    <el-button v-permission="'product:create'">创建产品</el-button>
    <el-button v-permission="'product:edit'">编辑产品</el-button>
    <el-button v-permission="'product:delete'">删除产品</el-button>
  </div>
</template>

// 使用权限函数
<script setup lang="ts">
import { hasPermission } from '@/utils/permission'

const canCreateProduct = hasPermission('product:create')
const canEditProduct = hasPermission('product:edit')

const handleCreate = () => {
  if (!canCreateProduct) {
    ElMessage.warning('没有创建产品的权限')
    return
  }
  // 创建逻辑
}
</script>

Product模块开发示例

1. 产品列表页面开发

vue
<!-- src/views/product/ProductList.vue -->
<template>
  <div class="product-list">
    <!-- 搜索区域 -->
    <div class="search-area">
      <el-form :model="searchForm" inline>
        <el-form-item label="产品名称">
          <el-input v-model="searchForm.name" placeholder="请输入产品名称" />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="searchForm.status" placeholder="请选择状态">
            <el-option label="全部" value="" />
            <el-option label="启用" :value="1" />
            <el-option label="禁用" :value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <!-- 操作区域 -->
    <div class="action-area">
      <el-button type="primary" icon="plus" v-permission="'product:create'" @click="handleCreate">
        创建产品
      </el-button>
      <el-button icon="refresh" @click="refreshList">刷新</el-button>
    </div>

    <!-- 表格区域 -->
    <div class="table-area">
      <el-table v-loading="loading" :data="tableData" border @sort-change="handleSortChange">
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="name" label="产品名称" min-width="120" />
        <el-table-column prop="price" label="价格" width="100" align="center">
          <template #default="{ row }"> ¥{{ row.price }} </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="80" align="center">
          <template #default="{ row }">
            <el-tag :type="row.status ? 'success' : 'danger'">
              {{ row.status ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="160" />
        <el-table-column label="操作" width="200" align="center" fixed="right">
          <template #default="{ row }">
            <el-button
              link
              type="primary"
              size="small"
              v-permission="'product:edit'"
              @click="handleEdit(row)"
            >
              编辑
            </el-button>
            <el-button
              link
              type="danger"
              size="small"
              v-permission="'product:delete'"
              @click="handleDelete(row)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <div class="pagination-area">
        <el-pagination
          v-model:current-page="pagination.current"
          v-model:page-size="pagination.size"
          :total="pagination.total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ProductApi } from '@/api/product'
import type { Product, ProductListParams } from '@/api/product/types'

const router = useRouter()

// 响应式数据
const loading = ref(false)
const tableData = ref<Product[]>([])

// 搜索表单
const searchForm = reactive({
  name: '',
  status: '',
})

// 分页配置
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0,
})

// 获取产品列表
const fetchProductList = async () => {
  loading.value = true
  try {
    const params: ProductListParams = {
      page: pagination.current,
      size: pagination.size,
      ...searchForm,
    }

    const response = await ProductApi.getList(params)
    tableData.value = response.data.data
    pagination.total = response.data.total || 0
  } catch (error) {
    ElMessage.error('获取产品列表失败')
  } finally {
    loading.value = false
  }
}

// 搜索处理
const handleSearch = () => {
  pagination.current = 1
  fetchProductList()
}

// 重置搜索
const handleReset = () => {
  Object.assign(searchForm, { name: '', status: '' })
  handleSearch()
}

// 分页处理
const handleSizeChange = (size: number) => {
  pagination.size = size
  pagination.current = 1
  fetchProductList()
}

const handleCurrentChange = (current: number) => {
  pagination.current = current
  fetchProductList()
}

// 排序处理
const handleSortChange = (sort: any) => {
  console.log('排序变化:', sort)
  // 处理排序逻辑
}

// 操作处理
const handleCreate = () => {
  router.push('/product/create')
}

const handleEdit = (row: Product) => {
  router.push(`/product/edit/${row.id}`)
}

const handleDelete = async (row: Product) => {
  try {
    await ElMessageBox.confirm(`确定要删除产品"${row.name}"吗?`, '删除确认', {
      type: 'warning',
      confirmButtonText: '确定',
      cancelButtonText: '取消',
    })

    await ProductApi.delete(row.id)
    ElMessage.success('删除成功')
    fetchProductList()
  } catch (error) {
    // 用户取消删除或删除失败
  }
}

// 刷新列表
const refreshList = () => {
  fetchProductList()
}

// 初始化
onMounted(() => {
  fetchProductList()
})
</script>

<style scoped lang="less">
.product-list {
  padding: 20px;

  .search-area {
    background: #f5f7fa;
    padding: 20px;
    margin-bottom: 20px;
    border-radius: 4px;
  }

  .action-area {
    margin-bottom: 20px;
  }

  .table-area {
    .pagination-area {
      margin-top: 20px;
      text-align: right;
    }
  }
}
</style>

2. 产品创建页面开发

vue
<!-- src/views/product/ProductCreate.vue -->
<template>
  <div class="product-create">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>创建产品</span>
          <el-button @click="router.back()">返回</el-button>
        </div>
      </template>

      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
        <el-form-item label="产品名称" prop="name">
          <el-input v-model="formData.name" placeholder="请输入产品名称" />
        </el-form-item>

        <el-form-item label="价格" prop="price">
          <el-input-number
            v-model="formData.price"
            :min="0"
            :precision="2"
            placeholder="请输入价格"
          />
        </el-form-item>

        <el-form-item label="描述" prop="description">
          <el-input
            v-model="formData.description"
            type="textarea"
            :rows="4"
            placeholder="请输入产品描述"
          />
        </el-form-item>

        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="formData.status">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="handleSubmit" :loading="loading"> 创建 </el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ProductApi } from '@/api/product'

const router = useRouter()
const formRef = ref<FormInstance>()
const loading = ref(false)

// 表单数据
const formData = reactive({
  name: '',
  price: 0,
  description: '',
  status: 1,
})

// 表单验证规则
const formRules: FormRules = {
  name: [
    { required: true, message: '请输入产品名称', trigger: 'blur' },
    { min: 2, max: 50, message: '产品名称长度在 2 到 50 个字符', trigger: 'blur' },
  ],
  price: [
    { required: true, message: '请输入价格', trigger: 'blur' },
    { type: 'number', min: 0, message: '价格必须大于等于 0', trigger: 'blur' },
  ],
  status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}

// 提交表单
const handleSubmit = async () => {
  if (!formRef.value) return

  try {
    const valid = await formRef.value.validate()
    if (!valid) return

    loading.value = true
    await ProductApi.create(formData)
    ElMessage.success('创建成功')
    router.push('/product/list')
  } catch (error) {
    ElMessage.error('创建失败')
  } finally {
    loading.value = false
  }
}

// 重置表单
const handleReset = () => {
  formRef.value?.resetFields()
}
</script>

<style scoped lang="less">
.product-create {
  padding: 20px;

  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .el-form {
    max-width: 600px;
  }
}
</style>

性能优化最佳实践

1. 组件懒加载

typescript
// 路由配置中使用懒加载
const ProductList = () => import('@/views/product/ProductList.vue')
const ProductCreate = () => import('@/views/product/ProductCreate.vue')

// 或者使用defineAsyncComponent
const AsyncComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'))

2. 图片懒加载

vue
<template>
  <img v-lazy="product.image" :alt="product.name" />
</template>

<script setup lang="ts">
import { useLazyLoad } from '@/directives/lazy-load'

const vLazy = useLazyLoad()
</script>

3. 数据缓存策略

typescript
// 使用缓存工具减少API调用
import { CacheUtil } from '@/utils/cache'

const CACHE_KEY = 'product_list'
const CACHE_TTL = 5 * 60 * 1000 // 5分钟

const fetchProducts = async () => {
  // 先检查缓存
  const cached = CacheUtil.getLocal(CACHE_KEY)
  if (cached) {
    return cached
  }

  // 调用API并缓存结果
  const data = await ProductApi.getList()
  CacheUtil.setLocal(CACHE_KEY, data, CACHE_TTL)
  return data
}

测试规范

单元测试规范

typescript
// tests/unit/product.spec.ts
import { describe, it, expect } from 'vitest'
import { useProduct } from '@/hooks/useProduct'

describe('useProduct', () => {
  it('should initialize with empty products', () => {
    const { products } = useProduct()
    expect(products.value).toEqual([])
  })

  it('should calculate product count correctly', () => {
    const { products, productCount } = useProduct()
    products.value = [{ id: 1, name: 'Test', price: 100 }]
    expect(productCount.value).toBe(1)
  })
})

E2E测试规范

typescript
// tests/e2e/product.spec.ts
describe('Product Management', () => {
  it('should create a new product', () => {
    cy.visit('/product/list')
    cy.get('[data-testid="create-product-btn"]').click()
    cy.get('[data-testid="product-name-input"]').type('Test Product')
    cy.get('[data-testid="product-price-input"]').type('100')
    cy.get('[data-testid="submit-btn"]').click()
    cy.contains('创建成功').should('be.visible')
  })
})

部署和运维规范

环境配置规范

bash
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境
VITE_BUILD_SOURCEMAP=false

# .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=开发环境
VITE_BUILD_SOURCEMAP=true

构建优化配置

typescript
// vite.config.ts
export default defineConfig({
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['lodash-es', 'dayjs'],
        },
      },
    },
    // 资源优化
    assetsInlineLimit: 4096,
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
})

总结

本文档提供了VJSP Vue3 Frame项目的完整开发规范和最佳实践。通过遵循这些规范,团队可以:

  1. 保持代码一致性:统一的代码风格和架构设计
  2. 提高开发效率:清晰的开发流程和工具链
  3. 确保代码质量:严格的代码审查和测试规范
  4. 优化性能表现:合理的性能优化策略
  5. 便于维护扩展:模块化的设计和清晰的文档

建议团队成员在开发过程中参考本文档,并根据实际项目需求进行适当调整和完善。