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”的意识
- 逻辑复杂但相对内聚,能独立于 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/auth的currentUser(见 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 推断体验更好。
参考:
- 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 (已做调整,在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 负责持久化镜像 (已做调整,在auth_user_info.js里面使用了getUserInfoFromStorage和removeUserInfoFromStorage函数做了封装)
3) 从 1 个大页面开始拆 composable:只拆“最独立的一块功能”
- 目标:不追求一次性完美分层,但要让页面先变薄,后续才好迭代与测试
- 典型样例:HomePage.vue、StudyDetailPage.vue
4) 清理“重复/并存”的实现:避免同名组件在不同目录各自演进
- 目标:减少认知负担与误用风险(例如存在两个
AppLayout) - 涉及文件:layouts/AppLayout.vue、components/layout/AppLayout.vue
5) 降低“语法形态”混用成本:能不用 JSX 就别用
- 目标:减少团队理解成本(JSX + 模板混用会让页面维护难度上升),也减少对
defineComponent/h等写法的依赖 - 涉及文件(存在
<script setup lang="jsx">):HomePage.vue、CoursesPage.vue、CourseDetailPage.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 选一个),把“风格争议”交给工具。