主题模式
URL / URLPattern API 深度解析:浏览器原生 URL 处理完全指南
前言
每个前端工程师每天都在和 URL 打交道。你可能写过无数次的:
javascript
// 手动拼接 —— 痛苦、容易出错
const url = 'https://api.example.com/users/' + userId + '?page=' + page + '&limit=' + limit;
// 字符串替换 —— 脆弱
const url = '/users/:id?page=:page'.replace(':id', userId).replace(':page', page);这些「土办法」的问题在于:URL 的结构被当成字符串处理,而不是有语义的结构化对象。
URL 和 URLPattern API 正是为此而生。它们是浏览器原生的 URL 解析与匹配工具,让 URL 操作从字符串操作升级为语义安全的 API 调用。
很多人以为这些 API 不过是
new URL()这么简单,但URL构造函数的细节、blob:URL 的生命周期、URLPattern的路由匹配能力,你可能从未真正掌握。
一、URL 构造函数的细节
1.1 基础用法
javascript
const url = new URL('/users', 'https://api.example.com');
// https://api.example.com/users ✅
const url2 = new URL('https://api.example.com/users?id=1');
// 直接解析完整 URL1.2 相对 URL 的解析规则
URL 构造函数接受两个参数:input(输入)和 base(基准)。这里有几个容易踩坑的点:
javascript
// 相对路径:相对于 base
new URL('/users', 'https://api.example.com/a/b'); // → https://api.example.com/users ✅
// 注意:/users 不继承 b,而是回到根路径!
// 只有相对路径(无前导 /):从 base 路径继承
new URL('users', 'https://api.example.com/a/b'); // → https://api.example.com/a/users ✅
new URL('./users', 'https://api.example.com/a/b'); // → https://api.example.com/a/users ✅
new URL('../users', 'https://api.example.com/a/b'); // → https://api.example.com/users ✅
// 数据 URL
new URL('data:text/plain,hello'); // ✅ 支持
new URL('javascript:alert(1)'); // ✅ 支持(但会被标记)
// 绝对路径(带协议)
new URL('//cdn.example.com/img.png', 'https://a.com'); // → https://cdn.example.com/img.png1.3 构造失败与 URL.canParse()
直接用 new URL() 构造无效 URL 会抛出异常:
javascript
try {
new URL('not-a-url');
} catch (e) {
e instanceof TypeError; // ✅
}
// 用 canParse 安全检查(推荐用于用户输入)
if (URL.canParse(userInput, window.location.origin)) {
const url = new URL(userInput, window.location.origin);
// ...
}
URL.canParse()是现代浏览器的安全网——先检查再构造,避免 try-catch 的样板代码。
二、URL 的组成部分:属性全览
URL 实例的每个部分都有独立的 getter/setter,直接赋值即可修改 URL:
javascript
const url = new URL('https://user:pass@api.example.com:8080/path/to/resource?foo=1&foo=2#section');
url.protocol; // 'https:'
url.host; // 'api.example.com:8080'
url.hostname; // 'api.example.com'(不含端口)
url.port; // '8080'
url.pathname; // '/path/to/resource'
url.search; // '?foo=1&foo=2'
url.hash; // '#section'
url.username; // 'user'
url.password; // 'pass'
url.origin; // 'https://api.example.com:8080'(只读,无法赋值)
// ⚠️ hostname 不含端口,host 含端口,origin 才是完整来源直接赋值修改 URL:
javascript
url.port = '9090'; // → https://user:pass@api.example.com:9090/path/to/resource?...
url.search = '?page=2'; // → https://.../?page=2
url.pathname = '/new'; // → https://.../new?page=2
url.hash = ''; // 移除 hash修改后整个 URL 会实时更新——这对动态构建 URL 非常方便。
2.1 origin 的特殊性
origin 是只读的,你无法直接赋值:
javascript
url.origin = 'https://other.com'; // 静默无效,不报错!
// 正确方式:重新构造
const newUrl = new URL(url.href);
newUrl.hostname = 'other.com';三、searchParams:URLSearchParams 的完整能力
url.searchParams 是 URLSearchParams 实例,它让查询参数的处理变得优雅:
javascript
const url = new URL('https://api.example.com/search?q=react&page=1&lang=en');
url.searchParams.get('q'); // 'react'
url.searchParams.getAll('lang'); // ['en'] — getAll 返回数组
url.searchParams.has('page'); // true
url.searchParams.keys(); // URLSearchParamsIterator { 'q', 'page', 'lang' }
url.searchParams.values(); // URLSearchParamsIterator { 'react', '1', 'en' }
url.searchParams.entries(); // URLSearchParamsIterator { ['q', 'react'], ... }
url.searchParams.toString(); // 'q=react&page=1&lang=en'3.1 增删改查
javascript
// 添加参数(不删除同名参数)
url.searchParams.append('tag', 'frontend'); // ?q=react&...&tag=frontend
// 设置参数(覆盖同名参数)
url.searchParams.set('tag', 'js'); // ?q=react&...&tag=js
// 删除参数
url.searchParams.delete('tag');
// 排序(按 key 字母顺序)
url.searchParams.sort();
// 删除所有参数
url.searchParams = new URLSearchParams();3.2 常见错误:重复参数
javascript
// ❌ 常见错误:直接拼接导致重复参数
url.search += '&page=2'; // 之前已有 page=1,变成 ?q=react&page=1&page=2
// ✅ 正确:用 searchParams API
url.searchParams.set('page', '2');3.3 自动编码:不需要手动 encodeURIComponent
javascript
url.searchParams.set('q', 'React & Vue'); // ✅ 自动编码为 'React%20%26%20Vue'
url.searchParams.set('tag', 'node.js, react'); // ✅ 自动编码
// url.search 现在是 ?q=React%20%26%20Vue&...
// 读取时自动解码
url.searchParams.get('q'); // 'React & Vue' ✅3.4 从对象一键生成查询参数
javascript
const params = { q: 'react hooks', page: 1, limit: 20 };
const url = new URL('https://api.example.com/search');
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
url.href; // https://api.example.com/search?q=react%20hooks&page=1&limit=20更简洁的写法——用 URLSearchParams 构造函数直接接受对象:
javascript
const params = new URLSearchParams({ q: 'react', page: 1 });
// URLSearchParams 构造函数接受 Record<string, string> 或 迭代器
// 但注意:数字会被 toString() 转成字符串四、Blob URL / Object URL:内存中的临时链接
URL.createObjectURL() 从 Blob 或 File 生成一个 blob: URL,用于在浏览器中引用内存中的二进制数据:
javascript
// 从 File input 获取文件
const file = fileInput.files[0];
const blobUrl = URL.createObjectURL(file);
// → 'blob:https://example.com/550e8400-e29b-41d4-a716-446655440000'
// 用于 img/video/audio 或 fetch
img.src = blobUrl; // ✅
video.src = blobUrl; // ✅4.1 生命周期:必须手动释放
这是最容易踩的坑。 blob: URL 是内存引用,不会被浏览器自动垃圾回收。你必须手动释放:
javascript
const blobUrl = URL.createObjectURL(file);
// 不用时必须释放,否则内存泄漏
URL.revokeObjectURL(blobUrl);生命周期管理最佳实践:
javascript
class MediaPreview {
#blobUrl = null;
loadFile(file) {
this.#release(); // 先释放旧的
this.#blobUrl = URL.createObjectURL(file);
this.#img.src = this.#blobUrl;
}
#release() {
if (this.#blobUrl) {
URL.revokeObjectURL(this.#blobUrl);
this.#blobUrl = null;
}
}
destroy() {
this.#release();
this.#img = null;
}
}4.2 blob URL 与跨域
blob: URL 是同源策略的——它只能在创建它的页面中使用。如果你在 iframe 中创建,blob: URL 不能被父页面访问。
对于跨域的 blob 数据,使用 URL.createObjectURL 之前请三思。更好的方案是:
javascript
// 跨域文件预览:用 fetch + blob + blob: URL(需后端 CORS 支持)
const res = await fetch(corsEnabledUrl, { mode: 'cors' });
const blob = await res.blob();
const url = URL.createObjectURL(blob);五、URLPattern:路由匹配与路由提取
URLPattern 是 2022 年新加入 Web 平台的 API,用于匹配 URL 路径或检测 URL 是否符合某种模式。它最初是浏览器为 CDD(Custom Elements)设计,后来扩展为通用 URL 匹配工具。
5.1 基础匹配
javascript
// 匹配一个 URL pattern
const pattern = new URLPattern({
pathname: '/users/:id/posts/:postId',
hash: ''
});
const result = pattern.test('https://example.com/users/42/posts/100');
result; // true ✅
// 非匹配
pattern.test('https://example.com/users/42/comments/5'); // false ❌5.2 提取参数(最有价值的部分)
javascript
const pattern = new URLPattern({
pathname: '/users/:userId/posts/:postId',
search: '?page=:page',
hash: ''
});
const result = pattern.exec('https://example.com/users/42/posts/99?page=3');
// result 中包含提取的参数!
result.pathname.groups;
// { userId: '42', postId: '99' }
result.search.groups;
// { page: '3' }
result.url.href;
// 完整 URL5.3 精确匹配 vs 通配符
javascript
// :name —— 命名段,匹配任意字符(非 /)
// * —— 零个或多个字符(跨越路径段)
// (regex) —— 内联正则
// 匹配 /articles/2024/03/15 这种路径
const datePattern = new URLPattern({
pathname: '/articles/:year/:month/:day'
});
datePattern.test('/articles/2024/03/15'); // true ✅
datePattern.test('/articles/2024/03'); // false ✅(段数不同)
// 匹配任意 /api 下所有路径
const apiPattern = new URLPattern({ pathname: '/api/*' });
apiPattern.test('/api/users'); // true
apiPattern.test('/api/users/42'); // true
// 正则段(带约束)
const idPattern = new URLPattern({
pathname: '/users/:id(\\d+)' // 只匹配数字
});
idPattern.test('/users/42'); // true
idPattern.test('/users/abc'); // false5.4 在路由框架中使用 URLPattern
手写路由匹配费时费力,URLPattern 让路由注册变得简洁:
javascript
// 轻量级路由实现
class Router {
#routes = [];
add(method, pattern, handler) {
this.#routes.push({
method: method.toUpperCase(),
pattern: new URLPattern({ pathname: pattern }),
handler
});
}
navigate(url) {
const method = 'GET';
for (const route of this.#routes) {
if (route.method !== method) continue;
const match = route.pattern.exec(url);
if (match) {
route.handler({ params: match.pathname.groups, search: match.search.groups });
return;
}
}
console.warn('No route matched:', url);
}
}
const router = new Router();
router.add('GET', '/users/:userId/posts/:postId', ({ params }) => {
console.log(`加载用户 ${params.userId} 的帖子 ${params.postId}`);
});
router.navigate('https://app.com/users/5/posts/99');
// 加载用户 5 的帖子 99六、实战一:安全构建 API URL(防止注入)
拼接 URL 参数时,searchParams 的自动编码是安全的基础:
javascript
// ❌ 危险:手动拼接导致 URL 注入
const bad = `${baseUrl}?q=${query}&page=${page}`;
// query = 'react&xss=<script>alert(1)</script>'
// → ?q=react&xss=<script>alert(1)</script>&page=1
// ✅ 安全:searchParams 自动处理转义
function buildSearchUrl(baseUrl, { q, page, sort }) {
const url = new URL(baseUrl);
if (q) url.searchParams.set('q', q);
if (page) url.searchParams.set('page', String(page));
if (sort) url.searchParams.set('sort', sort);
return url.href;
}七、实战二:URL 重构迁移
产品改版中 URL 结构变了,但需要兼容旧链接:
javascript
// 旧格式:/article?id=123
// 新格式:/articles/:id
const legacyPattern = new URLPattern({ pathname: '/article', search: '?id=:id' });
const currentPattern = new URLPattern({ pathname: '/articles/:id' });
function migrateLegacyUrl(legacyUrl) {
const match = legacyPattern.exec(legacyUrl);
if (match) {
const { id } = match.search.groups;
return `https://yourblog.com/articles/${id}`;
}
return legacyUrl;
}八、实战三:多条件搜索(带分页的 Filter Builder)
javascript
function buildFilterUrl(baseUrl, filters) {
const url = new URL(baseUrl);
const { category, minPrice, maxPrice, q, sort, page } = filters;
if (category) url.searchParams.set('category', category);
if (minPrice) url.searchParams.set('min_price', String(minPrice));
if (maxPrice) url.searchParams.set('max_price', String(maxPrice));
if (q) url.searchParams.set('q', q);
if (sort) url.searchParams.set('sort', sort);
if (page) url.searchParams.set('page', String(page));
// 保持现有参数(如来源追踪参数 utm_*)
return url.href;
}
// 使用:用户勾选了「电子产品」「价格 100-500」「排序为销量」
const searchUrl = buildFilterUrl('https://shop.com/products', {
category: 'electronics',
minPrice: 100,
maxPrice: 500,
sort: 'sales',
page: 1
});九、实战四:文件下载(带进度)
javascript
async function downloadFile(url, filename) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
// 创建 blob URL 并触发下载
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
// 立即清理
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
return blob;
}十、URL API 与 URLPattern 的浏览器支持
| API | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
URL 构造函数 | ✅ | ✅ | ✅ | ✅ |
URL.canParse() | ✅ 120+ | ✅ 120+ | ✅ 120+ | ✅ 17+ |
URL.createObjectURL() | ✅ | ✅ | ✅ | ✅ |
URL.revokeObjectURL() | ✅ | ✅ | ✅ | ✅ |
URLPattern | ✅ 102+ | ✅ 102+ | ✅ 118+ | ✅ 16.4+ |
对于旧版浏览器,urlpattern-polyfill npm 包可以提供兼容支持。
十一、总结
URL 和 URLPattern 构成了浏览器原生的 URL 处理基础设施:
URL:结构化 URL 解析、查询参数 API、自动编码解码、Blob URL 生成与释放URLPattern:URL 路由匹配、参数提取、正则约束、通配符模式
两者结合,你不再需要任何第三方 URL 处理库。浏览器已经为你搭好了安全、高效、可组合的 URL 处理层。
性能提示:
URL实例创建成本很低,每次搜索参数操作都在内部重新解析字符串。对于性能敏感的循环场景(如处理上万个 URL),可考虑缓存复用URL实例。
本文由小虾子 🦐 撰写