「VueUse」:解读 useUrlSearchParams

1,778 阅读5分钟

本文正在参加「金石计划」

代码未动,设计先行

一个 URL 对开发者来讲,最重要的就是 URL 中携带的参数,根据使用场景不同,一般有两个作用,以下面 URL 为例:

https://vueuse.org/?nickName=CondorHero&age=18
  1. 读取,获取 URL 参数做一些判断,比如 age > 18
  2. 写入,更改 URL 中的参数,比如 nickName=Shavahn

所以我们设计的 useUrlSearchParams 应该是这样的:

const params = useUrlSearchParams();

与此同时,URL 携带参数分为两种:

  1. search 模式,比如:https://vueuse.org/?nickName=CondorHero&age=18
  2. hash 模式,比如:https://vueuse.org/#nickName=CondorHero&age=18

对于 search 模式,浏览器提供了 URLSearchParams API,可以把 nickName=CondorHero&age=18 变成一个可操作的对象,但是对于 hash 浏览器则没有提供像 URLSearchParams 一样好用的 API 来解析 nickName=CondorHero&age=18,但我们可以仿照 URLSearchParams 自己实现。

现在给设计的 useUrlSearchParams 函数添加一个参数 mode,表示当前采用的模式,模仿 VueRouter 的 createWebHistory 路由,我们给 search 格式的路由命名为 history 同时作为默认值,可选 hash:

const params = useUrlSearchParams("history");

此时 useUrlSearchParams 返回的 params 应该是一个响应式的对象,以便开发者读取和写入 URL 的参数。

设计出大致轮廓就可以开干了,遇到问题再解决问题。

定义 useUrlSearchParams 函数

export function useUrlSearchParams(
	mode: "history" | "hash" = "history"
) {}

mode 表示路由模式,其中默认值为 history。

响应式路由状态

定义 state,最终会把路由参数存在其中,以供外部读取和修改。

export function useUrlSearchParams(
	mode: "history" | "hash" = "history"
) {
    const state = reactive<Record<string, any | any[]>>({});
    return state;
}

相信你注意到这里 state 的类型为:Record<string, any | any[]>;对象的值不仅可以单独存在,还可以为数组。

我们常见的 URL 都是一个字段对应一个基本类型,比如:

http://localhost:5173/?name=CondorHero&age=18&hobby=basketball

转成对象就是:

{
    "name": "CondorHero",
    "age": 18,
    "hobby": "basketball",
}

人的爱好 hobby 可能不止一个,所以 URL 中的 hobby 还可以给出多个:

http://localhost:5173/?name=CondorHero&age=18&hobby=basketball&hobby=football

转成对象就是:

{
    "name": "CondorHero",
    "age": 18,
    "hobby": [ "basketball", "football" ],
}

利用 location 获取 URL 参数

