Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

自定义搜索参数序列化 (Custom Search Param Serialization)

默认情况下,TanStack Router 会自动使用 JSON.stringifyJSON.parse 来解析和序列化 URL 搜索参数(Search Params)。除了对搜索对象进行序列化和反序列化外,此过程还涉及对查询字符串进行转义和取消转义,这是 URL 参数处理的常见做法。

例如,在默认配置下,如果你有以下搜索对象:

const search = {
  page: 1,
  sort: "asc",
  filters: { author: "tanner", min_words: 800 },
};

它会被序列化并转义为以下查询字符串:

?page=1&sort=asc&filters=%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D

我们可以通过以下代码手动实现这种默认行为:

import {
  createRouter,
  parseSearchWith,
  stringifySearchWith,
} from "@tanstack/react-router";

const router = createRouter({
  // ...
  parseSearch: parseSearchWith(JSON.parse),
  stringifySearch: stringifySearchWith(JSON.stringify),
});

然而,默认行为可能并不适用于所有场景。例如,你可能希望使用不同的序列化格式(如 Base64 编码),或者使用专门的序列化/反序列化库,如 query-stringJSURL2Zipson

你可以通过在 Router 配置中为 parseSearchstringifySearch 选项提供自定义函数来实现这一点。在操作时,可以利用 TanStack Router 内置的辅助函数 parseSearchWithstringifySearchWith 来简化流程。

以下是几种在 TanStack Router 中自定义搜索参数序列化的示例:

使用 Base64

为了在浏览器和 URL 预览工具(Unfurlers)之间获得最大的兼容性,对搜索参数进行 Base64 编码是很常见的做法。可以使用以下代码实现:

import {
  Router,
  parseSearchWith,
  stringifySearchWith,
} from "@tanstack/react-router";

const router = createRouter({
  parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
  stringifySearch: stringifySearchWith((value) =>
    encodeToBinary(JSON.stringify(value)),
  ),
});

function decodeFromBinary(str: string): string {
  return decodeURIComponent(
    Array.prototype.map
      .call(atob(str), function (c) {
        return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
      })
      .join(""),
  );
}

function encodeToBinary(str: string): string {
  return btoa(
    encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
      return String.fromCharCode(parseInt(p1, 16));
    }),
  );
}

⚠️ 为什么不直接使用原始的 atob/btoa?

在这种配置下,之前的对象转换后的查询字符串如下所示:

?page=1&sort=asc&filters=eyJhdXRob3IiOiJ0YW5uZXIiLCJtaW5fd29yZHMiOjgwMH0%3D

使用 query-string 库

query-string 是一个非常流行的库,能够可靠地解析和格式化查询字符串。你可以用它自定义序列化格式:

import { createRouter } from "@tanstack/react-router";
import qs from "query-string";

const router = createRouter({
  // ...
  stringifySearch: stringifySearchWith((value) =>
    qs.stringify(value, {
      // ...配置选项
    }),
  ),
  parseSearch: parseSearchWith((value) =>
    qs.parse(value, {
      // ...配置选项
    }),
  ),
});

在这种配置下,转换后的查询字符串如下所示:

?page=1&sort=asc&filters=author%3Dtanner%26min_words%3D800

使用 JSURL2 库

JSURL2 是一个非标准库,它可以在保持可读性的同时压缩 URL。

import {
  Router,
  parseSearchWith,
  stringifySearchWith,
} from "@tanstack/react-router";
import { parse, stringify } from "jsurl2";

const router = createRouter({
  // ...
  parseSearch: parseSearchWith(parse),
  stringifySearch: stringifySearchWith(stringify),
});

在这种配置下,转换后的查询字符串如下所示:

?page=1&sort=asc&filters=(author~tanner~min*_words~800)~

使用 Zipson 库

Zipson 是一个非常易用且高性能的 JSON 压缩库。要结合它来压缩搜索参数(这同样需要转义和 Base64 编码),可以使用以下代码:

import {
  Router,
  parseSearchWith,
  stringifySearchWith,
} from "@tanstack/react-router";
import { stringify, parse } from "zipson";

const router = createRouter({
  parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
  stringifySearch: stringifySearchWith((value) =>
    encodeToBinary(stringify(value)),
  ),
});

// 使用下方的安全编码/解码函数...

在这种配置下,转换后的查询字符串如下所示:

?page=1&sort=asc&filters=JTdCJUMyJUE4YXV0aG9yJUMyJUE4JUMyJUE4dGFubmVyJUMyJUE4JUMyJUE4bWluX3dvcmRzJUMyJUE4JUMyJUEyQ3UlN0Q%3D

安全的二进制编码/解码

在浏览器中,原生的 atobbtoa 函数无法保证能正确处理非 UTF-8 字符。我们建议使用以下工具函数进行编码/解码:

将字符串编码为二进制字符串:

export function encodeToBinary(str: string): string {
  return btoa(
    encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
      return String.fromCharCode(parseInt(p1, 16));
    }),
  );
}

将二进制字符串解码为字符串:

export function decodeFromBinary(str: string): string {
  return decodeURIComponent(
    Array.prototype.map
      .call(atob(str), function (c) {
        return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
      })
      .join(""),
  );
}