Skip to content
第 136 / 250 章前端⏱ 10 分钟阅读

第 136 章:项目结构与规范

学习目标

  • 掌握大型 Vue 3 项目的目录组织
  • 学会模块化、组件化、自动化设计
  • 理解团队协作的代码规范
  • 构建可维护的项目脚手架

一、为什么需要规范

二、目录结构

2.1 基础结构

my-project/
├── public/
├── src/
│   ├── api/
│   ├── assets/
│   ├── components/
│   ├── composables/
│   ├── directives/
│   ├── layouts/
│   ├── plugins/
│   ├── router/
│   ├── stores/
│   ├── styles/
│   ├── types/
│   ├── utils/
│   ├── views/
│   ├── App.vue
│   └── main.ts
├── tests/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

2.2 详细结构

src/
├── api/                # 接口
│   ├── http.ts        # axios 实例
│   ├── user.ts
│   └── product.ts
├── assets/             # 静态资源
│   ├── images/
│   ├── icons/
│   └── fonts/
├── components/         # 公共组件
│   ├── base/          # 基础组件
│   ├── business/      # 业务组件
│   └── index.ts
├── composables/        # 组合式函数
│   ├── useUser.ts
│   └── useFetch.ts
├── directives/         # 自定义指令
│   ├── permission.ts
│   └── index.ts
├── layouts/            # 布局
│   ├── DefaultLayout.vue
│   └── AdminLayout.vue
├── plugins/            # 插件
│   └── i18n.ts
├── router/             # 路由
│   ├── index.ts
│   └── guards.ts
├── stores/             # Pinia
│   ├── user.ts
│   └── index.ts
├── styles/             # 样式
│   ├── variables.scss
│   ├── mixins.scss
│   └── index.scss
├── types/              # 类型
│   ├── api.d.ts
│   └── router.d.ts
├── utils/              # 工具
│   ├── format.ts
│   ├── validate.ts
│   └── storage.ts
├── views/              # 页面
│   ├── home/
│   ├── user/
│   └── admin/
├── App.vue
├── main.ts
└── env.d.ts

三、命名规范

3.1 文件命名

类型命名示例
组件PascalCaseUserCard.vue
视图PascalCaseUserProfile.vue
工具camelCaseformatDate.ts
类型camelCaseapi.d.ts
样式kebab-caseuser-card.scss
目录kebab-caseuser-profile/

3.2 组件命名

typescript
// 基础组件:以 Base 前缀
BaseButton
BaseInput
BaseTable

// 业务组件:以业务名
UserCard
ProductList
OrderForm

// 单例组件:以 The 前缀
TheHeader
TheSidebar
TheFooter

3.3 变量命名

typescript
// 常量:全大写下划线
const MAX_COUNT = 100;
const API_URL = 'https://api.example.com';

// 变量:驼峰
const userName = 'Tom';
const isLoggedIn = true;

// 布尔:is/has/can 前缀
const isVisible = true;
const hasPermission = false;
const canEdit = true;

// 函数:动词开头
function getUser(id: number) {}
function fetchData() {}
function handleClick() {}

3.4 接口命名

typescript
// 普通接口
interface User {}

// 数据传输
interface UserDTO {}

// 响应数据
interface ApiResponse<T> {}

// 实体
interface UserEntity {}

// 视图模型
interface UserVO {}

四、组件设计

4.1 单一职责

vue
<!-- ✅ 单一职责 -->
<template>
  <div>{{ displayName }}</div>
</template>

<script setup lang="ts">
const props = defineProps<{ user: User }>();
const displayName = computed(() => props.user.name);
</script>

4.2 Props 类型化

vue
<script setup lang="ts">
interface Props {
  user: User;
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  disabled: false
});
</script>

4.3 Emits 验证

vue
<script setup lang="ts">
const emit = defineEmits<{
  submit: [payload: FormData];
  cancel: [];
  'update:visible': [value: boolean];
}>();
</script>

4.4 错误边界

vue
<!-- ErrorBoundary.vue -->
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue';

const error = ref<Error | null>(null);

onErrorCaptured((err) => {
  console.error('Component error:', err);
  error.value = err;
  return false;  // 阻止传播
});
</script>

<template>
  <div v-if="error" class="error">
    <p>出错了:{{ error.message }}</p>
  </div>
  <slot v-else />
</template>

五、模块化设计

5.1 功能模块

src/
├── modules/
│   ├── user/
│   │   ├── components/
│   │   ├── composables/
│   │   ├── stores/
│   │   ├── types/
│   │   ├── views/
│   │   ├── router.ts
│   │   └── index.ts
│   ├── product/
│   └── order/
└── shared/             # 共享

5.2 模块入口

typescript
// modules/user/index.ts
import { RouteRecordRaw } from 'vue-router';
import routes from './router';
import { useUserStore } from './stores';

export const userModule = {
  routes,
  stores: [useUserStore],
  components: {}
};

5.3 自动注册

typescript
// modules/index.ts
import { userModule } from './user';
import { productModule } from './product';

const modules = [userModule, productModule];

export function registerModules(app: App) {
  modules.forEach((m) => {
    m.routes.forEach((r) => router.addRoute(r));
    m.stores.forEach((s) => s());
  });
}

六、API 层

6.1 axios 封装

typescript
// api/http.ts
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';

const http: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
});

// 请求拦截
http.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截
http.interceptors.response.use(
  (response) => {
    const { code, data, message } = response.data;
    if (code === 0) return data;
    if (code === 401) {
      // 跳转登录
    }
    throw new Error(message);
  },
  (error) => {
    console.error('Request failed:', error);
    return Promise.reject(error);
  }
);

