跳到主要内容

前端编码规范

本文档定义了井云前端项目的编码规范和最佳实践,确保代码质量和一致性。

代码风格

命名规范

文件命名

  • 组件文件:PascalCase(如 UserProfile.vue
  • 工具文件:kebab-case(如 format-date.ts
  • 样式文件:kebab-case(如 user-profile.less
  • 类型文件:kebab-case(如 user-types.ts

变量命名

  • 变量:camelCase(如 userName
  • 常量:UPPER_SNAKE_CASE(如 API_BASE_URL
  • 函数:camelCase(如 getUserInfo
  • 类名:PascalCase(如 UserService

组件命名

  • 组件名:PascalCase(如 UserProfile
  • 组件属性:kebab-case(如 user-name
  • 组件事件:kebab-case(如 user-change

代码格式

缩进

  • 使用 2 个空格缩进
  • 不使用 Tab 字符
  • JSON 文件使用 2 个空格

分号

  • 语句末尾必须使用分号
  • 不省略分号

引号

  • 优先使用单引号
  • JSX 属性使用双引号
  • 模板字符串使用反引号

空格

  • 操作符前后添加空格
  • 逗号后添加空格
  • 冒号后添加空格
  • 括号内不添加空格

TypeScript 规范

类型定义

// 接口定义
interface User {
id: number
name: string
email?: string
}

// 类型别名
type UserRole = 'admin' | 'user' | 'guest'

// 泛型
interface ApiResponse<T> {
code: number
data: T
message: string
}

函数类型

// 函数声明
function getUserInfo(id: number): Promise<User> {
return api.getUser(id)
}

// 箭头函数
const formatDate = (date: Date, format: string): string => {
// 实现
}

// 函数类型
type FormatDateFunction = (date: Date, format: string) => string

类型断言

// 使用 as 断言
const user = response.data as User

// 使用尖括号断言(JSX 中不可用)
const user = <User>response.data

Vue 3 规范

组件结构

<template>
<!-- 模板内容 -->
</template>

<script setup lang="ts">
// 导入
import { ref, computed, onMounted } from 'vue'
import type { User } from '@/types/user'

// Props 定义
interface Props {
userId: number
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
})

// Emits 定义
interface Emits {
update: [user: User]
delete: [id: number]
}
const emit = defineEmits<Emits>()

// 响应式数据
const user = ref<User | null>(null)
const loading = ref(false)

// 计算属性
const displayName = computed(() => {
return user.value?.name || 'Unknown'
})

// 方法
const loadUser = async () => {
loading.value = true
try {
user.value = await getUserInfo(props.userId)
} finally {
loading.value = false
}
}

// 生命周期
onMounted(() => {
loadUser()
})
</script>

<style scoped lang="less">
// 样式
</style>

组件命名

// 组件文件名:UserProfile.vue
// 组件名:UserProfile
// 使用 kebab-case 在模板中
<user-profile />

Props 和 Emits

// Props 定义
interface Props {
title: string
count?: number
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
readonly: false
})

// Emits 定义
interface Emits {
'update:title': [title: string]
'item-click': [item: Item]
delete: [id: number]
}
const emit = defineEmits<Emits>()

CSS 规范

命名规范

  • 使用 BEM 规范
  • 组件样式使用 scoped
  • 避免全局样式污染

BEM 规范

// 块(Block)
.user-profile {
// 元素(Element)
&__header {
// 修饰符(Modifier)
&--active {
color: @primary-color;
}
}

&__content {
&--loading {
opacity: 0.5;
}
}
}

样式组织

// 变量定义
@primary-color: #1890ff;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #f5222d;

// 混合
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}

// 组件样式
.user-profile {
.flex-center();

&__header {
background-color: @primary-color;
}
}

文件组织

目录结构

src/
├── api/ # API 接口
├── assets/ # 静态资源
├── components/ # 组件
├── composables/ # 组合式函数
├── hooks/ # Hooks
├── layouts/ # 布局
├── pages/ # 页面
├── plugins/ # 插件
├── router/ # 路由
├── stores/ # 状态管理
├── styles/ # 样式
├── types/ # 类型定义
├── utils/ # 工具函数
└── main.ts # 入口文件

导入顺序

// 1. Vue 相关
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'

// 2. 第三方库
import axios from 'axios'
import { message } from 'tdesign-vue-next'

// 3. 项目内部模块
import { formatDate } from '@/utils/date'
import type { User } from '@/types/user'
import { UserService } from '@/api/user'

// 4. 相对路径导入
import './styles.less'

错误处理

异常捕获

// 使用 try-catch
const loadUser = async () => {
try {
const user = await UserService.getUser(id)
return user
} catch (error) {
console.error('Failed to load user:', error)
message.error('加载用户失败')
throw error
}
}

// 使用 Promise.catch
const loadUser = async () => {
return UserService.getUser(id).catch(error => {
console.error('Failed to load user:', error)
message.error('加载用户失败')
throw error
})
}

错误类型

// 自定义错误类
class ApiError extends Error {
constructor(
message: string,
public code: number,
public data?: any
) {
super(message)
this.name = 'ApiError'
}
}

// 错误处理
const handleApiError = (error: unknown) => {
if (error instanceof ApiError) {
switch (error.code) {
case 401:
// 处理认证错误
break
case 403:
// 处理权限错误
break
case 404:
// 处理资源不存在
break
default:
// 处理其他错误
break
}
}
}

性能优化

组件优化

// 使用 shallowRef 优化大对象
const largeData = shallowRef<LargeData>({})

// 使用 computed 缓存计算结果
const expensiveValue = computed(() => {
return heavyCalculation(props.data)
})

// 使用 watchEffect 监听响应式依赖
watchEffect(() => {
// 副作用逻辑
})

代码分割

// 路由懒加载
const routes = [
{
path: '/user',
component: () => import('@/pages/User.vue')
}
]

// 组件懒加载
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)

测试规范

单元测试

import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserProfile from './UserProfile.vue'

describe('UserProfile', () => {
it('renders user name correctly', () => {
const wrapper = mount(UserProfile, {
props: {
user: { id: 1, name: 'John Doe' }
}
})

expect(wrapper.text()).toContain('John Doe')
})

it('emits update event when name changes', async () => {
const wrapper = mount(UserProfile, {
props: {
user: { id: 1, name: 'John Doe' }
}
})

await wrapper.find('input').setValue('Jane Doe')

expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')[0]).toEqual([{ id: 1, name: 'Jane Doe' }])
})
})

代码审查

审查清单

  • 代码符合命名规范
  • 类型定义完整
  • 错误处理完善
  • 性能优化合理
  • 测试覆盖充分
  • 文档注释清晰
  • 代码可读性好
  • 安全性考虑

提交规范

feat: 新功能
fix: 修复 bug
docs: 文档更新
style: 代码格式
refactor: 重构
test: 测试
chore: 构建/工具