前端知库

URLPattern 适合做路由匹配吗?

介绍 URLPattern 的 pathname、hostname 和 search 匹配语法,说明参数提取、路由优先级、基准 URL、兼容性和安全边界,并区分它与完整路由器的职责。

参考答案

简答

  • URLPattern 可以分别约束 pathname、hostname、search 等 URL 部分并提取参数。
  • 通配符和查询参数要有明确的匹配语义;需要精确拆分时使用显式正则分组。
  • 它只负责匹配,路由优先级、加载和导航仍由路由器处理。
  • 在 Service Worker 中用 baseURL 限定源;使用前做特性检测并准备降级。

页面导航、Service Worker 请求拦截和权限边界经常需要回答“这个 URL 属于哪一类资源”。如果用字符串 startsWith('/users/') 或手写正则,容易把 /users/123/edit、查询参数、端口和编码差异处理错。URLPattern 提供了按 URL 各组成部分匹配的声明式方式,但它只是匹配器,不会替你加载组件、管理历史记录或决定权限。

JavaScript
const userPattern = new URLPattern(
  { pathname: '/users/:id' },
  'https://example.com',
);

console.log(userPattern.test('https://example.com/users/42')); // true
console.log(userPattern.test('https://example.com/users/42/edit')); // false

const result = userPattern.exec('https://example.com/users/42');
console.log(result?.pathname.groups.id); // 42

在不支持 URLPattern 的运行时,构造器本身可能不存在;能力检测和降级路径必须在模块初始化前处理。示例中的第二个参数把相对模式的基准固定为 https://example.com,避免同一模式在不同当前页面下得到不同解释。

URL 的不同部分可以分别约束

URLPattern 可以匹配 protocol、username、password、hostname、port、pathname、search 和 hash 等部分。实际应用应只声明需要的部分:

JavaScript
const assetPattern = new URLPattern({
  protocol: 'https',
  hostname: 'cdn.example.com',
  // 具名分组默认按非贪婪方式匹配,多段扩展名要用显式正则约束
  pathname: '/assets/:name([^/]+)\\.:ext([a-z0-9]+)',
});

const match = assetPattern.exec(
  'https://cdn.example.com/assets/app.8f3a.js',
);

console.log(match?.pathname.groups);
// { name: 'app.8f3a', ext: 'js' }

如果写成 '/assets/:name.:ext',name 会尽早停在第一个点,ext 得到 '8f3a.js'。文件名含多个点时,显式正则比默认分组更可预测。

没有写出的部分通常使用宽松匹配,不能误以为“只写 pathname 就自动限制同源”。服务 Worker、图片代理和跳转处理器要显式校验 origin、协议和允许的主机;匹配成功只是形状符合,不代表请求可信。

路径参数可以表达一段动态值,但不要把它当成解码和业务校验的替代品。拿到 groups.id 后仍要验证字符集、长度、数值范围和资源权限:

JavaScript
function readUserId(url, pattern) {
  const result = pattern.exec(url);
  const rawId = result?.pathname.groups.id;
  if (!rawId || !/^[1-9]\d{0,11}$/.test(rawId)) {
    return null;
  }
  return rawId;
}

服务端仍必须重新解析和授权。客户端 URLPattern 匹配、Service Worker 拦截或前端路由成功,都不能证明用户有权读取对应用户 ID。

通配符和查询参数要有明确语义

模式支持参数和通配形式,但复杂匹配可读性会迅速下降。与其写一条包揽所有路径的模式,不如为资源类型建立几个边界清晰的模式:

JavaScript
const routes = [
  {
    name: 'article',
    pattern: new URLPattern({ pathname: '/articles/:slug' }),
  },
  {
    name: 'article-edit',
    pattern: new URLPattern({ pathname: '/articles/:slug/edit' }),
  },
];

function matchRoute(url) {
  for (const route of routes) {
    const result = route.pattern.exec(url);
    if (result) return { name: route.name, result };
  }
  return null;
}

顺序是业务契约的一部分:更具体的 /edit 路由应优先于可能匹配更宽路径的规则。URLPattern 不会替你解决两个模式同时匹配时选哪一个,也不会自动按静态段数量排序。路由表应在初始化时检查重复和歧义,避免新规则加入后悄悄改变旧页面。

查询字符串可以作为 search 部分匹配,但筛选参数通常需要 URLSearchParams 做结构化解析:

JavaScript
const searchPattern = new URLPattern({
  pathname: '/resources',
  search: '*topic*',
});