const getRawParams = () => {
	if (mode === "history") {
		return window.location.search;
	} else {
		return window.location.hash.replace(/^#/, "?");
	}
}
  • window.location.search 获取的格式为:?name=CondotHero&age=18
  • window.location.hash 获取的格式为:#name=CondotHero&age=18

之所以要使用 replace 把 # 替换掉,是因为 URLSearchParams 无法解析它。

序列化 getRawParams

使用 URLSearchParams 把 getRawParams 读取的值变成编程可维护的状态。

const read = () => {
        return new URLSearchParams(getRawParams());
}

更新 state

return state 之前,更新 state 整个读取过程就完成了。

updateState(read());
return state;

updateState 的逻辑如下:

const updateState = (params: URLSearchParams) => {
        for (const key of params.keys()) {
                const paramsForKey = params.getAll(key)
                state[key] = paramsForKey.length > 1
                        ? paramsForKey
                        : (params.get(key) ?? "")
        }
}

这里唯一需要注意的是判断值,多个就作为数组,一个就不是数组。getAll 则可以获取 key 对应的所有得知,get 只会获取第一个出现的值。

演示值得读取

vue 组件中引入 useUrlSearchParams:

const params = useUrlSearchParams("history");

URL 为 http://localhost:5173/?name=CondorHero&age=18 显示结果如下:

image.png

URL 的写入

const params = useUrlSearchParams("history");

useUrlSearchParams 返回的 params 现在能够读取,作为一个响应式对象,它应该还支持写入,比如:

params.name = Shavahn;
params.age = 19;

写入的实现也就有思路了,只需要用 watch 监听 state 的改变,然后把 state 的值重新写入 URL 中。

watch(
	state,
	() => {
		const params = new URLSearchParams();
		Object.keys(state).forEach((key) => {
			const mapEntry = state[key]
			if (Array.isArray(mapEntry)){
				mapEntry.forEach(value => params.append(key, value))
			} else {
				params.set(key, mapEntry)
			}
		})
		write(params)
	},
	{ deep: true },
);

这里需要注意的一点路由参数如果是多个,也就是数组需要 append 添加,如果用 set 会覆盖。

更新 URL 逻辑放入到了 write 函数。

更新 URL

注意这里用的是 replaceState 表示替换当前路由,一般来讲,你只有在进入页面的时候等极少情况下修改 searchparams,大多数时候使用 vue-router 或者浏览器本身提供的路由就行了。

所以这里用 replaceState 重写当前路由是合适的。

const write = (params: URLSearchParams) => {

	window.history.replaceState(
		window.history.state,
		window.document.title,
		window.location.pathname + constructQuery(params),
	);

}

最后利用 constructQuery 拼出正确 URL:

const constructQuery = (params: URLSearchParams) => {
	const stringified = params.toString()

	if (mode === "history") {
		return `${stringified ? `?${stringified}` : ""}${window.location.hash}`
	}
	return `${window.location.search}${stringified ? `#${stringified}` : ""}`
}

演示写入

添加 popstate 事件

现在假如页面有一个标签:

<a href="#tab-one">Open Link: #tab-one</a>

添加 popstate 事件

点击之后页面肯定从 http://localhost:5173/#age=90&name=Jack 跳转到 http://localhost:5173/#tab-one,这个时候 params 也应该立即变化,像这种路由前进后退,监听它变化的事件就是 popstate:

const popstateEvent = () => {
        updateState(read());
}

onMounted(() => {
        window.addEventListener("popstate", popstateEvent)
})

onUnmounted(() => { window.reportError(popstateEvent) })

这个时候需要修改下 updateState 函数里面的逻辑,因为 state 对象上现在还有 name 和 age 字段,而新的 URL 已经没有了,所以再重新更新 state 的时候,应该把不存在的 key 删除:

const updateState = (params: URLSearchParams) => {
        const unUsedKeys = new Set(Object.keys(state));
        for (const key of params.keys()) {
                const paramsForKey = params.getAll(key)
                state[key] = paramsForKey.length > 1
                        ? paramsForKey
                        : (params.get(key) ?? "");
                unUsedKeys.delete(key);
        }
        Array.from(unUsedKeys).forEach(key => delete state[key]);
}

点击之后路由跳转之后的演示:

路由跳转之后的演示

添加默认值

可以利用 useUrlSearchParams 的第二个参数添加更多自定义行为,比如,进入页面默认添加些路由参数。

interface UseUrlSearchParamsOptions {
	initialValue?: Record<string, any | any[]>
}

export function useUrlSearchParams(
	mode: "history" | "hash" = "history",
	options: UseUrlSearchParamsOptions = {}
) {
    const { initialValue } = options;
}

接下来更改 updateState(read()) 的逻辑:

const initial = read();
if (initial.toString().length) {
        updateState(initial)
} else {
        Object.assign(state, initialValue)
}

如果路由有参数就不需要设置,否则使用默认参数。

最终效果

所有代码示例在此:

补充

VueUse 提供的 UrlSearchParams 和本例的 mode 有稍稍不同,VueUse 的 UrlSearchParams 支持三种模式:history、hash、hash-params。本例实现的 history 和 VueUse 是相同的,但 hash 模式实际上对应 VueUse 的 hash-params 模式。VueUse 的 hash,实际上是 history 和 hash-params 结合:

http://localhost:5173/#?age=90&name=Jack

如果你学会了本例讲解的两种模式,相信这个模式你也能轻松实现。

参考链接