Developer Guide

前端开发指南

Tagtag Starter 前端开发指南,包括组件开发规范、Hooks 使用指南和最佳实践。

Tagtag Starter 前端基于 Vue 3 + TypeScript + Vite 开发,采用组件化和模块化的设计理念。本文档将详细介绍前端开发的规范、最佳实践和常用工具,帮助开发者快速上手并保持代码质量。

1. 技术栈

技术版本用途
Vue3.3+前端框架,使用 Composition API
TypeScript5.0+类型系统,提供类型安全
Vite4.3+构建工具,提供快速的开发体验
Pinia2.0+状态管理,替代 Vuex
Vue Router4.2+路由管理,支持动态路由
Ant Design Vue4.0+UI 组件库,提供丰富的企业级组件
Axios1.5+HTTP 客户端,封装在 @vben/request 中
VXE Table4.0+高性能表格组件,用于复杂表格场景
ESLint8.40+代码检查,确保代码质量
Prettier2.8+代码格式化,统一代码风格
Vitest最新单元测试框架
Cypress最新E2E 测试框架

2. 项目结构

前端项目采用 Monorepo 结构,位于 frontend 目录下。这种结构允许我们在一个仓库中管理多个应用和共享包,提高代码复用性和维护效率。

frontend/
├── apps/                  # 应用目录
│   └── tagtag/            # 主应用
│       ├── src/           # 源代码
│       │   ├── adapter/   # 适配器层,封装第三方库
│       │   │   ├── component/ # 组件适配器
│       │   │   ├── form.ts    # 表单适配器
│       │   │   └── vxe-table.ts # 表格适配器
│       │   ├── api/       # API 定义
│       │   │   ├── core/  # 核心 API(登录、用户、菜单等)
│       │   │   ├── modules/ # 模块 API(iam、system、storage)
│       │   │   ├── request.ts # 请求客户端配置
│       │   │   └── index.ts
│       │   ├── assets/    # 静态资源
│       │   ├── components/# 公共组件
│       │   ├── composables/# 组合式函数
│       │   ├── hooks/     # 自定义 Hooks
│       │   │   └── web/   # Web 相关 Hooks
│       │   ├── layouts/   # 布局组件
│       │   │   ├── auth.vue    # 认证布局
│       │   │   └── basic.vue   # 基础布局
│       │   ├── locales/   # 国际化
│       │   │   └── langs/ # 语言包
│       │   ├── router/    # 路由配置
│       │   │   ├── routes/ # 路由定义
│       │   │   ├── guard.ts  # 路由守卫
│       │   │   └── access.ts # 权限控制
│       │   ├── store/     # 状态管理
│       │   │   ├── auth.ts    # 认证状态
│       │   │   └── index.ts
│       │   ├── styles/    # 样式文件
│       │   ├── types/     # 类型定义
│       │   ├── utils/     # 工具函数
│       │   ├── views/     # 页面组件
│       │   │   ├── _core/ # 核心页面(登录、404等)
│       │   │   ├── dashboard/ # 仪表盘
│       │   │   └── modules/ # 业务模块
│       │   ├── app.vue    # 根组件
│       │   ├── bootstrap.ts # 应用启动逻辑
│       │   ├── main.ts    # 入口文件
│       │   └── preferences.ts # 应用偏好设置
│       ├── public/        # 公共资源
│       ├── index.html     # HTML 模板
│       ├── vite.config.ts # Vite 配置
│       └── tsconfig.json  # TypeScript 配置
├── internal/              # 内部包
│   ├── lint-configs/      # 代码检查配置
│   ├── node-utils/        # Node 工具函数
│   ├── tailwind-config/   # Tailwind CSS 配置
│   ├── tsconfig/          # TypeScript 配置
│   └── vite-config/       # Vite 配置
├── packages/              # 公共包
│   └── @core/             # 核心包
│       ├── base/          # 基础功能(设计、图标、共享工具)
│       ├── composables/   # 组合式函数
│       └── typings/       # 类型定义
└── pnpm-workspace.yaml    # pnpm 工作区配置

