hookehuyr

docs: 添加 Vue3 项目代码风格与最佳实践指南

添加 VUE_CODE_STYLE_GUIDE.md 文件,包含以下内容:
1. 梳理当前项目 Vue 开发方式
2. 提供 2024-2025 Vue3 生态推荐写法
3. 分析项目现状与改进建议
4. 给出可直接落地的优化清单
5. 包含推荐代码示例与分层实践
# Vue3 项目代码风格与最佳实践(结合本项目)
本文件用于梳理本项目当前的 Vue 开发方式,并给出更贴近 2024-2025 Vue3 生态的写法与取舍建议,重点关注:可读性、复用性、可维护性、边界清晰。
## 0. 本项目的“硬约束”(先对齐再谈最佳实践)
以下是从项目现状与工程配置中抽出来的约束,建议把它们视为默认前提:
- 技术栈:Vue3 + Vite + Vue Router + Vant + TailwindCSS + Less
- 语言:以 JS 为主(项目存在 `.d.ts` 文件但业务代码不写 TS)
- 样式:优先 TailwindCSS 布局;需要补充时用 Less(层级嵌套)
- 请求约定:建议接口层始终返回 `{ code, data, msg }``code === 1` 表示成功)
- 风格落地:建议用工具固化缩进、分号、命名等(否则靠“人肉自律”会越来越散)
## 1. 结论(先给答案)
本项目整体开发模式是“Vue3 + Vite + Router + Vant/Tailwind + 业务 API 分层”,整体符合当前 Vue3 工程化的主流做法,尤其是:
- 大量页面采用 Composition API 与 `<script setup>`,属于 Vue 官方推荐写法之一(官方明确推荐在 SFC + Composition API 场景使用 `<script setup>`)。
- 路由层使用动态 import 做懒加载,符合 SPA 常见优化路径。
- 有明显的组件复用意识:例如把多处重复 UI 抽到 `/src/components/ui`
但也存在一些会影响“长期维护成本”的点,主要集中在:
- 代码风格不够统一(命名、缩进、分号、注释语言、函数声明方式混用)。
- 页面文件体积偏大(一个页面承载了过多业务 + UI + 请求 + 状态),导致复用与测试难。
- 状态管理存在“上下文 context + localStorage + axios 默认头”的多头来源,边界不清晰时容易出一致性问题。
- API 返回值语义不够稳定(有的地方返回对象,有的地方返回 `false`),使上层调用必须加很多防御判断。
如果你的目标是“最大限度解耦”,实际工程里通常会走向过度抽象;更推荐的方向是:
- 以“高内聚 + 边界清晰”为第一目标;
- 用“可读性”作为默认优先级;
- 在可读性不牺牲的前提下,通过 composables / 组件 / store 做复用;
- 尽量把“副作用”关进可控的层(请求、缓存、路由跳转、埋点等)。
## 2. 本项目已做得比较好的部分(可继续保持)
### 2.1 使用 `<script setup>` 的页面组织方式
示例:[timeline.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/recall/timeline.vue)
- 数据与 UI 绑定直接、模板可读性较强。
- 通过 `ref/computed/onMounted` 把“同一功能”的逻辑写在相近位置,这就是 Composition API 的核心优势:按功能聚合,而不是按 options 分散。
### 2.2 API 层集中管理与统一入口
示例:
- [fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
- [axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js)
优点:
- API 地址集中在同文件常量里,调用点清爽。
- axios 拦截器把 401 策略收口在一处,避免业务页面到处写重复逻辑。
### 2.3 有“抽离复杂逻辑到 utils”的意识
示例:[qiniuFileHash.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/qiniuFileHash.js)
- 逻辑复杂但相对内聚,能独立于 UI 存在,这种拆分方向非常正确。
## 3. 与最新 Vue3 主流实践的差距(建议优先优化的点)
### 3.1 API 返回值不稳定:对象 vs false
现状(示例):[fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
- 成功:返回 `res.data`
- 失败:返回 `false`
问题:
- 上层 `await api()` 后需要到处判断 `res && res.code`,否则很容易出现 `Cannot read properties of false`
- “失败原因”在上层丢失(只知道 false),难以做统一的错误展示、上报、降级。
推荐方向:
- 让所有 API 始终返回 `{ code, data, msg }` 结构(即使失败也返回),上层永远用 `if (res.code === 1)` 判断。
- UI toast 是否弹出由“页面层或 composable”控制,不建议在底层 `fn` 里默认弹 toast(否则调用方无法做静默失败、重试等策略)。
### 3.2 状态来源过多:localStorage / axios 默认头 / context
现状:
- axios 请求拦截器每次都读 `localStorage.user_info` 再写 headers(见 [axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js))。
- 业务又有 `contexts/auth``currentUser`(见 [App.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/App.vue) 与相关 contexts)。
问题:
- “当前用户是谁”到底以哪份为准?一旦出现并发更新,很难排查。
推荐方向(渐进):
- 选择一个“单一事实来源”(推荐 Pinia store 或你现在的 context 之一),并定义清晰的数据流:
- store/context 是运行时状态
- localStorage 是持久化镜像(启动时 hydrate,一处写入)
- axios headers 从 store/context 派生(store 更新 -> 同步更新 headers)
### 3.3 页面体积偏大:建议用 composables “分层”
现状示例:[HomePage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/HomePage.vue)(文件较大)
常见风险:
- 页面把“请求、数据适配、交互状态、UI”都堆在一起,后续加需求会越来越难动。
推荐方向:
- 页面只做“编排”:把具体业务逻辑下沉到 composable,例如 `useCourseList()``usePurchaseFlow()`
- composable 负责:请求 + 数据适配 + 状态管理 + 副作用封装(可选)。
- 组件负责:UI(最好尽量无接口依赖)。
### 3.4 风格统一性不足(这会真实影响团队协作)
你项目里同时存在:
- 分号有/无混用
- 2 空格 / 4 空格缩进混用
- function / arrow function 混用
- 中文/英文注释混用
这类问题不会立刻出 bug,但会显著降低代码可扫描性,团队规模越大越痛。
推荐做法:
- 用 ESLint/Prettier 固化风格(如果不想引入 Prettier,也至少把 ESLint 规则开起来,并且在 CI 或提交前跑)。
## 4. 2024-2025 Vue3 “最新主流写法”长什么样(精简版)
### 4.1 组件:优先 `<script setup>` + “按功能聚合”
Vue 官方建议在 SFC + Composition API 场景使用 `<script setup>`,因为更简洁、模板与逻辑处于同一作用域、性能与 IDE 推断体验更好。
参考:
- Vue `<script setup>` 文档:https://vuejs.org/api/sfc-script-setup.html
- Vue `setup()` 文档:https://vuejs.org/api/composition-api-setup.html
### 4.2 复用:优先 composables,而不是 mixins
- 复用业务逻辑:`/src/composables/useXxx.js`
- 复用 UI:`/src/components/ui/Xxx.vue`
- 复用纯函数:`/src/utils/xxx.js`
一个简单的判断原则:
- 需要响应式状态与生命周期 -> composable
- 纯入参出参,不需要 Vue 能力 -> utils
- 需要渲染 -> component
### 4.3 状态管理:Pinia 是 Vue3 官方推荐路线之一
如果你需要跨页面共享且复杂的状态(用户、购物车、多步骤流程),Pinia 往往比 provide/inject 更适合长期维护。
补充(结合本项目现状):
- 本项目目前未引入 Pinia(`package.json` 里没有 `pinia`),因此短期内更现实的路径是:先把现有 provide/inject + localStorage + axios headers 的边界收敛清楚;如果后续状态继续变复杂,再渐进引入 Pinia。
参考:
- Vue 官方脚手架 create-vue 会提供“是否选择 Pinia”作为默认选项之一:https://vuejs.org/guide/quick-start.html
- Pinia 关于 composables 的实践:https://pinia.vuejs.org/cookbook/composables.html
### 4.4 Vue 3.3-3.5 常用能力(了解即可,按需引入)
- `defineOptions()`:在 `<script setup>` 中补充少量 Options API 选项(如 `inheritAttrs`),避免回退到普通 `<script>` 写法(见 Vue 文档同页说明)。
- `defineModel()`:更清晰地表达组件的 `v-model`,适合做“可复用表单组件/弹窗组件”。
- 响应式 props 解构(3.5+):在需要时可减少 `props.xxx` 的噪音,但要注意可读性,不建议滥用。
## 5. 结合本项目:一套更“清晰边界”的推荐分层
建议你把前端逻辑按“层”来想(从上到下):
1) View(页面)
- 只做页面编排:拿数据、传 props、处理路由跳转
2) Composable(业务逻辑)
- 负责:请求、状态、数据适配、交互流程(可测试)
3) Service/API(接口层)
- 负责:请求与返回结构,尽量不处理 UI(不弹 toast)
4) Utils(纯工具)
- 负责:纯函数、格式化、兼容处理
## 6. 结合本项目:可直接落地的优化清单(按收益排序)
1) 统一 API 返回结构:去掉 `false` 返回,调用侧统一 `res.code === 1`
- 目标:减少大量 `if (res && res.code)` 的防御判断,把错误信息(msg)保留下来
- 涉及文件:[fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
- 现有风险:部分页面写法是 `if (res.code) { ... }`,当 `code=401` 等非 0 值时会被当成“成功”分支;建议统一改成 `if (res.code === 1)`
- 现有风险:`qs` 的包名是小写 `qs`,但当前存在 `import qs from 'Qs'` 写法,在大小写敏感环境下可能无法解析(建议统一为 `qs`)。
2) 收敛“用户态”的单一事实来源:context/store 负责运行时,localStorage 负责持久化镜像
- 目标:避免 `currentUser``user_info`、请求头三套来源互相覆盖导致的“偶发态”
- 涉及文件:[auth.js](file:///Users/huyirui/program/itomix/git/mlaj/src/contexts/auth.js)[axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js)
3) 从 1 个大页面开始拆 composable:只拆“最独立的一块功能”
- 目标:不追求一次性完美分层,但要让页面先变薄,后续才好迭代与测试
- 典型样例:[HomePage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/HomePage.vue)[StudyDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/study/StudyDetailPage.vue)
4) 清理“重复/并存”的实现:避免同名组件在不同目录各自演进
- 目标:减少认知负担与误用风险(例如存在两个 `AppLayout`
- 涉及文件:[layouts/AppLayout.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/layouts/AppLayout.vue)[components/layout/AppLayout.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/components/layout/AppLayout.vue)
5) 降低“语法形态”混用成本:能不用 JSX 就别用
- 目标:减少团队理解成本(JSX + 模板混用会让页面维护难度上升),也减少对 `defineComponent/h` 等写法的依赖
- 涉及文件(存在 `<script setup lang="jsx">`):[HomePage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/HomePage.vue)[CoursesPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/courses/CoursesPage.vue)[CourseDetailPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/courses/CourseDetailPage.vue)
## 7. 推荐写法示例(以“无 TS、可读性优先”为前提)
下面代码用于学习“结构”,不要求你立刻全量改造。
### 7.1 API:始终返回统一结构(不返回 false)
```js
/**
* @typedef {Object} ApiResult
* @property {number} code - 1 表示成功,其他表示失败
* @property {any} data - 返回数据
* @property {string} msg - 返回信息
*/
/**
* @description 把任意接口结果规范化为统一结构
* @param {any} raw - 原始响应
* @returns {ApiResult}
*/
export const normalize_api_result = (raw) => {
const data = raw?.data ?? raw ?? {}
return {
code: Number(data.code) || 0,
data: data.data ?? null,
msg: data.msg ?? ''
}
}
```
### 7.2 Composable:只暴露页面真正需要的状态与动作
```js
import { ref } from 'vue'
import { showFailToast } from 'vant'
/**
* @description 示例:封装一个“加载列表”的业务逻辑
* @param {Function} fetch_list_api - 具体接口函数
* @returns {{ loading: import('vue').Ref<boolean>, list: import('vue').Ref<any[]>, load: Function }}
*/
export const use_list_loader = (fetch_list_api) => {
const loading = ref(false)
const list = ref([])
const load = async (params) => {
loading.value = true
try {
const res = await fetch_list_api(params)
if (res.code === 1) {
list.value = Array.isArray(res.data?.list) ? res.data.list : []
return true
}
showFailToast(res.msg || '加载失败')
return false
} finally {
loading.value = false
}
}
return {
loading,
list,
load
}
}
```
### 7.3 页面:只做编排(不写复杂流程)
```vue
<script setup>
import { onMounted } from 'vue'
import { useTitle } from '@vueuse/core'
import { getNewsListAPI } from '@/api/news'
import { use_list_loader } from '@/composables/use_list_loader'
useTitle('消息列表')
const { loading, list, load } = use_list_loader(getNewsListAPI)
onMounted(() => {
load({ page: 1, limit: 10 })
})
</script>
```
## 8. 建议你“下一步优先做”的 3 件事(投入产出比最高)
1) 统一 API 返回结构(去掉 `false` 返回),减少上层防御代码与隐性 bug。
2) 把 1-2 个大页面拆出 composable(例如 HomePage 中的某一块功能),你会立刻感受到可读性提升。
3) 固化格式化规则(ESLint/Prettier 选一个),把“风格争议”交给工具。