我们有个项目,在几个页面之间频繁同步筛选状态:比如商品分类、价格区间、排序方式、分页……一开始用的是组件通信 + Pinia,全局 store 越写越大,维护起来越来越乱。
直到我们开始把状态写进 URL,才彻底摆脱了状态管理工具。
什么叫“写进 URL”?
举个例子:
https://example.com/products?category=shoe&price=100-200&sort=hot&page=2
这串 query string 本质上就描述了当前页面的状态。用户刷新页面、复制链接、用浏览器前进后退,状态都能完整保留。
我们把这个能力叫做:URL 状态化。
为什么这么做?
传统的状态管理工具像 Vuex / Pinia,适合维护组件间共享状态,但一旦涉及浏览器的“历史记录”、SEO、分享、刷新恢复等需求,就不得不额外维护一套状态同步机制。
与其双向同步,不如直接把状态藏在 URL:
- 浏览器原生支持导航控制
- SSR、分享、前进后退都天然支持
- 没有额外状态源,逻辑更清晰
以 Vue3 + Vue Router 为例
我们封装了一个 composable:useUrlState()
import { useRoute, useRouter, watch } from 'vue-router'
import { ref, watchEffect } from 'vue'
function useUrlState<T extends Record<string, any>>(defaults: T) {
const route = useRoute()
const router = useRouter()
const state = ref({ ...defaults, ...route.query })
watch(state, (val) => {
router.replace({ query: val })
}, { deep: true })
watch(route, () => {
state.value = { ...defaults, ...route.query }
})
return state
}
用法:
const filters = useUrlState({ category: '', price: '', sort: 'hot', page: 1 })
然后在模板中双向绑定:
<select v-model="filters.category">
<option value="shoe">鞋子</option>
<option value="hat">帽子</option>
</select>
一切行为都自动映射到 URL,不需要再额外 setStore / getStore / resetFilter。
一些实际踩坑
1. 页面状态不规范时,容易污染 URL
解决方案:加一个 sanitizeQuery()
function sanitizeQuery(q: any) {
const result = {} as any
if (typeof q.page === 'string') result.page = parseInt(q.page)
if (q.category) result.category = q.category
return result
}
2. 需要 debounce,避免 URL 被快速连续更改
我们加了一个节流操作:
let timeout: any
watch(state, (val) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
router.replace({ query: val })
}, 200)
})
对比 Pinia 或 Vuex,有哪些优劣?
| 功能 | URL 状态 | Vuex / Pinia |
|---|---|---|
| 页面刷新后保留 | ✅ | ❌(除非持久化) |
| 分享链接可还原 | ✅ | ❌ |
| 浏览器前进/后退可追踪 | ✅ | ❌ |
| 适合复杂业务逻辑 | ❌ | ✅ |
| SSR SEO 友好 | ✅ | ❌ |
所以我们的结论是:
页面级筛选条件、分页、排序类状态,优先使用 URL 状态化;而组件通信、用户权限类状态,继续使用 store。
现在我们新建业务页面的第一条原则是:不要急着建 store,看状态能不能直接写进 URL。
URL 是浏览器级的状态容器,利用好它,能让前端的用户体验更自然、代码更清晰。
如果你也遇到类似问题,请留言🙂