2.1 目录说明

  • adapter/: 适配器层,用于封装第三方库,降低对特定库的依赖。例如 vxe-table.ts 封装了 VXE Table 的配置,form.ts 封装了表单相关的逻辑。
  • api/: API 层,分为核心 API 和模块 API。核心 API 包含登录、用户信息、菜单等基础接口,模块 API 按业务模块划分。
  • router/: 路由配置,包含路由定义、路由守卫和权限控制。支持动态路由生成,根据用户权限动态加载菜单和路由。
  • store/: 状态管理,使用 Pinia 进行状态管理。主要包含认证状态、用户信息、权限等全局状态。
  • views/: 页面组件,按功能模块组织。_core 目录包含核心页面(登录、404等),modules 目录包含业务模块页面。

3. 组件开发规范

3.1 组件命名

  • 文件名:使用 PascalCase 命名,如 FormModal.vueUserTable.vue
  • 组件名:与文件名保持一致,使用 PascalCase 命名
  • 组件标签:在模板中使用 kebab-case,如 <form-modal></form-modal><user-table></user-table>

命名示例

文件名: UserFormDrawer.vue
组件名: UserFormDrawer
使用: <user-form-drawer />

3.2 组件结构

组件应遵循以下结构:

<template>
  <!-- 组件模板 -->
</template>

<script setup lang="ts">
// 组件逻辑
</script>

<style scoped>
/* 组件样式 */
</style>

3.3 组件逻辑组织

使用 script setup 语法糖,简化组件开发。按照以下顺序组织代码:

  1. 导入语句
  2. 组件 Props 定义
  3. Emits 定义
  4. 响应式数据定义
  5. 计算属性
  6. 生命周期钩子
  7. 方法定义
  8. 事件处理

示例

<template>
  <div class="form-modal">
    <a-modal
      v-model:open="visible"
      :title="title"
      @ok="handleOk"
      @cancel="handleCancel"
    >
      <a-form :model="formData" :rules="rules" ref="formRef">
        <!-- 表单内容 -->
      </a-form>
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { FormInstance } from 'ant-design-vue';

// 1. 导入语句
import { message } from 'ant-design-vue';

// 2. Props 定义
const props = defineProps<{
  visible: boolean;
  title?: string;
  initialData?: any;
}>();

// 3. Emits 定义
const emit = defineEmits<{
  (e: 'update:visible', value: boolean): void;
  (e: 'submit', data: any): void;
}>();

// 4. 响应式数据
const formRef = ref<FormInstance | null>(null);
const formData = reactive<any>({
  // 表单字段
});

// 5. 计算属性
const rules = computed(() => ({
  // 表单规则
}));

// 6. 生命周期钩子
onMounted(() => {
  if (props.initialData) {
    Object.assign(formData, props.initialData);
  }
});

// 7. 方法定义
const handleOk = async () => {
  if (formRef.value) {
    await formRef.value.validate();
    emit('submit', formData);
    emit('update:visible', false);
  }
};

const handleCancel = () => {
  emit('update:visible', false);
};
</script>

<style scoped>
.form-modal {
  /* 组件样式 */
}
</style>

3.4 组件通信

  • Props/Emits:父子组件通信首选 Props/Emits
  • Provide/Inject:祖先组件向后代组件传递数据,适用于深层嵌套的场景
  • Pinia:全局状态管理,适用于跨组件的共享状态
  • Event Bus:不推荐使用,建议使用 Pinia 替代

Props/Emits 示例

<!-- 父组件 -->
<template>
  <ChildComponent 
    :data="parentData" 
    @update="handleUpdate" 
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const parentData = ref({ name: 'test' });

const handleUpdate = (newData: any) => {
  parentData.value = newData;
};
</script>

