把状态写进 URL,页面不需要 Vuex 也能完美同步

382 阅读3分钟

我们有个项目,在几个页面之间频繁同步筛选状态:比如商品分类、价格区间、排序方式、分页……一开始用的是组件通信 + 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 是浏览器级的状态容器,利用好它,能让前端的用户体验更自然、代码更清晰。

如果你也遇到类似问题,请留言🙂

📌 你可以继续看我的系列文章