第十一章:Vue3 开发规范手册(工程化与团队协作指南)

三、编码规范(Vue 单文件组件与 JavaScript/TypeScript)

2. JavaScript/TypeScript 规范

(2)异步逻辑规范
  • 优先用 async/await:禁止用 Promise 链式调用(then/catch),代码更易读且便于调试:
    // 正确(async/await + try/catch)
    const fetchUser = async () => {
      try {
        const res = await getUserInfo(); // 调用异步接口
        return res.data;
      } catch (err) {
        console.error('获取用户信息失败:', err);
        ElMessage.error('加载失败,请重试');
        throw err; // 如需上层处理,可重新抛出错误
      }
    };
    
    // 错误(Promise 链式调用,嵌套层级深时难维护)
    const fetchUser = () => {
      return getUserInfo()
        .then(res => res.data)
        .catch(err => {
          console.error('获取用户信息失败:', err);
          ElMessage.error('加载失败,请重试');
          throw err;
        });
    };
    
  • 禁止无处理的异步错误:所有 async/await 必须包裹 try/catch,或在调用时用 .catch() 捕获错误,避免控制台报错和逻辑中断;
  • 异步请求防抖/节流:高频触发的异步请求(如搜索输入框)必须加防抖(debounce),避免无效请求,推荐用组合式函数 useDebounce 封装:
    // composables/useDebounce.js
    import { ref, watch, unref } from 'vue';
    
    export function useDebounce(value, delay = 300) {
      const debouncedValue = ref(unref(value));
      let timer;
    
      watch(value, (newVal) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
          debouncedValue.value = newVal;
        }, delay);
      });
    
      return debouncedValue;
    }
    
    // 组件中使用
    <script setup>
    import { ref, watch } from 'vue';
    import { useDebounce } from '@/composables/useDebounce';
    import { searchGoods } from '@/api/goods/list';
    
    const searchInput = ref('');
    const debouncedInput = useDebounce(searchInput, 500); // 500ms 防抖
    
    // 监听防抖后的值,触发搜索
    watch(debouncedInput, async (val) => {
      if (val) await searchGoods({ keyword: val });
    });
    </script>
    
(3)TypeScript 类型规范
  • 禁止滥用 any:除非明确兼容所有类型(如第三方库无类型声明),否则必须用具体类型、unknown 或泛型;若用 any,需加注释说明原因:
    // 正确(用 interface 定义具体类型)
    interface Goods {
      id: number;
      name: string;
      price: number;
      stock: number;
    }
    const goods: Goods = { id: 1, name: '手机', price: 5999, stock: 100 };
    
    // 正确(用 unknown 替代 any,需类型断言)
    const handleUnknownData = (data: unknown) => {
      if (typeof data === 'object' && data !== null && 'id' in data) {
        const user = data as { id: number; name: string };
        console.log(user.name);
      }
    };
    
    // 错误(无理由滥用 any)
    const goods: any = { id: 1, name: '手机' };
    
  • 接口(interface)与类型别名(type)区分
    • 定义对象/类的结构用 interface(支持扩展、实现);
    • 定义联合类型、交叉类型或简单类型别名用 type
      // 正确(interface 定义对象结构)
      interface User {
        id: number;
        name: string;
      }
      interface AdminUser extends User { // 扩展接口
        role: 'admin';
      }
      
      // 正确(type 定义联合类型)
      type GoodsCategory = 'electronics' | 'clothes' | 'food';
      type GoodsWithDiscount = Goods & { discount: number }; // 交叉类型
      
  • 泛型约束:使用泛型时,需通过 extends 添加约束,避免无意义的类型范围:
    // 正确(泛型约束为“有 id 属性的对象”)
    function getById<T extends { id: number }>(list: T[], id: number): T | undefined {
      return list.find(item => item.id === id);
    }
    
    // 错误(泛型无约束,无法安全访问 id 属性)
    function getById<T>(list: T[], id: number): T | undefined {
      return list.find(item => item.id === id); // 报错:T 上不存在 id
    }
    

四、组件设计规范(高复用、低耦合)

