Skip to content

开发规范

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
UIGuava 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 等元素粘连在一起
  • 详情页内容为自定义布局时需要使用GvRowGvCol组件,来保证布局的规范性和响应式
  • 表单设计完后,需要切换多语言查看是否有换行等问题,作出对应调整

表格的布局也要符合 大众审美,尽量展示出全部内容避免内容被遮挡,保持用户查看一目了然。

  • 表格列过多导致出现滚动条时,前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-ifv-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类型

提交流程

  1. 更新本地开发分支(拉取)
  2. 运行检查(如果通过了,则继续;如果未通过,则修复)。
  3. 暂存并提交
  4. 推送至远程仓库(push)
  5. 禁止直接向主分支 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 / 调试代码(或已关联任务)

相关规范