第 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.md2.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 文件命名
| 类型 | 命名 | 示例 |
|---|---|---|
| 组件 | PascalCase | UserCard.vue |
| 视图 | PascalCase | UserProfile.vue |
| 工具 | camelCase | formatDate.ts |
| 类型 | camelCase | api.d.ts |
| 样式 | kebab-case | user-card.scss |
| 目录 | kebab-case | user-profile/ |
3.2 组件命名
typescript
// 基础组件:以 Base 前缀
BaseButton
BaseInput
BaseTable
// 业务组件:以业务名
UserCard
ProductList
OrderForm
// 单例组件:以 The 前缀
TheHeader
TheSidebar
TheFooter3.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
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| user | User | - | 用户对象 |
| size | 'small' | 'medium' | medium | 尺寸 |
Events
| 名称 | 参数 | 说明 |
|---|---|---|
| click | MouseEvent | 点击 |
## 十四、错误处理
### 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 |
| 组件 | 单一职责、类型化 |
| API | axios 统一封装 |
| 状态 | 按模块拆分 |
| 路由 | 模块化注册 |
| 样式 | SCSS 变量 |
| Git | Conventional Commits |
| 文档 | README + JSDoc |
动手练习
- 目录结构:为你的项目规划合理的目录结构
- 命名规范:统一团队命名约定
- API 封装:封装 axios 实例与拦截器
- 错误处理:添加全局错误边界
- README:编写完整的项目 README
推荐阅读
- 📖 Vue 3 项目结构 — 官方推荐
- 📖 Conventional Commits — 提交规范
- 📖 TypeScript 风格指南 — Google TS 风格
- 🌐 Vue 3 最佳实践 — 风格指南
下一章:第 137 章:HTTP 与请求封装 →