1. 背景
在中后台页面的前端开发中,我们可能会用表单来实现数据的过滤功能
如果我们的页面能够支持url查询参数与表单值之间的联动,无疑能够大大提升用户的使用体验
出于提高代码复用性的考虑,我们可以封装这么一个React Hooks,专门用于管理url查询参数
-
入参:由n个字符串(key)组成
-
返回值:元组,包含一个js对象 和 一个更新这个对象的setter方法
-
生命周期:
-
初始化:在页面初始化时,读取url查询参数作为js对象的初始值
-
更新:通过setter更新js对象后,同步更新到url查询参数,但不引起页面刷新
-
2. 方案设计
管理url查询参数的过程,实质就是url查询参数与js对象相互转换的过程 :
- 初始化:需要将url查询参数转换为js对象
- 更新:需要将js对象转换为url查询参数
2.1 将url查询参数转换为js对象
第1️⃣步:将url查询参数转换为URLSearchParams对象
这一步非常简单,我们只需要拿到当前页面的url,并调用URLSearchParams的构造函数即可
const urlSearchParams = new URLSearchParams(window.location.search)
第2️⃣步:将URLSearchParams对象转换为js对象
在实际转换之前,定义js对象的类型为Params
- 为了让函数以及Hooks的返回值有类型提示,我们用泛型Key来标声明key的类型
- 若单个key对应单个值,则js对象中的对应值为字符串
- 若单个key对应多个值,则js对象中的对应值为字符串数组
type Params<Key extends string> = Record<Key, string | string[] | undefined>;
接下来,我们就可以利用 URLSearchParams.entries() 这个迭代器去遍历所有的键值对:
- 若params[key]为空:说明这是第一个与key匹配的值,直接写入params[key]即可
- 若params[key]为字符串:说明这是第二个与key匹配的值,需要把params[key]改写为字符串数组
- 若params[key]为数组:说明这是第n个(n>2)与key匹配的值,需要把它放进数组里
封装函数transformURLSearchParams2Params如下:
function transformURLSearchParams2Params(urlSearchParams: URLSearchParams) {
const params: Params<string> = {};
for (const array of urlSearchParams.entries()) {
const key = array[0];
const value = array[1];
if (!params[key]) {
params[key] = value;
} else if (Array.isArray(params[key])) {
(params![key] as string[]).push(value);
} else {
params[key] = [params[key] as string, value];
}
}
return params;
}
2.2 将js对象转换为url查询参数
由于URLSearchParams无法正确地识别字符串数组,我们不再使用URLSearchParams作为这一类转换的媒介
const params = {
a: '1',
b: ['2', '3']
}
// 预期:URLSearchParams { 'a' => '1', 'b' => '2', 'b' => '3' }
// 现状:URLSearchParams { 'a' => '1', 'b' => '2,3' }
console.log(new URLSearchParams(params))
因此我们需要封装一个函数,将js对象转换为 形如{key1}={value1}&{key2}={value2} 的字符串
基于reduce遍历js对象
- 若params[key]为空:直接跳过
- 若params[key]为字符串:说明这个key对应单个值,正常拼接
- 若params[key]为数组:说明这个key对应多个值,需要利用reduce遍历数组进行拼接
封装函数transformParams2String如下:
function transformParams2String<Key extends string>(params: Params<Key>) {
return Object.keys(params).reduce((str, key) => {
const value = params[key] as string | string[] | undefined;
if (typeof value === 'string' && value) {
return str + `${key}=${value}&`;
}
if (Array.isArray(value) && value.length > 0) {
return str + value.reduce((prev, v) => prev + `${key}=${v}&`, '');
}
return str;
}, '');
}
2.3 逻辑封装
接下来我们需要将上述的转换过程封装到React Hook里面
-
初始化:
- 通过
transformURLSearchParams2Params方法得到全量的js对象 - 利用lodash的pick方法从全量的js对象中选出所需的键值对,作为js对象(命名为params)的初始值
- 通过
-
更新:监听params值的变化
- 将当前的全量js对象、与当前params进行合并
- 通过
transformParams2String转换成字符串 - 通过
history.pushState更新url查询参数
import { pick } from 'lodash'
import { useUpdateEffect } from 'ahooks'
function useUrlQueryParams<Key extends string>(
...keys: Key[]
): [Params<Key>, React.Dispatch<React.SetStateAction<Params<Key>>>] {
const urlSearchParams = new URLSearchParams(window.location.search)
const allParams = transformURLSearchParams2Params(urlSearchParams)
const [params,setParams] = useState<Params<Key>>(() => {
return pick(allParams, keys)
})
useUpdateEffect(() => {
const str = transformParams2String({
...allParams,
...params
})
window.history.pushState({}, '', `${window.location.pathname}?${str}`);
}, [params])
return [params, setParams]
}
完整代码如下:
import { pick } from 'lodash'
import { useUpdateEffect } from 'ahooks'
type Params<Key extends string> = Record<Key, string | string[] | undefined>;
function transformURLSearchParams2Params(urlSearchParams: URLSearchParams) {
const params: Params<string> = {};
for (const array of urlSearchParams.entries()) {
const key = array[0];
const value = array[1];
if (!params[key]) {
params[key] = value;
} else if (Array.isArray(params[key])) {
(params![key] as string[]).push(value);
} else {
params[key] = [params[key] as string, value];
}
}
return params;
}
function transformParams2String<Key extends string>(params: Params<Key>) {
let result = '';
for (const key in params) {
const value = params[key];
if (!value?.length) {
continue;
}
if (typeof value === 'string') {
result += `&${key}=${value}`;
} else {
result += value.reduce((pre, cur) => {
return pre + `&${key}=${cur}`;
}, '');
}
}
return result;
}
function useUrlQueryParams<Key extends string>(
...keys: Key[]
): [Params<Key>, React.Dispatch<React.SetStateAction<Params<Key>>>] {
const history = useHistory()
const urlSearchParams = new URLSearchParams(window.location.search)
const allParams = transformURLSearchParams2Params(urlSearchParams)
const [params,setParams] = useState<Params<Key>>(() => {
return pick(allParams, keys)
})
useUpdateEffect(() => {
const str = transformParams2String({
...allParams,
...params
})
window.history.pushState({}, '', `${window.location.pathname}?${str}`);
}, [params])
return [params, setParams]
}
2.4 使用示例
以arco-design为例:
- 把params传递给Form组件的initialValues属性,作为表单的初始值
- 通过Form组件的onValuesChange监听到表单值变化时,调用setParams把更新同步到url查询参数
import React from 'react'
import { Form, Select } from '@arco-design/web-react'
import { useUpdateEffect } from 'ahooks'
export function App() {
const [form] = Form.useForm<{city: string}>()
const [params, setParams] = useUrlQueryParams('city')
useUpdateEffect(() => {
// 表单值变化时,重新请求数据
featchData()
}, [params])
return (
<div>
<Form
form={form}
initialValues={{
city: Array.isArray(params.city) ? params.city?.[0] : params.city
}}
onValuesChange={(_, values) => {
setParams(values)
}}
>
<Form.Item label='城市' field='city'>
<Select options={['Beijing', 'Shanghai']} />
</Form.Item>
</Form>
<Table data={[]} />
</div>
)
}