☰
前端获取当前页面URL全指南:从location到路由与参数解析
2026/10/21 2:39:07 网站建设 项目流程

"当前页面URL"大概是前端最"简单"的需求之一——嘴上说着不就是一行 location.href 吗,真落到业务里就完全不是那么回事了:有的场景只要路径,有的要带完整 query,有的得把 hash 里的参数单独抠出来,还有 H5 嵌第三方 App、微前端嵌套这类环境,同一套代码在不同宿主里拿到的 URL 语义都不一样。

这篇文章我把前端获取当前页面 URL 这件事从原生 API 到框架路由、再到各种边界场景完整拆一遍,适合刚接触前端、正在做分享/埋点功能、或者被路由参数折腾过的开发者参考。我会直接给出可以抄的代码和踩坑结论,也会解释背后的原理,有些内容网上通常搜不到这么细。

1. 需求场景盘点:拿到 URL 之后你到底想要什么

先说一句可能得罪人的话:很多人一上来就写window.location.href,其实并没有想清楚自己要的到底是哪一段。URL 不是一个整体,它由协议、域名、端口、路径、query、hash 组成,不同需求关心的片段完全不同。

拿最常见的几个场景来说:

  • 分享/复制链接:要的是完整地址,也就是location.href,或者拼上固定的推广参数(比如?from=share)。这个场景最宽容,多几个参数无所谓。
  • 埋点上报:通常要的是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:pass@www.example.com:8080/path/to/page?name=foo&age=18#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?name=foo&age=18query 部分,含 ? 号
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() 构造函数:不改地址栏也能拆解地址的进阶工具

实战里有个很常见但很多人没用顺手的 API:new URL()。它和window.location最大的区别是——它可以解析任意 URL,而不只是当前页面,而且完全不改地址栏。

基础用法很简单:

const url = new URL('https://www.example.com:8080/path/to/page?name=foo&age=18#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()真正厉害的地方是相对路径解析。它接受第二个参数作为 base:

new URL('../assets/js/index.js', 'https://www.example.com/app/detail?id=1'); // 结果: 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('name=foo&name=bar&age=18'); 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?tab=info#detail' route.query; // { tab: 'info' } —— 已被解析成对象 route.hash; // '#detail'

React Router 6 对应的是:

import { useLocation } from 'react-router-dom'; const location = useLocation(); location.pathname; // '/user/123' location.search; // '?tab=info' 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是"解释过"的对象,但它的解释规则在不同版本里有差异——比如?a=1&a=2这种重复 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或者?a=1&b;二是+号在 query 里的语义是空格,split 出来会变'hello+world';三是参数值里有&、=这些字符时(常见于 base64 或分享文案),不先解码就拆,数据直接错乱。

第二层:URLSearchParams

const 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_token=xxx而不是?access_token=xxx里。很多同学写new URLSearchParams(location.search).get('access_token')拿回来是null,排查许久发现参数在 hash 里。正确的取法是:

const params = new URLSearchParams(location.hash.replace(/^#/, ''));

坑二:base64 参数里的+被 URLSearchParams 解码成空格。

前面提过,URLSearchParams遵循 query 规范把+当作空格。如果参数值是YQ+bc==这种 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)就够了,不用存两份变量。这些看起来都是小事,累积起来才是真正不返工的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询