开发规范
TIP
规定前端日常开发中的编码风格、组件写法、状态管理、协作流程等约定,适用于 Vue 3 + TypeScript + Vite + Guava UI 技术栈。
设计原则
| 原则 | 说明 |
|---|---|
| 组合优于继承 | 使用 Composables 复用逻辑,避免深层组件继承 |
| 单一职责 | 组件、函数、模块只做一件事,保持可维护性 |
| 显式优于隐式 | 类型、依赖、副作用应清晰可见,避免魔法行为,如 any(特殊情况需注释说明) |
| 约定优于配置 | 遵循团队统一结构,减少个人风格差异 |
| 安全与性能默认可接受 | 不引入明显 XSS、内存泄漏、无效重渲染等问题, 【见反例与正例】 |
技术栈约定
| 类别 | 选型 | 说明 |
|---|---|---|
| 框架 | Vue 3 | 使用 Composition API + <script setup> |
| 语言 | TypeScript | 代码禁止 any(特殊情况需注释说明) |
| 构建 | Vite | 环境变量使用 import.meta.env |
| 路由 | Vue Router | 动态路由 |
| 状态 | Pinia | 跨页面共享状态;局部状态优先 ref / composables |
| UI | Guava UI | 优先使用组件库,减少重复造轮子 |
| HTTP | 统一 request 封装 | 拦截器、错误码、鉴权集中处理 |
| 国际化 | vue-i18n | 见 多语言规范 |
代码风格
通用
- 缩进 2 空格,UTF-8 编码
- 字符串优先使用 单引号,模板字符串用于插值
- 语句末尾加分号(与 ESLint 配置保持一致)
- 单行长度建议不超过 120 字符
- 使用 ESLint + Prettier 统一格式,提交前必须通过 lint
typescript
// ✅
const userName = 'guava';
const greeting = `Hello, ${userName}`;
// ❌
const userName = 'guava';Vue 单文件组件顺序
vue
<script setup lang="ts">
// 1. 类型导入/ 组件 / Composables 导入
// 2. defineOptions 定义组件名称
// 3. Props / Emits 定义
// 4. 响应式数据hook/data定义
// 5. 计算属性
// 6. 侦听器
// 7. 方法
// 8. 业务数据 bizData
// 9. 生命周期
// 10. expose(如有)
</script>
<template>
<!-- 模板 -->
</template>
<style scoped lang="scss">
/* 样式 */
</style>TypeScript
typescript
// ✅ 明确类型,
// GvForm 表单数据使用 ItemForm 类型
const userSearchList = ref<ItemForm[]>([]);
// GvTable header数据使用 TableHeadItem 类型
const tableHeadList = ref<TableHeadItem[]>([]);
// GvTable columns数据使用 Recordable<any> 类型
const searchUserData = ref<Recordable<any>>({});
// GvForm 实例 数据使用 FormInstance 类型,import type { FormInstance } from 'element-plus';
const userSearchFm = ref<FormInstance>();
// GvTable 实例 数据使用 TableInstance 类型,import type { TableInstance } from 'element-plus';
const userTableList = ref<TableInstance>();
// 基础类型,必须定义类型
const operateType = ref<string>('');
// @method
/**
* @todo: edit user
* @author: andy.ten@tom.com & xukaixing@hotmail.com
* @Date: 2026-06-03 08:56:40
* @param {*} row
* @param {*} _index
*/
const editUser = (row: Recordable<any>, _index: number) => {
operateType.value = 'update';
rowData.value = row;
dialogVisible.value = true;
};
// ❌
// 禁止使用any类型,必须使用具体类型
const userList = ref<any>([]);
// 函数禁止使用function,必须使用const + 箭头函数
async function fetchList(params: any) {}表单与表头回掉方法传参规范
如果参数名、顺序与方法调用的是一致的,不需要在方法中定义参数,直接使用即可。
typescript
// data 表单数据
export const buildUserEditList = (handlers: EditFormHandlers, operateType = ''): FormItem[] => [
{
type: 'dic',
format: [1, 'idDic', 6],
dicType: 'zzlx',
label: '部门级别',
field: 'userLevel',
cb: handlers.dictSelectedCB,
clear: handlers.dictClearCB
},
{
type: 'dic',
format: [1, 'isDic', 20],
label: '所属部门',
field: 'deptId',
isreload: true,
dicType: 't#FUNC-DEPT-DEPTID',
dicRemote: findDictFromTableApi,
cb: handlers.dictSelectedCB
}
// ... other fields
];
// define edit form handlers
interface EditFormHandlers {
dictSelectedCB: DictSelectedFn;
dictClearCB: DictSelectedFn;
}
// app.d.ts
type DictSelectedFn = (row: DictSelectedValue, field: string, dicType: string) => void;布局规范
表单布局
表单的布局要符合 大众审美,对齐工整、留白舒适、视觉层次清晰。避免无脑堆叠元素,保持布局简洁明了。
- 文本域(
textarea)元素需要单独起一行,且尽量放到表单的最下方展示,避免文字过多导致换行时影响其它元素布局 - 选择框(
select)如果是多选,需要单独起一行,避免选项过多导致布局混乱 - 表单中如果存在多个开关(
GvSwitch),且均无关联字段,视情况需要将其放在同一行 - 所有详情页的组件均需使用抽屉(
GvDrawer) - 详情页表单元素数量少时,抽屉宽度调小,单个元素充满整行,避免空白空间过大影响美观
- 单行元素多,每个元素之间留有适当间距,避免后一个元素的
label与前一个元素的input等元素粘连在一起 - 详情页内容为自定义布局时需要使用
GvRow与GvCol组件,来保证布局的规范性和响应式 - 表单设计完后,需要切换多语言查看是否有换行等问题,作出对应调整
表格的布局也要符合 大众审美,尽量展示出全部内容避免内容被遮挡,保持用户查看一目了然。
- 表格列过多导致出现滚动条时,前2列需要固定在左侧,避免主要内容在滚动时被遮挡
- 合理设置表头的宽度,避免内容被遮挡
- 表格列至少有一个auto宽度,避免所有列宽度固定出现可能显示不全的情况
- 表格设计完后,需要切换多语言查看是否有换行等问题,作出对应调整
组件分类与职责
| 类型 | 职责 | 放置位置 |
|---|---|---|
| 页面组件 | 路由入口 | views/moduleName/ |
| 子组件/原子组件/ts | 功能组件 | views/moduleName/module/ |
| 业务组件 | 含领域逻辑的可复用块 | 模块 components/moduleName/ |
Props 设计
vue
<script setup lang="ts">
// @props
const props = defineProps({
visible: propTypes.bool.def(false),
rowData: propTypes.object,
operateType: propTypes.string
});
</script>- Props 必须定义类型,使用
useUtil-propTypes - 禁止直接修改 Props,需变更时通过
emit通知父组件 - 需设置默认值的 Props,必须使用
def定义
模板规范
vue
<template>
<!-- ✅ 多属性换行,保持可读 -->
<GvTable ref="userTableList" ref-table="userTableList" :table-head="tableHeadList" :table-data="searchUserData">
<GvButton @click="saveUser">新增</GvButton>
</GvTable>
<!-- ✅ v-for 必须带 key,且 key 必须为稳定唯一标识 -->
<div v-for="user in userList" :key="user.id">
{{ user.userName }}
</div>
<!-- ❌ 禁止使用复杂表达式,抽到 computed -->
<!-- {{ list.filter(i => i.status === 1).map(i => i.name).join(',') }} -->
</template>- 复杂条件使用
computed,复杂模板逻辑拆分子组件 v-if与v-for不要用在同一元素上
样式规范
- 组件样式默认
<style scoped lang="scss"> - 优先使用 Guava UI 主题变量与设计 token
- 深度选择器
:deep()仅在必要时使用,并注释原因 - 避免
!important,避免全局污染
Composables 开发规范
typescript
/**
* @todo: 上传模版
* @author: bugcao@163.com
* @Date: 2026-06-17 17:38:52
* @param {*} message
* @param {*} templateTableList
* @return {*}
*/
import { findALlImportTemplateApi } from '@/api/gv.common';
import { crud } from '@/hook/service/useCrud';
import { downloadTemplate } from '@/utils';
import type { TableInstance } from 'element-plus';
import type { Ref } from 'vue';
import { ref } from 'vue';
interface useTemplateUploadProps {
message: (msg: string, type?: string) => void;
templateTableList: Ref<TableInstance | undefined>;
}
export function useTemplateUpload({ message, templateTableList }: UseTemplateUploadProps) {
const searchTemplateData = ref<Recordable<any>>({});
/**
* @todo: 下载模版
* @author: bugcao@163.com
* @Date: 2026-06-17 17:38:52
* @param {*} row
*/
const handleDownload = async (row: Recordable<any>, _index: number) => {
await downloadTemplate(row.fileName);
};
/**
* @todo: 查询模版列表
* @author: bugcao@163.com
* @Date: 2026-06-17 17:39:07
*/
const handleSearch = async () => {
const table = templateTableList.value;
if (!table) return;
try {
searchTemplateData.value = await crud.searchNoFm(table, findALlImportTemplateApi, {});
} catch (e) {
message(e, 'error');
}
};
/**
* @todo: 上传回调
* @author: bugcao@163.com
* @Date: 2026-06-17 17:39:22
*/
const handleUploadCb = () => {
handleSearch();
};
return {
searchTemplateData,
handleDownload,
handleSearch,
handleUploadCb
};
}| 规则 | 说明 |
|---|---|
| 命名 | use 前缀,见 命名规范 |
| 返回值 | 返回对象而非数组,便于解构扩展 |
| 副作用 | 在 Composable 内使用 onUnmounted 清理定时器、监听器 |
| 职责 | 一个 Composable 聚焦一类逻辑,避免「万能 Hook」 |
状态管理
何时用 Pinia
| 场景 | 方案 |
|---|---|
| 组件内临时状态 | ref / 禁止使用reactive |
| 跨组件复用逻辑 | Composables |
| 跨页面 / 跨模块共享 | Pinia |
Store 使用规范
typescript
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// console.log('userStore', userStore.getUserInfo);接口与数据处理
接口导入规范
- 所有接口在页面中导入,禁止使用后缀名
vue
import { findDictFromTableApi } from '@/api/admin/user';请求规范
typescript
// api 层只做请求,不做 UI 提示
// fetch user api
export const findUsersApi = (datas: Recordable<any>) => {
const { fetch } = useFetch();
return fetch.post(`${server.gateway_admin}/sysuser/findUsers`, datas);
};
// 页面 / Composable 中处理业务与提示
const searchUserList = async () => {
const fm = userSearchFm.value;
const table = userTableList.value;
if (!fm || !table) return;
try {
searchUserData.value = await crud.search(fm, table, findUsersApi);
} catch (e) {
message(e, 'error');
}
};| 规则 | 说明 |
|---|---|
| 分层 | api/ 请求 → composables/ 逻辑 & views/ 展示 |
| 类型 | 请求/响应必须有 TypeScript 类型 |
| try/catch | 所有请求操作需 try/catch,避免全局异常处理 |
数据转换
- DTO → 根据情况是否需要转换,如需要视图模型转换需放在 Composable 或
utils/toXxx中, - 不在模板中做数据格式化,使用
computed或工具函数
错误处理
typescript
// 全局:axios 拦截器处理 HTTP 状态码、登录过期
// 业务:catch 后补充上下文日志或降级 UI
/**
* @todo: save or update user info
* @author: andy.ten@tom.com & xukaixing@hotmail.com
* @Date: 2026-06-09 14:00:58
*/
const saveUserInfo = async () => {
const fm = userEditFm.value;
if (!fm) return;
const rowData = props.rowData;
try {
if (rowData?.id) {
const updateData = await crud.update(fm, rowData?.id, updateUserApi);
if (updateData) emit('saved', { type: 'update', data: updateData, rownums: rowData?.rownums });
} else {
const saveData = await crud.save(fm, saveUserApi, true);
if (saveData) emit('saved', { type: 'insert', data: saveData });
}
} catch (e) {
message(e, 'error');
}
};| 层级 | 处理方式 |
|---|---|
| 网络 / HTTP | 请求拦截器统一处理 |
| 业务错误码 | 展示提示信息 |
| 未捕获异常 | 全局 handler 上报 |
性能约定
- 大列表使用分页,避免一次渲染上千 DOM
- 合理使用
computed,避免在模板中创建内联函数/对象 - 第三方库按需引入,关注打包体积
typescript
// ❌ 每次渲染创建新对象,Vue 认为 Props 变了,导致子组件无效更新
<ChildComponent :config="{ pageSize: 20 }" />
// ✅ 在 <script setup> 中只初始化执行一次,之后无论父组件渲染多少次,传给子组件的始终是同一个内存地址的对象。
const size = ref(20)
const tableConfig = computed(() => ({ pageSize: size.value }))
<ChildComponent :config="tableConfig" />安全约定
- 禁止将 token、密钥写入源码或提交到仓库
- 敏感信息使用环境变量(
VITE_前缀) - 用户输入提交前校验,输出展示防 XSS
- 外链打开使用
rel="noopener noreferrer" - 权限控制前端 + 后端双重校验,前端仅做体验层拦截
Git 与协作流程
Commit Message
| type | 说明 |
|---|---|
feat | 新功能 |
fix | 修补 bug |
docs | 文档变更 |
style | 格式、样式(不影响代码运行的变动) |
refactor | 重构(即不是新增功能,也不是修改 BUG 的代码) |
perf | 优化相关,比如提升性能、体验 |
test | 添加测试 |
ci | 续集成修改 |
chore | 构建 / 工具变更 |
revert | 回滚到上一个版本 |
workflow | 工作流改进 |
mod | 不确定分类的修改 |
wip | 开发中 |
types | 类型 |
提交流程
- 更新本地开发分支(拉取)
- 运行检查(如果通过了,则继续;如果未通过,则修复)。
- 暂存并提交
- 推送至远程仓库(push)
- 禁止直接向主分支 push
反例与正例
逻辑分层
typescript
// ❌ 在模板组件中直接写请求 + 弹窗 + 路由跳转
const handleClick = async () => {
const res = await axios.get('/api/users');
message('ok', 'success');
router.push('/UserEdit/' + res.data.id);
};
// ✅ api + composable/组件各司其职
// api/admin/user.ts → useUserMng.ts/UserMngIndex.vue接口导入规范
typescript
// ✅ 正确导入接口
import { findDictFromTableApi } from '@/api/admin/user';
// ❌ 错误导入接口
import { findDictFromTableApi as findDictFromTableApi_ } from '@/api/admin/user';
import { findDictFromTableApi } from '@/api/admin/user.js';
import { findDictFromTableApi } from '@/api/admin/user.ts';状态滥用
typescript
// ❌ 仅父子通信用 Pinia
const visible = useDialogStore().userFormVisible;
// ✅ 局部状态
const visible = ref(false);类型缺失
typescript
// ❌
const searchUserData = ref({});
// ✅
const searchUserData = ref<Recordable<any>>({});XSS(跨站脚本攻击)
vue
// ❌ 错 如果content来自用户输入,会直接执行 javascript 脚本, 导致 XSS 攻击
<script setup lang="ts">
import { ref } from 'vue';
const content = ref('<img src=x onerror="alert(document.cookie)">');
</script>
<template>
<div v-html="content"></div>
</template>
// ✅ 对 content 进行转义, 防止 XSS 攻击
<script setup lang="ts">
import { ref } from 'vue';
const content = ref('<img src=x onerror="alert(document.cookie)">');
</script>
<template>
<div>{{ content }}</div>
</template>内存泄漏
vue
// ❌ 错 监听 resize 事件, 未移除事件监听, 导致内存泄漏
<script setup lang="ts">
import { onMounted } from 'vue';
const handleResize = () => {
console.log('resize');
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
// ✅ 对 resize 组件销毁时,移除 resize 事件监听
<script setup lang="ts">
import { onMounted,onUnmounted } from 'vue';
const handleResize = () => {
console.log('resize');
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});无效重复渲染
vue
// ❌ 错 每次组件更新, 都会执行 getUserName 函数, 导致无效重复渲染
<script setup lang="ts">
import { ref } from 'vue';
const user = ref({
name: '张三'
});
const getUserName = () => {
return user.value.name;
};
</script>
<template>
<div>{{ getUserName() }}</div>
</template>
// ✅ 对 只有依赖变化才重新计算。
<script setup lang="ts">
import { computed, ref } from 'vue';
const user = ref({
name: '张三'
});
const userName = computed(() => user.value.name);
</script>
<template>
<div>{{ userName }}</div>
</template>Code Review 要点
- 目录与命名符合 目录规范 / 命名规范
- 对外 API 有文档注释,见 注释规范
- 无
any、无调试代码(console.log、debugger) - 异步逻辑有错误处理
- 无硬编码文案,用户可见文本走 i18n
- 组件职责清晰,无过大单文件(建议 SFC 不超过 300 行,超出则拆分)
- 无安全与性能明显问题
开发 Checklist
功能开发完成、提交前确认:
- 功能符合需求,边界情况已考虑(空数据、无权限)
- TypeScript 无类型错误
- ESLint / Prettier 通过
- 关键路径已自测(含不同分辨率,中英文环境等)
- 新增i18n key 已同步配置
- 无遗留
TODO/ 调试代码(或已关联任务)