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/ # 页面组件模块化设计原则
- 单一职责原则: 每个模块/组件只负责特定功能
- 依赖倒置原则: 高层模块不依赖低层模块,都依赖抽象
- 开闭原则: 对扩展开放,对修改关闭
- 接口隔离原则: 使用多个特定接口比使用单一通用接口更好
开发流程规范
代码提交规范
项目采用约定式提交规范(Conventional Commits):
bash
# 提交格式
<type>[optional scope]: <description>
# 示例
feat(user): 新增用户管理功能
fix(api): 修复登录接口异常
refactor(utils): 重构缓存工具函数
docs(readme): 更新项目文档提交类型说明:
feat: 新功能fix: 修复bugdocs: 文档更新style: 代码格式调整refactor: 代码重构test: 测试相关chore: 构建过程或辅助工具变动
分支管理策略
项目采用Git Flow分支管理模型:
shell
main # 主分支,生产环境代码
├── develop # 开发分支,集成功能
├── feature/ # 功能分支,新功能开发
├── release/ # 发布分支,版本发布准备
└── hotfix/ # 热修复分支,紧急bug修复代码审查流程
- 功能开发: 在feature分支进行开发
- 本地测试: 完成功能后本地测试验证
- 提交PR: 创建Pull Request到develop分支
- 代码审查: 团队成员进行代码审查
- CI/CD: 自动构建和测试
- 合并发布: 审查通过后合并发布
代码规范指南
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项目的完整开发规范和最佳实践。通过遵循这些规范,团队可以:
- 保持代码一致性:统一的代码风格和架构设计
- 提高开发效率:清晰的开发流程和工具链
- 确保代码质量:严格的代码审查和测试规范
- 优化性能表现:合理的性能优化策略
- 便于维护扩展:模块化的设计和清晰的文档
建议团队成员在开发过程中参考本文档,并根据实际项目需求进行适当调整和完善。