<!-- 子组件 -->
<template>
  <div @click="emit('update', { name: 'updated' })">
    {{ data.name }}
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{
  data: any;
}>();

const emit = defineEmits<{
  (e: 'update', data: any): void;
}>();
</script>

3.5 组件样式

  • 使用 scoped 属性确保样式作用域隔离
  • 采用 BEM 命名规范:block__element--modifier
  • 避免使用 !important
  • 使用 CSS 变量管理主题色和通用样式

示例

/* BEM 命名规范 */
.post-card {
  /* 块样式 */
  padding: 16px;
  border: 1px solid #e8e8e8;
}

.post-card__title {
  /* 元素样式 */
  font-size: 18px;
  font-weight: 500;
}

.post-card--highlight {
  /* 修饰符样式 */
  background-color: #f0f9ff;
}

.post-card__content--large {
  /* 元素 + 修饰符 */
  font-size: 16px;
}

3.6 组件复用

对于可复用的组件,建议:

  1. 将通用组件放在 src/components 目录
  2. 为组件添加清晰的 Props 类型定义
  3. 提供插槽(Slots)以增强灵活性
  4. 编写组件文档和使用示例

示例

<template>
  <div class="card">
    <div v-if="$slots.header" class="card__header">
      <slot name="header"></slot>
    </div>
    <div class="card__body">
      <slot></slot>
    </div>
    <div v-if="$slots.footer" class="card__footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script setup lang="ts">
interface CardProps {
  title?: string;
  bordered?: boolean;
}

const props = withDefaults(defineProps<CardProps>(), {
  bordered: true
});
</script>

4. Hooks 使用指南

4.1 自定义 Hooks 命名

  • 使用 use 前缀命名,如 useAuthuseTableuseDict
  • 文件名与 Hook 名保持一致,使用 camelCase 命名
  • Hook 应该返回响应式数据和方法,便于组件使用

4.2 常用内置 Hooks

Hook用途示例
ref创建响应式数据const count = ref(0)
reactive创建响应式对象const state = reactive({ count: 0 })
computed创建计算属性const double = computed(() => count.value * 2)
watch监听数据变化watch(count, (newVal) => console.log(newVal))
watchEffect自动监听依赖变化watchEffect(() => console.log(count.value))
onMounted组件挂载后执行onMounted(() => fetchData())
onUpdated组件更新后执行onUpdated(() => console.log('updated'))
onUnmounted组件卸载前执行onUnmounted(() => clearInterval(timer))
provide提供数据给后代组件provide('key', value)
inject从祖先组件获取数据const value = inject('key')

4.3 自定义 Hooks 示例

4.3.1 useAuth Hook

用于管理用户认证状态,封装了登录、登出、权限检查等功能。

// src/hooks/useAuth.ts
import { useAuthStore } from '@/store/auth';
import { computed } from 'vue';

export function useAuth() {
  const authStore = useAuthStore();

  const isAuthenticated = computed(() => !!authStore.token);
  const userInfo = computed(() => authStore.userInfo);
  const permissions = computed(() => authStore.permissions);

  const hasPermission = (permission: string) => {
    return permissions.value.includes(permission);
  };

  const login = async (loginData: any) => {
    return authStore.login(loginData);
  };

  const logout = async () => {
    return authStore.logout();
  };

  return {
    isAuthenticated,
    userInfo,
    permissions,
    hasPermission,
    login,
    logout
  };
}

使用示例

<template>
  <div v-if="isAuthenticated">
    <p>欢迎,{{ userInfo?.username }}</p>
    <a-button @click="logout">退出登录</a-button>
  </div>
  <div v-else>
    <a-button @click="handleLogin">登录</a-button>
  </div>
</template>

<script setup lang="ts">
import { useAuth } from '@/hooks/useAuth';

const { isAuthenticated, userInfo, login, logout } = useAuth();