1. 组件拆分原则

  • 单一职责:一个组件只做一件事,避免“万能组件”(如 GoodsList 只负责渲染商品列表,筛选逻辑拆到 GoodsFilter 组件);
  • 粒度适中
    • 公共组件:粒度细(如 ButtonInput),确保高复用;
    • 业务组件:粒度按业务场景划分(如 OrderItem 包含订单信息、操作按钮,无需拆分为更小组件);
  • 无状态组件优先:若组件仅接收 props 并渲染,无内部状态(如 GoodsCard),优先设计为“纯展示组件”,减少状态管理成本。

2. Props 设计规范

  • 显式类型声明:必须指定 type,可选时添加 default,必选时添加 required: true,禁止仅写 props: ['goods']
    // 正确(完整 Props 配置)
    const props = defineProps({
      goods: {
        type: Object as PropType<Goods>, // TypeScript 需指定具体类型
        required: true,
        validator: (value) => { // 自定义校验(确保有 id 和 name 属性)
          return 'id' in value && 'name' in value;
        }
      },
      showPrice: {
        type: Boolean,
        default: true // 可选属性必须有默认值
      },
      maxDescLength: {
        type: Number,
        default: 50,
        validator: (value) => value >= 0 // 校验值为非负
      }
    });
    
    // 错误(无类型、无默认值、无校验)
    const props = defineProps(['goods', 'showPrice', 'maxDescLength']);
    
  • Props 单向数据流:子组件禁止修改 props,需通过 emit 通知父组件修改,避免数据流混乱:
    <!-- 正确(子组件通过 emit 通知父组件) -->
    <script setup>
    const props = defineProps({ count: Number });
    const emit = defineEmits(['update:count']);
    const increment = () => {
      emit('update:count', props.count + 1); // 通知父组件更新
    };
    </script>
    
    <template>
      <button @click="increment">{{ props.count }}</button>
    </template>
    
    <!-- 父组件使用 -->
    <template>
      <Counter :count="count" @update:count="newCount => count = newCount" />
    </template>
    
    <!-- 错误(子组件直接修改 props) -->
    <script setup>
    const props = defineProps({ count: Number });
    const increment = () => {
      props.count++; // 报错:props 是只读的
    };
    </script>
    
  • 避免过多 Props:若组件 Props 超过 8 个,需拆分组件或合并为对象类型(如将 nameageavatar 合并为 user 对象)。

3. 组件通信规范

  • 父子通信:优先用 props + emit,禁止用 $parent/$children(耦合度高,难以维护);
  • 跨层级通信
    • 静态配置(如主题、权限)用 provide/inject
    • 动态数据(如用户信息)用 Pinia,禁止用 provide/inject 传递频繁更新的数据;
  • 兄弟通信:禁止用“事件总线(mitt)”处理复杂逻辑,优先用“父组件中转”或 Pinia,避免事件流混乱。

五、状态管理规范(Pinia 最佳实践)

1. Store 设计原则

  • 按业务模块拆分:一个 Store 对应一个业务领域(如 userStore 处理用户相关,cartStore 处理购物车相关),禁止一个 Store 包含所有状态;
  • 状态分类:Store 内状态按“核心状态(如 tokenuserId)”和“衍生状态(如 isLogin = !!token)”拆分,衍生状态优先用 Getter 计算,禁止冗余存储:
    // 正确(衍生状态用 Getter 计算)
    export const useUserStore = defineStore('user', {
      state: () => ({
        token: '',
        userId: ''
      }),
      getters: {
        isLogin: (state) => !!state.token // 衍生状态,不存储在 state 中
      }
    });
    
    // 错误(冗余存储衍生状态)
    export const useUserStore = defineStore('user', {
      state: () => ({
        token: '',
        userId: '',
        isLogin: false // 冗余,应通过 token 计算
      })
    });
    
  • Action 职责
    • 同步 Action:仅用于修改状态(如 updateUserInfo),禁止包含业务逻辑;
    • 异步 Action:处理异步逻辑(如接口请求),请求成功后调用同步 Action 修改状态,禁止在组件中直接调用接口后修改 Store 状态。