export default http;

6.2 模块化 API

typescript
// api/user.ts
import http from './http';

export interface User {
  id: number;
  name: string;
}

export const userApi = {
  list: () => http.get<User[]>('/users'),
  get: (id: number) => http.get<User>(`/users/${id}`),
  create: (data: Partial<User>) => http.post<User>('/users', data),
  update: (id: number, data: Partial<User>) => http.put<User>(`/users/${id}`, data),
  delete: (id: number) => http.delete(`/users/${id}`)
};

6.3 类型化

typescript
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

http.get<User>('/users/1');  // 返回 ApiResponse<User>

七、状态管理

7.1 分模块

typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const profile = ref<User | null>(null);
  const token = ref<string>('');

  const isLoggedIn = computed(() => !!token.value);

  async function login(credentials: LoginDTO) {
    const res = await userApi.login(credentials);
    token.value = res.token;
    profile.value = res.user;
  }

  function logout() {
    token.value = '';
    profile.value = null;
  }

  return { profile, token, isLoggedIn, login, logout };
});

7.2 持久化

typescript
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: {
    paths: ['token']
  }
});

八、路由管理

8.1 模块化路由

typescript
// router/user.ts
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/users',
    component: () => import('@/views/user/UserList.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users/:id',
    component: () => import('@/views/user/UserDetail.vue')
  }
];

export default routes;
typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import userRoutes from './user';
import productRoutes from './product';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    ...userRoutes,
    ...productRoutes,
    { path: '/:pathMatch(.*)*', component: NotFound }
  ]
});

export default router;

8.2 路由守卫

typescript
// router/guards.ts
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } };
  }
  return true;
});

九、样式规范

9.1 SCSS 变量

scss
// styles/variables.scss
$primary: #42b883;
$danger: #f56c6c;
$warning: #e6a23c;

$font-size-sm: 12px;
$font-size-base: 14px;
$font-size-lg: 16px;

$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;

$border-radius: 4px;

9.2 工具类

scss
// styles/utilities.scss
.text-center { text-align: center; }
.flex { display: flex; }
.flex-center { display: flex; align-items: center; justify-content: center; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }

9.3 暗色主题

scss
// styles/_theme.scss
:root {
  --bg-primary: #ffffff;
  --text-primary: #303133;
}

[data-theme="dark"] {
  --bg-primary: #1a1a1a;
  --text-primary: #e4e7ed;
}

十、Git 规范

10.1 分支策略

main           # 生产
├── develop    # 测试
├── feature/*  # 功能
├── bugfix/*   # 修复
└── hotfix/*   # 紧急修复

10.2 提交信息

bash
# 格式
<type>(<scope>): <subject>

# 示例
feat(user): 新增用户列表
fix(api): 修复请求超时
docs(readme): 更新文档
style(button): 调整按钮样式
refactor(auth): 重构登录逻辑
test(user): 添加用户测试
chore: 升级依赖

10.3 类型

yaml
feat: 新功能
fix: 修复
docs: 文档
style: 格式
refactor: 重构
test: 测试
chore: 杂项
perf: 性能
build: 构建
ci: CI/CD

十一、README 模板

markdown
# My Project

## 项目介绍

简短的介绍。

## 技术栈

- Vue 3
- TypeScript
- Vite
- Pinia
- Vue Router

## 快速开始

```bash
pnpm install
pnpm dev

脚本命令

命令说明
pnpm dev开发
pnpm build构建
pnpm test测试
pnpm lint检查

目录结构

...

贡献

PR 请遵循 Conventional Commits。


## 十二、协作规范

### 12.1 Code Review

```markdown
## 检查清单
- [ ] 代码风格符合规范
- [ ] 类型定义完整
- [ ] 组件可复用
- [ ] 测试已添加
- [ ] 文档已更新
- [ ] 无 console.log
- [ ] 无明显性能问题

12.2 文档先行

  • API 文档
  • 组件文档
  • 变更日志(CHANGELOG.md)
  • 部署文档

十三、文档化

13.1 JSDoc

typescript
/**
 * 获取用户信息
 * @param id 用户ID
 * @returns 用户对象
 * @throws 当用户不存在时抛出错误
 */
export async function getUser(id: number): Promise<User> {
  // ...
}

13.2 组件 README

markdown
# UserCard

用户卡片组件。

## 用法

```vue
<UserCard :user="user" @click="handleClick" />

Props

名称类型默认说明
userUser-用户对象
size'small' | 'medium'medium尺寸

Events

名称参数说明
clickMouseEvent点击

## 十四、错误处理

### 14.1 全局错误

```typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.config.errorHandler = (err, instance, info) => {
  console.error('Global error:', err, info);
  // 上报
  reportError(err);
};

app.mount('#app');

14.2 异步错误

typescript
async function fetchUser() {
  try {
    const user = await api.getUser();
    return user;
  } catch (e) {
    console.error('Fetch user failed:', e);
    return null;
  }
}

十五、本章小结

维度关键
目录模块化、按功能
命名PascalCase / camelCase
组件单一职责、类型化
APIaxios 统一封装
状态按模块拆分
路由模块化注册
样式SCSS 变量
GitConventional Commits
文档README + JSDoc

动手练习

  1. 目录结构:为你的项目规划合理的目录结构
  2. 命名规范:统一团队命名约定
  3. API 封装:封装 axios 实例与拦截器
  4. 错误处理:添加全局错误边界
  5. README:编写完整的项目 README

推荐阅读


下一章第 137 章:HTTP 与请求封装

本站基于 VitePress 构建 · 由 Codebook 团队维护