docs: 添加 Vue3 项目代码风格与最佳实践指南
添加 VUE_CODE_STYLE_GUIDE.md 文件,包含以下内容: 1. 梳理当前项目 Vue 开发方式 2. 提供 2024-2025 Vue3 生态推荐写法 3. 分析项目现状与改进建议 4. 给出可直接落地的优化清单 5. 包含推荐代码示例与分层实践
Showing
1 changed file
with
304 additions
and
0 deletions
VUE_CODE_STYLE_GUIDE.md
0 → 100644
| 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 选一个),把“风格争议”交给工具。 |
-
Please register or login to post a comment