2. 状态访问规范

  • 组件中禁止直接修改 Store 状态:必须通过 Action 修改,确保状态变更可追踪(便于调试和日志记录):
    // 正确(通过 Action 修改状态)
    const userStore = useUserStore();
    userStore.loginSuccess({ token: 'xxx', userId: 1 });
    
    // 错误(直接修改 Store 状态)
    const userStore = useUserStore();
    userStore.token = 'xxx';
    userStore.userId = 1;
    
  • 避免过度依赖 Store:组件私有状态(如表单输入值、弹窗显示隐藏)无需放入 Store,仅将“跨组件共享的状态”存入 Store。

六、工程化与协作规范

1. 代码提交规范(Git Commit 规范)

采用 Conventional Commits 规范,确保提交信息清晰,便于生成 CHANGELOG 和版本管理,格式为:
<type>[optional scope]: <description>

  • type 类型

    • feat:新增功能(如“feat: 新增购物车结算功能”);
    • fix:修复 bug(如“fix: 修复购物车数量计算错误”);
    • docs:仅修改文档(如“docs: 更新 API 文档”);
    • style:不影响代码逻辑的格式修改(如缩进、空格);
    • refactor:重构代码(既不新增功能也不修复 bug,如“refactor: 优化用户信息请求逻辑”);
    • test:新增/修改测试用例;
    • chore:构建流程、依赖管理的修改(如“chore: 更新依赖版本”)。
  • 示例

    feat(cart): 新增购物车商品批量删除功能
    fix(goods): 修复商品详情页图片加载失败问题
    refactor(user): 重构用户登录状态管理逻辑
    

2. 代码审查(Code Review)规范

  • 审查重点
    • 功能正确性:是否符合需求文档;
    • 代码规范:是否遵循本手册规范;
    • 性能问题:是否存在冗余渲染、无效请求;
    • 安全性:是否存在 XSS、接口未授权访问等问题;
  • 反馈原则
    • 对事不对人,反馈需具体(如“这里未加防抖,会导致频繁请求”而非“这里写得不好”);
    • 复杂问题需附优化示例代码。

3. 环境配置规范

  • 环境变量:通过 .env.development(开发环境)、.env.production(生产环境)、.env.test(测试环境)配置环境变量,变量名必须以 VITE_ 开头(Vite 要求):
    # .env.development
    VITE_API_BASE_URL=http://dev.example.com/api
    VITE_ENV=development
    
    # .env.production
    VITE_API_BASE_URL=https://prod.example.com/api
    VITE_ENV=production
    
  • 禁止硬编码环境相关配置:如接口地址、APP 名称等,必须通过环境变量获取(import.meta.env.VITE_API_BASE_URL)。

七、性能与安全规范

1. 性能优化规范

  • 避免不必要的渲染
    • 组件 props 为对象/数组时,避免每次渲染创建新对象(如 :props="{ id: 1 }" 改为 const props = ref({ id: 1 }),再 :props="props");
    • v-memo 缓存列表渲染结果(如 v-memo="[item.id]",仅当 item.id 变化时重新渲染);
  • 接口请求优化
    • 合并重复请求(如同一时间多次调用 getUserInfo,仅发送一次请求);
    • 缓存请求结果(如商品详情页数据,短时间内重复访问无需重新请求)。

2. 安全规范

  • XSS 防护
    • 禁止直接插入 HTML(如 v-html),若必须使用,需先过滤危险标签/属性(如用 DOMPurify 库);
    • 接口返回的用户输入内容(如评论、昵称)必须做转义处理;
  • 接口安全
    • 所有接口必须通过 HTTPS 访问;
    • 敏感接口(如登录、支付)需加签名验证,禁止明文传输密码;
  • 权限控制
    • 前端权限仅做辅助控制,核心权限逻辑必须在后端实现(如用户无权访问某接口,后端需返回 403);
    • 路由守卫中必须验证用户权限,禁止通过 URL 直接访问无权限页面。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