const handleLogin = async () => {
  await login({ username: 'admin', password: '123456' });
};
</script>

4.3.2 useTable Hook

用于简化表格组件的开发,封装了分页、搜索、重置等功能。

// src/hooks/useTable.ts
import { ref, reactive } from 'vue';

export function useTable<T>(fetchData: (params: any) => Promise<any>) {
  const loading = ref(false);
  const dataSource = ref<T[]>([]);
  const total = ref(0);
  
  const pagination = reactive({
    current: 1,
    pageSize: 10,
    showSizeChanger: true,
    pageSizeOptions: ['10', '20', '50', '100'],
    showTotal: (total: number) => `${total} 条数据`
  });
  
  const searchParams = reactive<any>({});
  
  const fetchTableData = async () => {
    loading.value = true;
    try {
      const params = {
        page: pagination.current,
        size: pagination.pageSize,
        ...searchParams
      };
      const result = await fetchData(params);
      dataSource.value = result.records || [];
      total.value = result.total || 0;
    } catch (error) {
      console.error('获取表格数据失败:', error);
    } finally {
      loading.value = false;
    }
  };
  
  const handleSearch = () => {
    pagination.current = 1;
    fetchTableData();
  };
  
  const handleReset = () => {
    Object.keys(searchParams).forEach(key => {
      searchParams[key] = '';
    });
    pagination.current = 1;
    fetchTableData();
  };
  
  const handlePageChange = (page: number, pageSize: number) => {
    pagination.current = page;
    pagination.pageSize = pageSize;
    fetchTableData();
  };
  
  return {
    loading,
    dataSource,
    total,
    pagination,
    searchParams,
    fetchTableData,
    handleSearch,
    handleReset,
    handlePageChange
  };
}

4.3.3 useDict Hook

用于加载和管理字典数据,支持多个字典类型的并行加载。

// src/hooks/useDict.ts
import { onMounted, reactive, toRefs } from 'vue';
import { getDictDataList } from '@/api/modules/system/dict';

export function useDict(...dictTypes: string[]) {
  const dicts = reactive<Record<string, any[]>>({});

  const initDict = async () => {
    const promises = dictTypes.map(async (type) => {
      const data = await getDictDataList(type);
      dicts[type] = data.map((item) => ({
        label: item.itemName,
        value: item.itemCode,
        color: item.cssClass,
        ...item,
      }));
    });

    await Promise.all(promises);
  };

  onMounted(() => {
    initDict();
  });

  return {
    ...toRefs(dicts),
  };
}

使用示例

<template>
  <a-form-item label="性别">
    <a-select v-model:value="form.gender">
      <a-select-option 
        v-for="item in genderDict" 
        :key="item.value" 
        :value="item.value"
      >
        {{ item.label }}
      </a-select-option>
    </a-select>
  </a-form-item>
</template>

<script setup lang="ts">
import { reactive } from 'vue';
import { useDict } from '@/hooks/useDict';

const { genderDict } = useDict('gender');

const form = reactive({
  gender: undefined
});
</script>

4.4 Hooks 最佳实践

  • 单一职责:每个 Hook 只负责一个功能,保持简洁
  • 可复用性:设计通用的 Hook,避免业务逻辑耦合
  • 类型安全:为 Hook 添加完整的 TypeScript 类型定义
  • 清理副作用:在组件卸载时清理副作用,如定时器、事件监听等
  • 返回响应式数据:使用 toRefs 返回响应式数据,便于解构使用
  • 错误处理:在 Hook 中添加适当的错误处理逻辑

5. 状态管理

5.1 Pinia 规范

Pinia 是 Vue 3 官方推荐的状态管理库,相比 Vuex 更轻量、更简洁。在 Tagtag Starter 中,我们使用 Pinia 进行全局状态管理。

  • Store 命名:使用 PascalCase 命名,如 AuthStoreUserStore
  • 文件结构:每个模块对应一个 Store 文件
  • 状态定义:使用 state 函数定义响应式状态
  • Action 命名:使用 camelCase 命名,异步 Action 应返回 Promise
  • Getters 命名:使用 camelCase 命名

