【react.js + hooks】useUrl 监听路由参数

【react.js + hooks】useUrl 监听路由参数

本节我们来实现一个监听并解析 URL 参数的 hook:useUrl。而且这个 hook 的返回类型是可推断的。

实现思路

  • 监听 URL 变化 - 事件监听
  • 根据 URL 地址获取参数并返回 - 依赖工具方法
  • 推断参数结构 - 泛型参数(对象式 & 模板式)
  • 返回参数 - 返回解析好的参数,并合并 location 和 history 以提供更多功能

监听 URL

监听 popstate 事件即可,注意因为是全局监听,创建一个总事件。

// 全局的事件监听器
const listeners = new Set<Function>();

window.addEventListener("popstate", () => {
  // do something
  listeners.forEach((listener) => listener());
});

解析参数

使用内置的 decodeURIComponent 解析参数即可:

  • 后面几个参数是对解析的细节配置
    • mode - 分了两种解析模式
      • string - 全解析为字符串
      • auto - 智能解析
    • autoParams - 指定被智能解析的字段
    • stringifyParams - 指定被解析为字符串的字段
    • custom - 自定义参数解析的映射配置
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 虽好,但请慎用!

你可能感兴趣的:(react.js,前端,前端框架)