前端获取当前页面URL全指南:从location到路由与参数解析 当前页面URL大概是前端最简单的需求之一——嘴上说着不就是一行 location.href 吗真落到业务里就完全不是那么回事了有的场景只要路径有的要带完整 query有的得把 hash 里的参数单独抠出来还有 H5 嵌第三方 App、微前端嵌套这类环境同一套代码在不同宿主里拿到的 URL 语义都不一样。这篇文章我把前端获取当前页面 URL 这件事从原生 API 到框架路由、再到各种边界场景完整拆一遍适合刚接触前端、正在做分享/埋点功能、或者被路由参数折腾过的开发者参考。我会直接给出可以抄的代码和踩坑结论也会解释背后的原理有些内容网上通常搜不到这么细。1. 需求场景盘点拿到 URL 之后你到底想要什么先说一句可能得罪人的话很多人一上来就写window.location.href其实并没有想清楚自己要的到底是哪一段。URL 不是一个整体它由协议、域名、端口、路径、query、hash 组成不同需求关心的片段完全不同。拿最常见的几个场景来说分享/复制链接要的是完整地址也就是location.href或者拼上固定的推广参数比如?fromshare。这个场景最宽容多几个参数无所谓。埋点上报通常要的是pathname search不关心域名因为 dev、test、prod 域名不一样上报时统一路径才好聚合分析。如果你把整个 href 上报了后面做路径去重就非常痛苦。登录跳转 / 参数提取要的是search或hash里携带的 token、channel、redirectUrl。这里最容易出事因为参数值经常是 base64 或二次编码过的粗暴 split 会丢数据。SPA 路由感知在用 Vue Router / React Router 的项目里window.location.pathname不等于你“业务里感知到的路由”尤其是 hash 模式和 history 模式的差异不清楚会拿到完全错误的结果。动态内容渲染根据当前路径高亮菜单、切换文案要的是pathname。这里要注意带不带尾部斜杠、大小写、URL 编码等细节。我把这些场景整理成了一张表后面几节会逐个展开场景要拿的数据最容易踩的坑分享链接完整 href忘了拼渠道参数埋点上报pathname search域名/端口不一致导致数据分散鉴权参数提取query / hash 内参数未解码、 号变空格SPA 路由感知路由对象而非 location模式混用、base 路径偏移动态渲染pathname 部分 query大小写、编码不一致所以拿到当前页面 URL这个需求时先别急着写代码反问一句我要的是整条链接还是某个片段这决定了你接下来的 API 选择。2. window.location 全属性拆解每一段都有自己的脾气window.location是浏览器内置对象它同时挂在document上所以document.location也指向同一个对象。这个对象本质上就是当前页面地址的结构化视图每个属性对应 URL 里的一段。先用一个完整例子拆开看https://user:passwww.example.com:8080/path/to/page?namefooage18#section属性值说明location.href整条完整地址最常用可读写location.originhttps://www.example.com:8080只读协议域名端口location.protocolhttps:注意带冒号location.hostwww.example.com:8080域名端口location.hostnamewww.example.com仅域名location.port8080空字符串表示默认端口location.pathname/path/to/page路径部分不含域名location.search?namefooage18query 部分含 ? 号location.hash#section锚点/路由部分含 # 号location.username/passworduser/pass少见但规范里有这里有几个容易忽略的细节第一protocol是带冒号的https:而不是https。如果你要拼 origin 或者判断是否 HTTPS别直接location.protocol https要写https:或者干脆用location.origin判断。第二非默认端口下location.host和location.hostname不相等。开发环境经常起在8080、3000、5173这些端口上如果你用hostname拼接口地址端口会丢用host才能保住端口。第三hash 和 search 都自带前缀符号一个?一个#。所以很多人判断URL 是否带参数时写if (location.search)是安全的因为没参数时它是空字符串有参数时以?开头天然为真。第四很多人不知道location还有assign()、replace()、reload()三个方法虽然它们不直接属于获取 URL的范畴但经常一起被用到。assign会留下历史记录replace会替换当前历史记录跳转登录页这类场景建议用replace这样用户点返回不会回到那个需要鉴权的页面。还有一个经典对比document.URL和location.href。document.URL在大部分现代浏览器里返回的也是完整 URL看起来几乎一样但两点差异值得注意规范里document.URL对某些历史浏览器存在不可写问题不展开而且它不具备location那批结构化属性。所以我的建议是一律用location对象不要在document.URL上纠结。3. URL() 构造函数不改地址栏也能拆解地址的进阶工具实战里有个很常见但很多人没用顺手的 APInew URL()。它和window.location最大的区别是——它可以解析任意 URL而不只是当前页面而且完全不改地址栏。基础用法很简单const url new URL(https://www.example.com:8080/path/to/page?namefooage18#section); console.log(url.pathname); // /path/to/page console.log(url.searchParams.get(name)); // foo console.log(url.hash); // #section console.log(url.origin); // https://www.example.com:8080它返回的对象属性和location几乎一一对应但多了一个关键成员searchParams这是一个URLSearchParams实例可以直接.get()、.set()、.has()、.getAll()比自己去抠字符串舒服太多。URL()真正厉害的地方是相对路径解析。它接受第二个参数作为 basenew URL(../assets/js/index.js, https://www.example.com/app/detail?id1); // 结果: https://www.example.com/assets/js/index.js这个特性在解析静态资源路径、拼接接口地址、处理前端路由的 base 时非常好用比如在某个项目里页面结构经常变用相对路径 base URL 的方式能把拼接逻辑大大简化。和window.location比URLSearchParams提供的便利让我在绝大多数场景都优先使用new URL(window.location.href)只有一种情况例外当页面 URL 非常规协议或格式异常导致new URL()抛异常时退回用location的原始字符串兜底。我会在最后一节的安全边界部分详细说。需要特别提一下URL和URLSearchParams的关系。searchParams只是 URL 对象的一个属性它本身也可以独立使用const params new URLSearchParams(namefoonamebarage18); params.getAll(name); // [foo, bar]正确的理解模型是URL 负责拆地址结构URLSearchParams 负责解析键值对。两者配合页面 URL 几乎没有搞不定的场景。4. 路由场景下的取 URL 姿势框架里别再用原生 API 硬抠SPA 普及之后大量业务代码跑在 Vue Router、React Router 这类路由库之上。很多从 jQuery 时代过来的开发者习惯性写window.location.pathname去判断当前路由结果在 history 模式下可能碰巧能跑hash 模式下直接翻车。原因在于路由库维护的是它自己内部的状态地址栏只是它的影子。以 Vue Router 4 为例在组件里正确的做法是import { useRoute } from vue-router; const route useRoute(); route.path; // /user/123 route.fullPath; // /user/123?tabinfo#detail route.query; // { tab: info } —— 已被解析成对象 route.hash; // #detailReact Router 6 对应的是import { useLocation } from react-router-dom; const location useLocation(); location.pathname; // /user/123 location.search; // ?tabinfo location.hash; // #detail你会发现路由库给出的path/pathname和window.location.pathname在history 模式下通常是相等的但在hash 模式下完全不等hash 模式下地址栏里真实路径可能是https://www.example.com/index.html#/user/123此时window.location.pathname返回的是/index.html而业务路由是/user/123。这就解释了一个经典困惑为什么我把window.location.pathname写进埋点统计出来全是/index.html另一个很容易被忽略的问题是子应用路由前缀。在微前端或者多页应用里某个应用可能挂载在/appA下面路由对象里的path和浏览器location.pathname会差一个 base 前缀。比如基座把子应用 A 路由在https://www.example.com/appA/下子应用内部route.path是/home但浏览器里实际路径是/appA/home。这时候如果直接用window.location.pathname去匹配子应用路由表必然全部失效正确做法是用路由库导出的当前路由信息或者先减去 base 再比较。我的建议是业务代码里遵循一个原则能用路由库就给路由库管只有纯浏览器侧需求比如分享整条链接、拼接推广参数才用原生 location。两者不是竞争关系各管一段。不过这里要补充一个真实坑路由库的query是解释过的对象但它的解释规则在不同版本里有差异——比如?a1a2这种重复 key可能得到数组也可能只取最后一个。如果你要对 query 做精确处理还是老老实实用URLSearchParams按原样解析不要完全信任路由库的解析结果。5. 参数解析的三种境界从 split 到 URLSearchParams 到手写工具如果说获取 URL是入门那从 URL 里准确取出参数才是真正考验功力的地方。同样一个需求我见过三种层级工作年限一眼就能看出来。第一层字符串 split 大法const query window.location.search.replace(/^\?/, ); const parts query.split(); const result {}; parts.forEach(item { const [key, value] item.split(); result[key] value; });这段代码在所有参数都是简单字母数字、不会重复、没有编码的理想世界里是能跑的。但它至少有三个隐患一是左右没有值时直接崩比如?flag或者?a1b二是号在 query 里的语义是空格split 出来会变helloworld三是参数值里有、这些字符时常见于 base64 或分享文案不先解码就拆数据直接错乱。第二层URLSearchParamsconst params new URLSearchParams(window.location.search); params.get(name); // foo params.has(age); // true params.getAll(tag) // [a, b]它正确处理了转空格、URL 解码、重复 key 等问题是官方推荐方案。绝大多数业务场景到这里就够用了不需要自己造轮子。第三层理解边界之后的手写工具为什么还需要第三层因为URLSearchParams有两个不可忽略的怪癖它会把当成空格对 URL query 是对的但如果参数值是 base64 密码这类含的内容就麻烦了而且它遍历时默认是最后出现的 key 保留一个还是合并不同浏览器实现一致的但你要把对象转成普通对象时重复 key 只会保留一个。这种需求不多但一旦踩到就是线上问题。一个比较稳妥的工具函数长这样function parseSearch(search window.location.search) { const result {}; if (!search || search ?) return result; const searchStr search.replace(/^\?/, ); // 不用 URLSearchParams手动拆是为了保留 号语义的自主控制 const pairs searchStr.split(); for (const pair of pairs) { if (!pair) continue; const eqIndex pair.indexOf(); if (eqIndex -1) { // 形如 ?flag 无值参数value 视为 const key decodeURIComponent(pair.replace(/\/g, )); result[key] ; continue; } const rawKey pair.slice(0, eqIndex); const rawValue pair.slice(eqIndex 1); // 这里默认 decodeURIComponent捕获异常防止非法编码直接炸掉 let key, value; try { key decodeURIComponent(rawKey.replace(/\/g, )); value decodeURIComponent(rawValue.replace(/\/g, )); } catch (e) { key rawKey; value rawValue; } // 重复 key 合并成数组 if (Object.prototype.hasOwnProperty.call(result, key)) { if (Array.isArray(result[key])) { result[key].push(value); } else { result[key] [result[key], value]; } } else { result[key] value; } } return result; }这个函数做了三件 split 大法没做的事处理号语义、对无值参数兜底、对非法编码兜底、重复 key 自动合并。你可以把这段直接拿去作为项目里的公共工具。有人问为什么不用URLSearchParams包一层就好了因为URLSearchParams的构造和迭代都经过一套既定解码规则当你要覆盖它边界输入的行为时手动解析反而更容易控制。这也算是一种工具是给常态用的异常得自己留一手的思路。6. 踩坑清单与安全边界URL 里藏着多少意料之外最后一节写点实地踩出来的教训每一条都对应一个见过的事故。坑一hash 里的参数search 永远拿不到。常见于 OAuth 或 SSO 跳转有时候 token 放在#access_tokenxxx而不是?access_tokenxxx里。很多同学写new URLSearchParams(location.search).get(access_token)拿回来是null排查许久发现参数在 hash 里。正确的取法是const params new URLSearchParams(location.hash.replace(/^#/, ));坑二base64 参数里的被 URLSearchParams 解码成空格。前面提过URLSearchParams遵循 query 规范把当作空格。如果参数值是YQbc这种 base64 串取出来会变成YQ bc。这种场景需要绕过 URLSearchParams手动decodeURIComponent处理或者先把替换成%2B再解。我踩过一次之后凡是接第三方跳转参数都默认先手动解码再交给业务逻辑。坑三iframe 嵌套环境下能拿到的 URL 不等于你想要的 URL。页面被嵌进 iframe 时window.location.href返回的是 iframe 自己的地址不是父页面地址。跨域情况下父页面地址根本读不到脚本会直接抛安全错误同域时可以试着用window.parent.location.href。所以做来源页统计时不要想当然认为location.href就是用户看到的地址先确认自己有没有被嵌。很多编辑器、低代码平台的组件页面都会在这里翻车。坑四about:blank、javascript:这类特殊协议会让new URL()直接抛异常。比如有些安全检测或第三方脚本会在空页面里执行window.location.href是about:blank直接new URL(location.href)会在某些浏览器里报错。稳妥做法是先判断协议再解析let parsed null; try { parsed new URL(window.location.href); } catch (e) { parsed null; }另一个相关经验是如果你用 URL 的origin去匹配白名单务必处理null这个字符串——about:blank、srcdoc等场景下origin不是null值而是字符串null用 null判断会漏掉。坑五埋点上报 URL 时小心敏感信息泄露。这是安全里最容易被忽视的一条。URL 的 query 或 hash 经常带 token、sessionId、手机号等敏感信息如果你把整条location.href直接打进日志、上报到监控平台等于把用户凭证写进了持久化系统。我的习惯是上报前做一层清洗只保留 pathname把 query 里的敏感键抹掉再做聚合。分享链接同理如果要给外部用户生成链接先用后端接口换一个短链参数不要在 URL 里明文传 token。坑六history 模式下服务端配置错误前端拿到的 pathname 是404页面的。这个坑不在前端代码但症状非常迷惑本地开发一切正常线上部署后location.pathname永远是/index.html或者某个 404 页面地址。原因是服务端没有做 history 模式的路由回退所有未匹配请求都返回了 404 页面。排查思路是看网络请求返回的文档是不是index.html如果是就没辙得找后端改配置。很多前端同学会在这个问题上浪费一整天我这里直接帮你排除了。最后再补充一个我的个人习惯做了这么多年前端我现在处理获取当前页面 URL的基本流程已经固化了先问业务要的是完整链接还是片段再决定用 location 还是路由库涉及参数解析一律走 URLSearchParams特殊编码场景才上手动解析任何要持久化或上报的 URL 先脱敏。另外一个小技巧调试时经常要快速看当前页面的完整 URL 和参数直接在浏览器控制台输入location回车Chrome 会把每个属性都列出来比一个个 console.log 快太多了。如果你想在代码里同时拿到 URL 解析结果和原始字符串一行const url new URL(location.href)就够了不用存两份变量。这些看起来都是小事累积起来才是真正不返工的经验。