跳到主要内容
Vabora 开发极客
首页 归档 分类 标签 案例 服务 关于 聊聊需求

Vue 3 组合式函数的组织方式:按能力切,不按组件切

后台有 20 多个页面,最容易失控的是"每个页面复制一套请求/分页/筛选逻辑"。用组合式函数按能力拆分后,列表页的公共逻辑收敛到三个函数。

Vue 3 组合式函数的组织方式:按能力切,不按组件切 封面

#背景

后台控制台有 20 多个列表页,最初的写法是每页各自 reactive 一套 filters、pagination、load()。三个月后的问题清单很典型:

  • 从列表点进详情再返回,筛选条件全丢,用户要重新选一遍;
  • 分页页码在刷新后回到第 1 页;
  • 窄屏下抽屉宽度写死像素,右侧被切掉;
  • 提交失败的错误字段回填逻辑,每个页面写法都不一样。

这些都不是"某个页面的 bug",而是缺少公共能力。

#做法:按能力切

composables/
├── useViewState.js    # 列表页的筛选 + 分页记忆(sessionStorage)
├── useViewport.js     # 视口判断(窄屏下的抽屉/表格策略)
└── useForm.js         # 提交、错误回填、脏标记

#1. useViewState:状态要能"记得住"

js
const { filters, pagination, reset, clearViewState } = useViewState('cases', {
  filters: { keyword: '', status: '' },
  pagination: { page: 1, per_page: 20 },
})

用 sessionStorage 而不是 localStorage:关掉标签页即清,不会让三天前的筛选条件阴魂不散地影响今天的判断。

一个必须处理的细节:从外部带参数跳转进来时以 URL 为准(比如从仪表盘点"待审核评论"跳到评论列表),此时要 clearViewState() 再应用 URL 参数,否则记忆值会盖掉 URL 参数,用户点了却看不到想看的。

#2. useViewport:窄屏判断集中一处

js
const { isMobile } = useViewport()

宽度用 matchMedia 而不是监听 resize:前者不会在拖拽窗口时触发几十次回调,也不需要手动清理。

抽屉宽度是它的第一个用武之地:isMobile ? '100%' : 720 —— 固定像素在手机上会让抽屉超出屏幕,而 fixed 定位的溢出不会出现在 scrollWidth 里,常规的溢出检查抓不到。

#3. useForm:错误回填统一

js
const { saving, errors, submit, serverFeedback } = useForm(api.saveCase)

后端返回的字段级错误({"title": "标题不能为空"})直接映射到表单上,前端校验规则与服务端规则共享同一份字段名。这个约定省掉了"前端显示的位置不对"这类来回改。

#命名与边界

三条约定:

  1. use 前缀 + 名词性能力名(useViewState、useViewport),不要用动词(useLoadList)——能力应该能描述"它给你什么",而不是"它做了什么";
  2. 返回值只暴露需要的:内部 ref 不要整个 return,避免调用方误改内部状态;
  3. 一个组合式函数只解决一件事:useViewState 不管请求,useForm 不管分页。需要组合时在页面里组合。

#结果

  • 列表页从"每页 40 行重复逻辑"降到"每页 8 行调用";
  • 筛选/分页记忆、窄屏适配、错误回填在全站一次性统一;
  • 新增列表页不再需要抄代码——抄代码会抄错,调用能力不会。

#取舍

  • 不引入 Pinia 存页面状态:页面级状态(筛选、分页)放在组合式函数 + sessionStorage 里足够;全局状态(当前用户、权限)才进 store。把页面状态塞进全局 store 会让"谁能改这个值"变得难以追踪。
  • 不做通用 CRUD 生成器:抽象层次越高,例外情况越难处理。宁可多写 8 行调用,也不要一个"配置驱动的万能表格"——那种东西改一次需求就要改生成器,最终没人敢动。
最后更新: 本文采用 CC BY-NC-SA 4.0 许可,转载请注明出处与作者。
related

相关文章

评论

还没有评论,来说点什么吧。

发表评论

评论需站长审核后展示 · 最多 1000 字