VUE_CODE_STYLE_GUIDE.md 13.6 KB

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

  • 数据与 UI 绑定直接、模板可读性较强。
  • 通过 ref/computed/onMounted 把“同一功能”的逻辑写在相近位置,这就是 Composition API 的核心优势:按功能聚合,而不是按 options 分散。

2.2 API 层集中管理与统一入口

示例:

优点:

  • API 地址集中在同文件常量里,调用点清爽。
  • axios 拦截器把 401 策略收口在一处,避免业务页面到处写重复逻辑。

2.3 有“抽离复杂逻辑到 utils”的意识

示例:qiniuFileHash.js

  • 逻辑复杂但相对内聚,能独立于 UI 存在,这种拆分方向非常正确。

3. 与最新 Vue3 主流实践的差距(建议优先优化的点)

3.1 API 返回值不稳定:对象 vs false

现状(示例):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)。
  • 业务又有 contexts/authcurrentUser(见 App.vue 与相关 contexts)。

问题:

  • “当前用户是谁”到底以哪份为准?一旦出现并发更新,很难排查。

推荐方向(渐进):

  • 选择一个“单一事实来源”(推荐 Pinia store 或你现在的 context 之一),并定义清晰的数据流:
    • store/context 是运行时状态
    • localStorage 是持久化镜像(启动时 hydrate,一处写入)
    • axios headers 从 store/context 派生(store 更新 -> 同步更新 headers)

3.3 页面体积偏大:建议用 composables “分层”

现状示例: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 推断体验更好。

参考:

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。

参考:

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 (已做调整,在recall_users.js里面使用了request函数做了封装)

  • 目标:减少大量 if (res && res.code) 的防御判断,把错误信息(msg)保留下来
  • 涉及文件:fn.js
  • 现有风险:部分页面写法是 if (res.code) { ... },当 code=401 等非 0 值时会被当成“成功”分支;建议统一改成 if (res.code === 1)
  • 现有风险:qs 的包名是小写 qs,但当前存在 import qs from 'Qs' 写法,在大小写敏感环境下可能无法解析(建议统一为 qs)。

2) 收敛“用户态”的单一事实来源:context/store 负责运行时,localStorage 负责持久化镜像

  • 目标:避免 currentUseruser_info、请求头三套来源互相覆盖导致的“偶发态”
  • 涉及文件:auth.jsaxios.js

3) 从 1 个大页面开始拆 composable:只拆“最独立的一块功能”

4) 清理“重复/并存”的实现:避免同名组件在不同目录各自演进

5) 降低“语法形态”混用成本:能不用 JSX 就别用

  • 目标:减少团队理解成本(JSX + 模板混用会让页面维护难度上升),也减少对 defineComponent/h 等写法的依赖
  • 涉及文件(存在 <script setup lang="jsx">):HomePage.vueCoursesPage.vueCourseDetailPage.vue

7. 推荐写法示例(以“无 TS、可读性优先”为前提)

下面代码用于学习“结构”,不要求你立刻全量改造。

7.1 API:始终返回统一结构(不返回 false)

/**
 * @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:只暴露页面真正需要的状态与动作

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 页面:只做编排(不写复杂流程)

<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 选一个),把“风格争议”交给工具。