示例

// src/store/auth.ts
import { defineStore } from 'pinia';
import { loginApi, logoutApi } from '@/api/core/auth';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
    permissions: JSON.parse(localStorage.getItem('permissions') || '[]')
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    hasPermission: (state) => (permission: string) => {
      return state.permissions.includes(permission);
    }
  },
  
  actions: {
    async login(loginData: any) {
      const result = await loginApi(loginData);
      this.token = result.token;
      this.userInfo = result.user;
      this.permissions = result.user.permissions || [];
      
      localStorage.setItem('token', this.token);
      localStorage.setItem('userInfo', JSON.stringify(this.userInfo));
      localStorage.setItem('permissions', JSON.stringify(this.permissions));
      
      return result;
    },
    
    async logout() {
      await logoutApi();
      this.clearAuth();
    },
    
    clearAuth() {
      this.token = '';
      this.userInfo = null;
      this.permissions = [];
      
      localStorage.removeItem('token');
      localStorage.removeItem('userInfo');
      localStorage.removeItem('permissions');
    }
  }
});

5.2 实际项目中的 Store 实现

在 Tagtag Starter 项目中,认证 Store 的实现更加完善,包含了 token 刷新、登录状态管理等功能。

// frontend/apps/tagtag/src/store/auth.ts
import type { Recordable, UserInfo } from '@vben/types';

import { ref } from 'vue';
import { useRouter } from 'vue-router';

import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';

import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';

import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';

