 解析的七个坑:443 端口消失、中文域名变 xn--、改一个参数整串被重编码)
new URL()是浏览器和 Node 里解析链接最靠谱的办法比自己写正则强得多。但它是按 WHATWG URL 标准把地址「规范化」之后再给你的拿到的结果经常和你粘进去的那串字符不一样。下面这串地址我故意把几个容易出问题的地方都塞了进去https://例子.测试:443/docs/../api/v1 list?qabra%20b#top解析结果是这样的端口没了域名变成了一串xn--路径里的../不见了空格变成了%20和%20解出来都是空格。下面一个个说环境是 Chrome 154代码都可以直接贴到控制台跑。一、默认端口会被吞掉newURL(https://example.com:443/a).port// newURL(https://example.com:443/a).host// example.comnewURL(https://example.com:8443/a).port// 8443newURL(https://example.com:80/).port// 80协议的默认端口http 的 80、https 的 443会被去掉port返回空字符串href里也不再有:443。这会影响两类代码一是拿url.port判断「有没有指定端口」443 会被当成没写二是拿href和原始字符串做比较或者当缓存 keyhttps://a.com:443/x和https://a.com/x解析后是同一个href原始字符串却不相等。要拿端口号自己补默认值constporturl.port||({http::80,https::443})[url.protocol]||二、中文域名变成 xn-- 开头的 punycodenewURL(https://例子.测试/).hostname// xn--fsqu00a.xn--0zwm56d国际化域名在 URL 里统一用 punycode 表示。拿hostname去和配置里的中文域名比较永远不相等。做域名白名单时白名单本身也用new URL()解析一遍再比两边都是 punycode 就对上了。要把 punycode 显示回中文给用户看浏览器里没有直接的 APINode 可以用url.domainToUnicode()。顺带一提主机名会被转成小写HTTPS://ExAmple.COM/Path解析后是https://example.com/Path。路径保持原样大小写敏感与否由服务端决定。三、路径里的../、./和反斜杠会被规范化newURL(https://example.com/a/b/../c/./d).pathname// /a/c/dnewURL(https://example.com\\a\\b).pathname// /a/bnewURL(https://example.com/a b).pathname// /a%20b三个行为点段被折叠对 http/https 这类特殊协议反斜杠被当成斜杠空格这类字符被百分号编码。如果你在前端用pathname做路由匹配或者权限判断拿到的是规范化之后的结果和用户实际在地址栏里敲的不一样。服务端那边也会做自己的规范化两边规则不一定完全一致所以权限判断一定要放在服务端用服务端规范化后的路径做别依赖前端解析出来的样子。四、IP 地址的几种写法都会被转成点分十进制newURL(http://0x7f.1/).hostname// 127.0.0.1newURL(http://2130706433/).hostname// 127.0.0.1十六进制、省略段、整数形式的 IPv4 都会被转成标准的127.0.0.1。这一条主要提醒做「禁止访问内网地址」校验的场景拿原始字符串做黑名单匹配是挡不住的要先解析再对解析后的hostname判断。更稳妥的做法是在服务端解析域名拿到真实 IP 之后再判断因为域名本身也可能解析到内网地址。五、相对地址直接抛异常base 末尾有没有斜杠结果不同newURL(/api/user)// TypeError: Invalid URLnewURL(example.com/path)// TypeError: Invalid URL没写协议也不行new URL()只接受绝对地址。相对地址要传第二个参数 basenewURL(b,https://x.com/a).href// https://x.com/bnewURL(b,https://x.com/a/).href// https://x.com/a/bnewURL(/b,https://x.com/a/).href// https://x.com/bnewURL(//cdn.com/x.js,https://x.com/a).href// https://cdn.com/x.js前两行只差 base 末尾一个斜杠结果差了一级目录拼 API 地址时这是很常见的 bug。最后一行是协议相对地址会直接换掉主机。想先判断能不能解析、又不想写 try/catch新一点的环境有URL.canParse()URL.canParse(/x)// falseURL.canParse(/x,https://a.com)// true六、searchParams 读的时候把当空格改一个参数会重写整串constunewURL(https://x.com/s?qabra%20btc%2Bd)u.searchParams.get(q)// a bu.searchParams.get(r)// a bu.searchParams.get(t)// cdsearchParams按application/x-www-form-urlencoded规则解析和%20都是空格真正的加号要写成%2B。截图里的查询参数表也是这样q和r显示的都是a b。更隐蔽的是写操作u.searchParams.set(n,1)u.search// ?qabrabtc%2Bdn1我只加了一个n原来的ra%20b却变成了rab。只要动了searchParams整个查询串都会按它的规则重新序列化一遍。大多数服务端把两者都当空格没问题但如果对方要拿原始查询串算签名很多开放平台的接口签名都是这么做的签名就对不上了。这种场景别用searchParams改直接拼字符串或者签名在最后一步、对最终的字符串算。七、空的#和?读出来是空字符串但 href 里还在constanewURL(https://x.com/a#)a.hash// a.href// https://x.com/a#hash和search为空字符串时分不清是「没有」还是「有个空的」href里却保留了那个符号。拿url.hash 判断「链接里没有锚点」是不严谨的要区分的话只能看原始字符串。另外两个小点mailto:ab.com这种非特殊协议hostname是空的内容全在pathname里origin对mailto:是字符串null对file:在 Chrome 里是file://不同浏览器不完全一致别拿它做安全判断。最后整理成一句话new URL()给你的是规范化之后的地址凡是要拿它做比较、做白名单、做签名的地方都要想清楚比的是规范化前还是规范化后。平时排查一个可疑链接我会直接丢进 福兮的 URL 解析工具 看一眼上面那张截图就是它内部用的就是浏览器原生的URL所以看到的和你代码里拿到的一致查询参数也会按searchParams的规则拆成表格。局限也是同一个它只展示规范化之后的结果不会提示你哪些地方被改过比如端口被吞、%20被解码这些得自己对着原串看相对地址它也解析不了会直接判为无效。你在项目里被new URL()坑过吗