本节我们来实现一个监听并解析 URL 参数的 hook:useUrl。而且这个 hook 的返回类型是可推断的。
监听 popstate 事件即可,注意因为是全局监听,创建一个总事件。
// 全局的事件监听器
const listeners = new Set<Function>();
window.addEventListener("popstate", () => {
// do something
listeners.forEach((listener) => listener());
});
使用内置的 decodeURIComponent
解析参数即可:
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 虽好,但请慎用!