hookehuyr

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

添加 VUE_CODE_STYLE_GUIDE.md 文件,包含以下内容:
1. 梳理当前项目 Vue 开发方式
2. 提供 2024-2025 Vue3 生态推荐写法
3. 分析项目现状与改进建议
4. 给出可直接落地的优化清单
5. 包含推荐代码示例与分层实践
1 +# Vue3 项目代码风格与最佳实践(结合本项目)
2 +
3 +本文件用于梳理本项目当前的 Vue 开发方式,并给出更贴近 2024-2025 Vue3 生态的写法与取舍建议,重点关注:可读性、复用性、可维护性、边界清晰。
4 +
5 +## 0. 本项目的“硬约束”(先对齐再谈最佳实践)
6 +
7 +以下是从项目现状与工程配置中抽出来的约束,建议把它们视为默认前提:
8 +
9 +- 技术栈:Vue3 + Vite + Vue Router + Vant + TailwindCSS + Less
10 +- 语言:以 JS 为主(项目存在 `.d.ts` 文件但业务代码不写 TS)
11 +- 样式:优先 TailwindCSS 布局;需要补充时用 Less(层级嵌套)
12 +- 请求约定:建议接口层始终返回 `{ code, data, msg }``code === 1` 表示成功)
13 +- 风格落地:建议用工具固化缩进、分号、命名等(否则靠“人肉自律”会越来越散)
14 +
15 +## 1. 结论(先给答案)
16 +
17 +本项目整体开发模式是“Vue3 + Vite + Router + Vant/Tailwind + 业务 API 分层”,整体符合当前 Vue3 工程化的主流做法,尤其是:
18 +
19 +- 大量页面采用 Composition API 与 `<script setup>`,属于 Vue 官方推荐写法之一(官方明确推荐在 SFC + Composition API 场景使用 `<script setup>`)。
20 +- 路由层使用动态 import 做懒加载,符合 SPA 常见优化路径。
21 +- 有明显的组件复用意识:例如把多处重复 UI 抽到 `/src/components/ui`
22 +
23 +但也存在一些会影响“长期维护成本”的点,主要集中在:
24 +
25 +- 代码风格不够统一(命名、缩进、分号、注释语言、函数声明方式混用)。
26 +- 页面文件体积偏大(一个页面承载了过多业务 + UI + 请求 + 状态),导致复用与测试难。
27 +- 状态管理存在“上下文 context + localStorage + axios 默认头”的多头来源,边界不清晰时容易出一致性问题。
28 +- API 返回值语义不够稳定(有的地方返回对象,有的地方返回 `false`),使上层调用必须加很多防御判断。
29 +
30 +如果你的目标是“最大限度解耦”,实际工程里通常会走向过度抽象;更推荐的方向是:
31 +
32 +- 以“高内聚 + 边界清晰”为第一目标;
33 +- 用“可读性”作为默认优先级;
34 +- 在可读性不牺牲的前提下,通过 composables / 组件 / store 做复用;
35 +- 尽量把“副作用”关进可控的层(请求、缓存、路由跳转、埋点等)。
36 +
37 +## 2. 本项目已做得比较好的部分(可继续保持)
38 +
39 +### 2.1 使用 `<script setup>` 的页面组织方式
40 +
41 +示例:[timeline.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/recall/timeline.vue)
42 +
43 +- 数据与 UI 绑定直接、模板可读性较强。
44 +- 通过 `ref/computed/onMounted` 把“同一功能”的逻辑写在相近位置,这就是 Composition API 的核心优势:按功能聚合,而不是按 options 分散。
45 +
46 +### 2.2 API 层集中管理与统一入口
47 +
48 +示例:
49 +
50 +- [fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
51 +- [axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js)
52 +
53 +优点:
54 +
55 +- API 地址集中在同文件常量里,调用点清爽。
56 +- axios 拦截器把 401 策略收口在一处,避免业务页面到处写重复逻辑。
57 +
58 +### 2.3 有“抽离复杂逻辑到 utils”的意识
59 +
60 +示例:[qiniuFileHash.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/qiniuFileHash.js)
61 +
62 +- 逻辑复杂但相对内聚,能独立于 UI 存在,这种拆分方向非常正确。
63 +
64 +## 3. 与最新 Vue3 主流实践的差距(建议优先优化的点)
65 +
66 +### 3.1 API 返回值不稳定:对象 vs false
67 +
68 +现状(示例):[fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
69 +
70 +- 成功:返回 `res.data`
71 +- 失败:返回 `false`
72 +
73 +问题:
74 +
75 +- 上层 `await api()` 后需要到处判断 `res && res.code`,否则很容易出现 `Cannot read properties of false`
76 +- “失败原因”在上层丢失(只知道 false),难以做统一的错误展示、上报、降级。
77 +
78 +推荐方向:
79 +
80 +- 让所有 API 始终返回 `{ code, data, msg }` 结构(即使失败也返回),上层永远用 `if (res.code === 1)` 判断。
81 +- UI toast 是否弹出由“页面层或 composable”控制,不建议在底层 `fn` 里默认弹 toast(否则调用方无法做静默失败、重试等策略)。
82 +
83 +### 3.2 状态来源过多:localStorage / axios 默认头 / context
84 +
85 +现状:
86 +
87 +- axios 请求拦截器每次都读 `localStorage.user_info` 再写 headers(见 [axios.js](file:///Users/huyirui/program/itomix/git/mlaj/src/utils/axios.js))。
88 +- 业务又有 `contexts/auth``currentUser`(见 [App.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/App.vue) 与相关 contexts)。
89 +
90 +问题:
91 +
92 +- “当前用户是谁”到底以哪份为准?一旦出现并发更新,很难排查。
93 +
94 +推荐方向(渐进):
95 +
96 +- 选择一个“单一事实来源”(推荐 Pinia store 或你现在的 context 之一),并定义清晰的数据流:
97 + - store/context 是运行时状态
98 + - localStorage 是持久化镜像(启动时 hydrate,一处写入)
99 + - axios headers 从 store/context 派生(store 更新 -> 同步更新 headers)
100 +
101 +### 3.3 页面体积偏大:建议用 composables “分层”
102 +
103 +现状示例:[HomePage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/HomePage.vue)(文件较大)
104 +
105 +常见风险:
106 +
107 +- 页面把“请求、数据适配、交互状态、UI”都堆在一起,后续加需求会越来越难动。
108 +
109 +推荐方向:
110 +
111 +- 页面只做“编排”:把具体业务逻辑下沉到 composable,例如 `useCourseList()``usePurchaseFlow()`
112 +- composable 负责:请求 + 数据适配 + 状态管理 + 副作用封装(可选)。
113 +- 组件负责:UI(最好尽量无接口依赖)。
114 +
115 +### 3.4 风格统一性不足(这会真实影响团队协作)
116 +
117 +你项目里同时存在:
118 +
119 +- 分号有/无混用
120 +- 2 空格 / 4 空格缩进混用
121 +- function / arrow function 混用
122 +- 中文/英文注释混用
123 +
124 +这类问题不会立刻出 bug,但会显著降低代码可扫描性,团队规模越大越痛。
125 +
126 +推荐做法:
127 +
128 +- 用 ESLint/Prettier 固化风格(如果不想引入 Prettier,也至少把 ESLint 规则开起来,并且在 CI 或提交前跑)。
129 +
130 +## 4. 2024-2025 Vue3 “最新主流写法”长什么样(精简版)
131 +
132 +### 4.1 组件:优先 `<script setup>` + “按功能聚合”
133 +
134 +Vue 官方建议在 SFC + Composition API 场景使用 `<script setup>`,因为更简洁、模板与逻辑处于同一作用域、性能与 IDE 推断体验更好。
135 +
136 +参考:
137 +
138 +- Vue `<script setup>` 文档:https://vuejs.org/api/sfc-script-setup.html
139 +- Vue `setup()` 文档:https://vuejs.org/api/composition-api-setup.html
140 +
141 +### 4.2 复用:优先 composables,而不是 mixins
142 +
143 +- 复用业务逻辑:`/src/composables/useXxx.js`
144 +- 复用 UI:`/src/components/ui/Xxx.vue`
145 +- 复用纯函数:`/src/utils/xxx.js`
146 +
147 +一个简单的判断原则:
148 +
149 +- 需要响应式状态与生命周期 -> composable
150 +- 纯入参出参,不需要 Vue 能力 -> utils
151 +- 需要渲染 -> component
152 +
153 +### 4.3 状态管理:Pinia 是 Vue3 官方推荐路线之一
154 +
155 +如果你需要跨页面共享且复杂的状态(用户、购物车、多步骤流程),Pinia 往往比 provide/inject 更适合长期维护。
156 +
157 +补充(结合本项目现状):
158 +
159 +- 本项目目前未引入 Pinia(`package.json` 里没有 `pinia`),因此短期内更现实的路径是:先把现有 provide/inject + localStorage + axios headers 的边界收敛清楚;如果后续状态继续变复杂,再渐进引入 Pinia。
160 +
161 +参考:
162 +
163 +- Vue 官方脚手架 create-vue 会提供“是否选择 Pinia”作为默认选项之一:https://vuejs.org/guide/quick-start.html
164 +- Pinia 关于 composables 的实践:https://pinia.vuejs.org/cookbook/composables.html
165 +
166 +### 4.4 Vue 3.3-3.5 常用能力(了解即可,按需引入)
167 +
168 +- `defineOptions()`:在 `<script setup>` 中补充少量 Options API 选项(如 `inheritAttrs`),避免回退到普通 `<script>` 写法(见 Vue 文档同页说明)。
169 +- `defineModel()`:更清晰地表达组件的 `v-model`,适合做“可复用表单组件/弹窗组件”。
170 +- 响应式 props 解构(3.5+):在需要时可减少 `props.xxx` 的噪音,但要注意可读性,不建议滥用。
171 +
172 +## 5. 结合本项目:一套更“清晰边界”的推荐分层
173 +
174 +建议你把前端逻辑按“层”来想(从上到下):
175 +
176 +1) View(页面)
177 + - 只做页面编排:拿数据、传 props、处理路由跳转
178 +2) Composable(业务逻辑)
179 + - 负责:请求、状态、数据适配、交互流程(可测试)
180 +3) Service/API(接口层)
181 + - 负责:请求与返回结构,尽量不处理 UI(不弹 toast)
182 +4) Utils(纯工具)
183 + - 负责:纯函数、格式化、兼容处理
184 +
185 +## 6. 结合本项目:可直接落地的优化清单(按收益排序)
186 +
187 +1) 统一 API 返回结构:去掉 `false` 返回,调用侧统一 `res.code === 1`
188 +
189 +- 目标:减少大量 `if (res && res.code)` 的防御判断,把错误信息(msg)保留下来
190 +- 涉及文件:[fn.js](file:///Users/huyirui/program/itomix/git/mlaj/src/api/fn.js)
191 +- 现有风险:部分页面写法是 `if (res.code) { ... }`,当 `code=401` 等非 0 值时会被当成“成功”分支;建议统一改成 `if (res.code === 1)`
192 +- 现有风险:`qs` 的包名是小写 `qs`,但当前存在 `import qs from 'Qs'` 写法,在大小写敏感环境下可能无法解析(建议统一为 `qs`)。
193 +
194 +2) 收敛“用户态”的单一事实来源:context/store 负责运行时,localStorage 负责持久化镜像
195 +
196 +- 目标:避免 `currentUser``user_info`、请求头三套来源互相覆盖导致的“偶发态”
197 +- 涉及文件:[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)
198 +
199 +3) 从 1 个大页面开始拆 composable:只拆“最独立的一块功能”
200 +
201 +- 目标:不追求一次性完美分层,但要让页面先变薄,后续才好迭代与测试
202 +- 典型样例:[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)
203 +
204 +4) 清理“重复/并存”的实现:避免同名组件在不同目录各自演进
205 +
206 +- 目标:减少认知负担与误用风险(例如存在两个 `AppLayout`
207 +- 涉及文件:[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)
208 +
209 +5) 降低“语法形态”混用成本:能不用 JSX 就别用
210 +
211 +- 目标:减少团队理解成本(JSX + 模板混用会让页面维护难度上升),也减少对 `defineComponent/h` 等写法的依赖
212 +- 涉及文件(存在 `<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)
213 +
214 +## 7. 推荐写法示例(以“无 TS、可读性优先”为前提)
215 +
216 +下面代码用于学习“结构”,不要求你立刻全量改造。
217 +
218 +### 7.1 API:始终返回统一结构(不返回 false)
219 +
220 +```js
221 +/**
222 + * @typedef {Object} ApiResult
223 + * @property {number} code - 1 表示成功,其他表示失败
224 + * @property {any} data - 返回数据
225 + * @property {string} msg - 返回信息
226 + */
227 +
228 +/**
229 + * @description 把任意接口结果规范化为统一结构
230 + * @param {any} raw - 原始响应
231 + * @returns {ApiResult}
232 + */
233 +export const normalize_api_result = (raw) => {
234 + const data = raw?.data ?? raw ?? {}
235 + return {
236 + code: Number(data.code) || 0,
237 + data: data.data ?? null,
238 + msg: data.msg ?? ''
239 + }
240 +}
241 +```
242 +
243 +### 7.2 Composable:只暴露页面真正需要的状态与动作
244 +
245 +```js
246 +import { ref } from 'vue'
247 +import { showFailToast } from 'vant'
248 +
249 +/**
250 + * @description 示例:封装一个“加载列表”的业务逻辑
251 + * @param {Function} fetch_list_api - 具体接口函数
252 + * @returns {{ loading: import('vue').Ref<boolean>, list: import('vue').Ref<any[]>, load: Function }}
253 + */
254 +export const use_list_loader = (fetch_list_api) => {
255 + const loading = ref(false)
256 + const list = ref([])
257 +
258 + const load = async (params) => {
259 + loading.value = true
260 + try {
261 + const res = await fetch_list_api(params)
262 + if (res.code === 1) {
263 + list.value = Array.isArray(res.data?.list) ? res.data.list : []
264 + return true
265 + }
266 + showFailToast(res.msg || '加载失败')
267 + return false
268 + } finally {
269 + loading.value = false
270 + }
271 + }
272 +
273 + return {
274 + loading,
275 + list,
276 + load
277 + }
278 +}
279 +```
280 +
281 +### 7.3 页面:只做编排(不写复杂流程)
282 +
283 +```vue
284 +<script setup>
285 +import { onMounted } from 'vue'
286 +import { useTitle } from '@vueuse/core'
287 +import { getNewsListAPI } from '@/api/news'
288 +import { use_list_loader } from '@/composables/use_list_loader'
289 +
290 +useTitle('消息列表')
291 +
292 +const { loading, list, load } = use_list_loader(getNewsListAPI)
293 +
294 +onMounted(() => {
295 + load({ page: 1, limit: 10 })
296 +})
297 +</script>
298 +```
299 +
300 +## 8. 建议你“下一步优先做”的 3 件事(投入产出比最高)
301 +
302 +1) 统一 API 返回结构(去掉 `false` 返回),减少上层防御代码与隐性 bug。
303 +2) 把 1-2 个大页面拆出 composable(例如 HomePage 中的某一块功能),你会立刻感受到可读性提升。
304 +3) 固化格式化规则(ESLint/Prettier 选一个),把“风格争议”交给工具。