if (searchPattern.test('https://example.com/resources?topic=react')) {
  const url = new URL('https://example.com/resources?topic=react');
  const topic = url.searchParams.get('topic');
  console.log(topic);
}

上面的 search 模式只说明 URL 是否符合一个文本形状,不能代替重复键、编码、数组、空值和允许列表校验。对签名参数或权限参数,前后端必须使用相同的重复键和规范化策略,避免一个系统取第一项、另一个取最后一项。

它和完整路由器不是一层能力

一个完整前端路由器还要负责:

  • 历史记录和后退前进;
  • 路由参数与 loader 数据的关联;
  • 导航取消、错误边界和代码分割;
  • 页面标题、滚动恢复和键盘焦点;
  • 权限检查、缓存和页面状态保留。

URLPattern 只提供匹配和分组提取。可以把它作为路由器内部的一个小部件,也可以在 Service Worker、边缘请求分流和轻量 Web Component 中单独使用。不要因为模式能匹配 /dashboard,就把它当成导航完成或页面已经安全加载。

匹配器与导航状态也应分开:URL 变化后,旧请求可能仍在进行,新的匹配结果不应自动允许旧响应覆盖当前页面。路由器仍需使用导航身份、AbortSignal 或版本号检查结果是否过期。

基准 URL、编码和标准化

相对模式需要一个明确的基准。没有基准时,包含相对 pathname、协议或主机的规则可能因构造方式不同而得到意外结果。推荐在共享路由表中使用绝对 URL 或固定 base URL,不要把当前页面地址隐式作为配置来源。

URL 的大小写、百分号编码、尾部斜杠和默认端口会影响匹配结果或规范化结果。业务应先决定 /docs 与 /docs/ 是否是同一资源,再在模式、服务器重定向和静态文件部署中保持一致。不要通过对整个 URL toLowerCase() 来解决大小写问题:路径、查询参数值和用户 ID 可能大小写敏感。

groups 中的值不是任意可信文本,也不应直接拼进 HTML、SQL、shell 命令或跳转地址。用于生成链接时应让 URL 和 URLSearchParams 负责编码,并限制协议和目标 origin,避免把匹配后的参数变成开放重定向或注入入口。

Service Worker 中的使用边界

Service Worker 可以用 URLPattern 判断哪些请求交给缓存策略,但缓存命中仍需考虑 HTTP 方法、请求头、用户身份和响应的共享性:

JavaScript
// URLPatternInit 没有 origin 字段;用 baseURL 把协议、主机和端口固定为当前源
const staticAsset = new URLPattern({
  pathname: '/assets/*',
  baseURL: self.location.origin,
});

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET' || !staticAsset.test(event.request.url)) {
    return;
  }

  event.respondWith(caches.match(event.request));
});

这只是路由分支示意。缓存命中 undefined 时仍要决定是否回源,缓存键是否包含查询参数,响应是否含私有 Cookie,以及 Service Worker 更新期间旧脚本如何切换。URLPattern 不会自动防止把带用户数据的响应放入共享缓存,也不会让跨源请求绕过 CORS。

兼容性和降级策略

URLPattern 的支持范围依赖浏览器、WebView、Node.js 和 Service Worker 运行时。能力检测可以选择成熟路由库、URL/URLSearchParams 组合或经过测试的有限正则作为降级,但降级实现必须保留相同的路径、编码、参数和优先级语义:

JavaScript
function createPathMatcher() {
  if (typeof URLPattern === 'function') {
    return new URLPattern({ pathname: '/users/:id' });
  }

  return {
    exec(input) {
      const pathname = new URL(input, location.href).pathname;
      const match = /^\/users\/([^/]+)$/.exec(pathname);
      return match
        ? { pathname: { groups: { id: decodeURIComponent(match[1]) } } }
        : null;
    },
  };
}

示例降级路径只覆盖一个简单模式,decodeURIComponent 的异常、编码斜杠和重复参数在生产中必须处理。不要把一个小正则包装成“完整兼容 URLPattern”,否则新模式加入后会出现只在部分浏览器生效的路由差异。

验收时至少测试静态段与动态段冲突、尾部斜杠、编码字符、查询参数、不同 origin、非法输入、Service Worker 更新、旧浏览器和路由取消。URLPattern 的价值是把 URL 结构匹配写成可读、可复用的声明;路由优先级、数据加载、权限、安全和生命周期仍然需要应用层明确负责。