---
title: Vue 3 组合式函数的组织方式：按能力切，不按组件切
url: https://vabora.dev/posts/vue3-composables-patterns
author: Vabora
published: 2026-08-02 10:00:00
updated: 2026-09-27 10:33:42
category: 技术分享
tags: 架构设计, 前端, Vue 3
canonical: https://vabora.dev/posts/vue3-composables-patterns
license: 转载与引用请注明出处并保留原文链接
---

# Vue 3 组合式函数的组织方式：按能力切，不按组件切

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

后台控制台有 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 行调用，也不要一个"配置驱动的万能表格"——那种东西改一次需求就要改生成器，最终没人敢动。