export const useAuthStore = defineStore('auth', () => {
  const accessStore = useAccessStore();
  const userStore = useUserStore();
  const router = useRouter();

  const loginLoading = ref(false);

  async function authLogin(
    params: Recordable<any>,
    onSuccess?: () => Promise<void> | void,
  ) {
    let userInfo: null | UserInfo = null;
    try {
      loginLoading.value = true;
      const { accessToken, refreshToken } = await loginApi(params);

      if (accessToken) {
        accessStore.setAccessToken(accessToken);
        accessStore.setRefreshToken(refreshToken ?? null);

        const [fetchUserInfoResult, accessCodes] = await Promise.all([
          fetchUserInfo(),
          getAccessCodesApi(),
        ]);

        userInfo = fetchUserInfoResult;

        userStore.setUserInfo(userInfo);
        accessStore.setAccessCodes(accessCodes);

        if (accessStore.loginExpired) {
          accessStore.setLoginExpired(false);
        } else {
          onSuccess
            ? await onSuccess?.()
            : await router.push(
                userInfo.homePath || preferences.app.defaultHomePath,
              );
        }

        if (userInfo?.realName || userInfo?.username) {
          notification.success({
            description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName || userInfo?.username}`,
            duration: 3,
            message: $t('authentication.loginSuccess'),
          });
        }
      }
    } finally {
      loginLoading.value = false;
    }

    return {
      userInfo,
    };
  }

  async function logout(redirect: boolean = true) {
    try {
      const token = accessStore.accessToken ?? '';
      if (token) {
        await logoutApi(token);
      }
    } catch {
    }
    resetAllStores();
    accessStore.setLoginExpired(false);

    await router.replace({
      path: LOGIN_PATH,
      query: redirect
        ? {
            redirect: encodeURIComponent(router.currentRoute.value.fullPath),
          }
        : {},
    });
  }

  async function fetchUserInfo() {
    let userInfo: null | UserInfo = null;
    userInfo = await getUserInfoApi();
    userStore.setUserInfo(userInfo);
    return userInfo;
  }

  function $reset() {
    loginLoading.value = false;
  }

  return {
    $reset,
    authLogin,
    fetchUserInfo,
    loginLoading,
    logout,
  };
});

5.3 状态管理最佳实践

  • 合理划分 Store:按功能模块划分 Store,避免单一 Store 过大。例如:auth(认证)、user(用户)、access(权限)
  • 避免过度使用全局状态:只将需要全局共享的状态放入 Store,组件内部状态使用 refreactive
  • 使用 Actions 处理异步逻辑:所有异步操作都应放在 Actions 中,保持状态更新的可追踪性
  • 持久化存储:对于需要持久化的状态,使用 localStorage 或 cookie 存储
  • 使用 Getters 计算派生状态:避免在组件中重复计算相同的派生状态
  • 类型安全:为 State、Getters、Actions 添加完整的 TypeScript 类型定义

6. API 请求

6.1 Axios 配置

前端使用 Axios 进行 API 请求,配置位于 src/api/index.ts

示例

// src/api/index.ts
import axios from 'axios';
import { useAuthStore } from '@/store/auth';

const request = axios.create({
  baseURL: import.meta.env.VITE_APP_API_BASE_URL,
  timeout: 10000
});

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore();
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    return response.data;
  },
  async (error) => {
    // 错误处理逻辑
    return Promise.reject(error);
  }
);

export default request;

6.2 API 模块化

将 API 按模块划分,每个模块对应一个文件。

示例

// src/api/modules/user.ts
import request from '../index';

export const userApi = {
  // 获取用户列表
  getList: (params: any) => {
    return request.get('/users', { params });
  },
  
  // 获取用户详情
  getDetail: (id: number) => {
    return request.get(`/users/${id}`);
  },
  
  // 创建用户
  create: (data: any) => {
    return request.post('/users', data);
  },
  
  // 更新用户
  update: (data: any) => {
    return request.put('/users', data);
  },
  
  // 删除用户
  delete: (id: number) => {
    return request.delete(`/users/${id}`);
  }
};

6.3 API 调用规范

  • 使用 async/await:统一使用 async/await 处理异步请求
  • 错误处理:在调用 API 时添加 try/catch 处理错误
  • 参数验证:在调用 API 前验证参数的合法性
  • 加载状态:显示适当的加载状态,提高用户体验

7. 路由管理

7.1 路由配置

路由配置位于 src/router 目录下,采用模块化设计。

示例

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import Layout from '@/layouts/main.vue';

const routes = [
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: {
          title: '仪表盘',
          icon: 'dashboard',
          permission: 'sys:dashboard:view'
        }
      }
    ]
  },
  {
    path: '/auth',
    component: () => import('@/layouts/auth.vue'),
    children: [
      {
        path: 'login',
        name: 'Login',
        component: () => import('@/views/auth/login.vue'),
        meta: {
          title: '登录',
          requiresAuth: false
        }
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/error/404.vue')
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;

7.2 路由守卫

使用路由守卫实现权限控制和页面跳转逻辑。

示例

// src/router/guard.ts
import router from './index';
import { useAuthStore } from '@/store/auth';

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore();
  
  // 设置页面标题
  document.title = `${to.meta.title || 'Tagtag Starter'} - 后台管理系统`;
  
  // 检查是否需要认证
  if (to.meta.requiresAuth !== false) {
    if (authStore.isAuthenticated) {
      // 已登录,检查权限
      if (to.meta.permission) {
        if (authStore.hasPermission(to.meta.permission as string)) {
          next();
        } else {
          // 无权限,跳转到403页面
          next({ path: '/403' });
        }
      } else {
        next();
      }
    } else {
      // 未登录,跳转到登录页面
      next({ path: '/auth/login', query: { redirect: to.fullPath } });
    }
  } else {
    next();
  }
});

8. 代码规范

8.1 ESLint 规则

前端项目使用 ESLint 进行代码检查,规则配置位于 .eslintrc.cjs 文件中。

主要规则:

  • 缩进:使用 2 个空格
  • 引号:使用单引号
  • 分号:不使用分号
  • 尾随逗号:使用尾随逗号
  • 禁止未使用的变量
  • 禁止未使用的导入

8.2 Prettier 配置

使用 Prettier 进行代码格式化,配置位于 .prettierrc 文件中。

8.3 提交规范

使用 Commitizen 提交代码,遵循 Angular 提交规范:

<type>(<scope>): <subject>

<body>

<footer>

类型

  • feat:新功能
  • fix:修复 bug
  • docs:文档更新
  • style:代码样式更新
  • refactor:代码重构
  • test:测试更新
  • chore:构建工具或依赖更新

9. 测试

9.1 单元测试

使用 Vitest 进行单元测试,测试文件位于 __tests__ 目录下。

示例

// src/components/Button/__tests__/Button.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Button from '../Button.vue';

describe('Button Component', () => {
  it('should render correctly', () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Button Text'
      }
    });
    expect(wrapper.text()).toBe('Button Text');
  });
  
  it('should emit click event', () => {
    const wrapper = mount(Button);
    wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
  });
});

9.2 E2E 测试

使用 Cypress 进行端到端测试,测试文件位于 cypress/e2e 目录下。

10. 性能优化

10.1 组件优化

  • 懒加载组件:使用 defineAsyncComponent 或动态 import 懒加载组件
  • 使用 v-once:对于静态内容使用 v-once 指令
  • 使用 v-memo:对于频繁更新的列表使用 v-memo 指令
  • 避免不必要的渲染:使用 computedwatch 优化渲染逻辑

10.2 网络优化

  • HTTP 缓存:合理设置缓存策略
  • 压缩资源:启用 Gzip 或 Brotli 压缩
  • CDN 加速:使用 CDN 加速静态资源访问
  • 减少请求次数:合并请求,使用 HTTP/2

10.3 构建优化

  • 代码分割:使用 Vite 的代码分割功能
  • Tree Shaking:移除未使用的代码
  • 按需加载:按需加载第三方库
  • 预构建依赖:使用 Vite 的依赖预构建功能

11. 开发工具

11.1 VS Code 插件

  • Volar:Vue 3 官方扩展
  • TypeScript Vue Plugin:TypeScript Vue 支持
  • ESLint:代码检查
  • Prettier:代码格式化
  • GitLens:Git 增强
  • Ant Design Vue Helper:Ant Design Vue 组件辅助

11.2 命令行工具

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建生产版本
pnpm build

# 预览生产版本
pnpm preview

# 运行 ESLint
pnpm lint

# 运行 Prettier
pnpm format

# 运行单元测试
pnpm test:unit

# 运行 E2E 测试
pnpm test:e2e

12. 最佳实践

12.1 代码可读性

  • 注释:为复杂逻辑添加注释
  • 命名规范:使用有意义的变量名和函数名
  • 代码结构:合理组织代码结构,使用空行分隔不同逻辑块
  • 缩进:保持一致的缩进风格

12.2 可维护性

  • 模块化设计:将代码拆分为可复用的模块
  • 文档:为公共组件和函数编写文档
  • 测试:为核心功能编写测试用例
  • 版本控制:合理使用 Git,提交粒度适中

12.3 用户体验

  • 响应式设计:适配不同屏幕尺寸
  • 加载状态:显示适当的加载状态
  • 错误处理:友好的错误提示
  • 动画效果:适当的动画效果,提升用户体验
  • 性能优化:确保页面加载和交互流畅

13. 总结

本文档介绍了 Tagtag Starter 前端开发的规范、最佳实践和常用工具。遵循这些规范可以提高代码质量、可维护性和开发效率,确保团队协作顺畅。

前端开发是一个不断演进的领域,建议开发者持续关注 Vue 3、TypeScript 等技术的最新发展,不断学习和实践新的技术和最佳实践。