【react.js + hooks】useUrl 监听路由参数
本节我们来实现一个监听并解析 URL 参数的 hook:useUrl。而且这个 hook 的返回类型是可推断的。
实现思路
- 监听 URL 变化 - 事件监听
- 根据 URL 地址获取参数并返回 - 依赖工具方法
- 推断参数结构 - 泛型参数(对象式 & 模板式)
- 返回参数 - 返回解析好的参数,并合并 location 和 history 以提供更多功能
监听 URL
监听 popstate 事件即可,注意因为是全局监听,创建一个总事件。
// 全局的事件监听器
const listeners = new Set<Function>();window.addEventListener("popstate", () => {// do somethinglisteners.forEach((listener) => listener());
});
解析参数
使用内置的 decodeURIComponent
解析参数即可:
- 后面几个参数是对解析的细节配置
- mode - 分了两种解析模式
- string - 全解析为字符串
- auto - 智能解析
- autoParams - 指定被智能解析的字段
- stringifyParams - 指定被解析为字符串的字段
- custom - 自定义参数解析的映射配置
- mode - 分了两种解析模式
function getParams<T>(url: string,mode: "string" | "auto" = "auto",autoParams: (keyof T | (string & {}))[] = [],stringifyParams: (keyof T | (string & {}))[] = [],custom: { [K in keyof T]?: (value: string | undefined) => any } = {}
) {const params: {[key: string]: string | number | boolean | null | undefined;} = {};// 先处理 custom 对象for (const key in custom) {const value = new URLSearchParams(url).get(key);params[key] = custom[key as keyof T]?.(value ?? undefined);}const questionMarkIndex = url.indexOf("?");if (questionMarkIndex !== -1) {const queryString = url.substring(questionMarkIndex + 1);const pairs = queryString.split("&");for (const pair of pairs) {const [key, value] = pair.split("=");try {const decodedKey = decodeURIComponent(key);const decodedValue = decodeURIComponent(value);if (custom[decodedKey as keyof T]) {continue; // 如果这个键在 custom 对象中,我们已经处理过它了}if (stringifyParams.includes(decodedKey)) {params[decodedKey] = decodedValue;} else if (autoParams.includes(decodedKey) || mode === "auto") {if (decodedValue === "true") {params[decodedKey] = true;} else if (decodedValue === "false") {params[decodedKey] = false;} else if (decodedValue === "null") {params[decodedKey] = null;} else if (decodedValue === "undefined") {params[decodedKey] = undefined;} else if (!isNaN(Number(decodedValue))) {params[decodedKey] = Number(decodedValue);} else {params[decodedKey] = decodedValue;}} else {params[decodedKey] = decodedValue;}} catch (error) {console.error("Failed to decode URL parameter:", error);}}}return params as T;
}
类型推断
繁琐的类型体操,Github ts 练习中的 ParseQueryString 魔改升级版,加入了一些解析配置的泛型参数,以支持尽可能细致的类型推断(看看就好,工作中不建议写,费时间且头大,虽然写完后用着很舒服…),代码见完整实现。
完整实现
import { useState, useEffect, useMemo } from "react";
import { ApplyMode, ParseQueryString, Prettify } from "./types";type UrlInfo<T extends Record<string, any>> = {readonly params: Prettify<Readonly<T>>;readonly name?: string;
} & Location &History;type UrlChangeCallback<T extends Record<string, any>> = (urlInfo: UrlInfo<T>
) => void;function getParams<T>(url: string,mode: "string" | "auto" = "auto",autoParams: (keyof T | (string & {}))[] = [],stringifyParams: (keyof T | (string & {}))[] = [],custom: { [K in keyof T]?: (value: string | undefined) => any } = {}
) {const params: {[key: string]: string | number | boolean | null | undefined;} = {};// 先处理 custom 对象for (const key in custom) {const value = new URLSearchParams(url).get(key);params[key] = custom[key as keyof T]?.(value ?? undefined);}const questionMarkIndex = url.indexOf("?");if (questionMarkIndex !== -1) {const queryString = url.substring(questionMarkIndex + 1);const pairs = queryString.split("&");for (const pair of pairs) {const [key, value] = pair.split("=");try {const decodedKey = decodeURIComponent(key);const decodedValue = decodeURIComponent(value);if (custom[decodedKey as keyof T]) {continue; // 如果这个键在 custom 对象中,我们已经处理过它了}if (stringifyParams.includes(decodedKey)) {params[decodedKey] = decodedValue;} else if (autoParams.includes(decodedKey) || mode === "auto") {if (decodedValue === "true") {params[decodedKey] = true;} else if (decodedValue === "false") {params[decodedKey] = false;} else if (decodedValue === "null") {params[decodedKey] = null;} else if (decodedValue === "undefined") {params[decodedKey] = undefined;} else if (!isNaN(Number(decodedValue))) {params[decodedKey] = Number(decodedValue);} else {params[decodedKey] = decodedValue;}} else {params[decodedKey] = decodedValue;}} catch (error) {console.error("Failed to decode URL parameter:", error);}}}return params as T;
}// 全局的事件监听器
const listeners = new Set<Function>();window.addEventListener("popstate", () => {listeners.forEach((listener) => listener());
});/*** ## useUrl hook* Converts a string to a query parameter object. Return an object merged with location, history, params and name.** ### Parameters* - callback (?) - The **callback** to call when the url changes.* - name (?) - The name of the listener* - immediate (`false`) - Whether to call the callback immediately.* - config (?) - The configuration of the params parser.* + mode (`"auto"`) - The mode of the params parser: `"string"` | `"auto"` = `"auto"`.* + autoParams (?) - The parameters to treat as auto.* + stringifyParams (?) - The parameters to treat as string.* + custom (?) - The custom parser of certain query parameters.** ### Type Parameters* - T - `string` or `object`.* + The string to convert, like `"http://localhost?id=1&name=evan"`* + object: object to inferred as, like `{ id: 1, name: "evan" }`* - Mode - The mode to use when converting: `"string"` | `"fuzzy"` | `"auto"` | `"strict"` | `"any"` = `"auto"`.* - StrictParams - The parameters to treat as strict.* - FuzzyParams - The parameters to treat as fuzzy.** ### Notes* - Type infer mode is not associated with the mode parameter of parser.** @return location merged with history, params and name.*/
function useUrl<T extends Record<string, any> | string,Mode extends "any" | "fuzzy" | "auto" | "auto" | "strict" = "auto",StrictParams extends string[] = [],FuzzyParams extends string[] = []
>(callback?: UrlChangeCallback<Partial<T extends string? ParseQueryString<T, Mode, StrictParams, FuzzyParams>: ApplyMode<T, Mode, StrictParams, FuzzyParams>>>,name?: string,immediate?: boolean,config: {mode?: "string" | "auto";autoParams?: (| keyof (T extends string ? ParseQueryString<T> : ApplyMode<T>)| (string & {}))[];stringifyParams?: (| keyof (T extends string ? ParseQueryString<T> : ApplyMode<T>)| (string & {}))[];custom?: {[K in keyof (T extends string ? ParseQueryString<T> : ApplyMode<T>)]?: (value: string | undefined) => any;};} = {}
): UrlInfo<Partial<T extends string? ParseQueryString<T, Mode, StrictParams, FuzzyParams>: ApplyMode<T, Mode, StrictParams, FuzzyParams>>
> {function getUrlInfo() {return {params: getParams(window.location.href,config?.mode,config?.autoParams,config?.stringifyParams,config?.custom),name: name,...window.location,...window.history,};}const [urlInfo, setUrlInfo] = useState<UrlInfo<T extends string? ParseQueryString<T, Mode, StrictParams, FuzzyParams>: ApplyMode<T, Mode, StrictParams, FuzzyParams>>>(getUrlInfo() as any);const memoizedConfig = useMemo(() => config,[config.mode, config.autoParams, config.stringifyParams, config.custom]);useEffect(() => {if (immediate) {const urlInfo = getUrlInfo();callback?.(urlInfo as any);setUrlInfo(urlInfo as any);}}, [immediate, JSON.stringify(memoizedConfig), name]);useEffect(() => {const handlePopState = () => {const urlInfo = getUrlInfo();setUrlInfo(urlInfo as any);callback?.(urlInfo as any);};// 在组件挂载时注册回调函数listeners.add(handlePopState);return () => {// 在组件卸载时注销回调函数listeners.delete(handlePopState);};}, [callback]);return urlInfo as any;
}export default useUrl;
types:
/*** Converts a string to a query parameter object.* ### Parameters* - S - The string to convert, like `"http://localhost?id=1&name=evan"`.* - Mode - The mode to use when converting: `"string"` | `"fuzzy"` | `"auto"` | `"strict"` | `"any"` = `"auto"`.** - StrictParams - The parameters to treat as strict.** - FuzzyParams - The parameters to treat as fuzzy.** @return A query parameter object*/
export type ParseQueryString<S extends string,Mode extends "string" | "fuzzy" | "auto" | "strict" | "any" = "auto",StrictParams extends string[] = [],FuzzyParams extends string[] = []
> = Prettify<S extends `${infer _Prefix}?${infer Params}`? Params extends ""? {}: MergeParams<SplitParams<Params>, Mode, StrictParams, FuzzyParams>: MergeParams<SplitParams<S>, Mode, StrictParams, FuzzyParams>
>;type SplitParams<S extends string> = S extends `${infer E}&${infer Rest}`? [E, ...SplitParams<Rest>]: [S];type MergeParams<T extends string[],Mode extends "string" | "fuzzy" | "auto" | "strict" | "any" = "auto",StrictParams extends string[] = [],FuzzyParams extends string[] = [],M = {}
> = T extends [infer E, ...infer Rest extends string[]]? E extends `${infer K}=${infer V}`? MergeParams<Rest,Mode,StrictParams,FuzzyParams,SetProperty<M, K, V, Mode, StrictParams, FuzzyParams>>: E extends `${infer K}`? MergeParams<Rest,Mode,StrictParams,FuzzyParams,SetProperty<M, K, undefined, Mode, StrictParams, FuzzyParams>>: never: M;type SetProperty<T,K extends PropertyKey,V extends any = true,Mode extends "string" | "fuzzy" | "auto" | "strict" | "any" = "auto",StrictParams extends string[] = [],FuzzyParams extends string[] = []
> = {[P in keyof T | K]: P extends K? P extends keyof T? T[P] extends V? T[P]: T[P] extends any[]? V extends T[P][number]? T[P]: [...T[P], V]: [T[P], V]: P extends FuzzyParams[number]? string: P extends StrictParams[number]? V extends "true"? true: V extends "false"? false: V extends "null"? null: V extends `${number}`? number: V: Mode extends "string"? string: Mode extends "fuzzy"? string: Mode extends "auto"? V extends "true" | "false"? boolean: V extends "null"? null: V extends `${number}`? number: string: Mode extends "strict"? V extends "true"? true: V extends "false"? false: V extends "null"? null: V extends `${number}`? ToNumber<V>: V: Mode extends "any"? any: never: P extends keyof T? T[P]: never;
};export type ApplyMode<T,Mode extends "string" | "fuzzy" | "auto" | "strict" | "any" = "auto",StrictParams extends string[] = [],FuzzyParams extends string[] = []
> = Mode extends "auto"? T: {[P in keyof T]: P extends FuzzyParams[number]? string: P extends StrictParams[number]? T[P] extends "true"? true: T[P] extends "false"? false: T[P] extends "null"? null: T[P] extends `${number}`? ToNumber<T[P]>: T[P]: Mode extends "string"? string: Mode extends "fuzzy"? string: Mode extends "strict"? T[P] extends "true"? true: T[P] extends "false"? false: T[P] extends "null"? null: T[P] extends `${number}`? ToNumber<T[P]>: T[P]: Mode extends "any"? any: T[P];};export type Prettify<T> = {[K in keyof T]: T[K];
} & {};
使用示例
比如在地址栏中传 id 和 source 两个参数,并更改它们的值:
const { params } = useUrl<"?id=2&source=Hangzhou">((urlInfo) => {console.log(`id: ${urlInfo.params.id} source: ${urlInfo.params.source}`);},"ursUrl exmaple listener",true // call immediately
);
Bingo! 一个监听 URL 的 hook 就酱紫实现了!TS 虽好,但请慎用!