尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

window.location 完全解析:从 URL 操作到前端路由实战

window.location 完全解析:从 URL 操作到前端路由实战 1. 先把基础对象搞清楚location 到底是谁身上挂了多少东西1.1 location 在 window 和 document 上的挂载关系以及一个常见误解很多写了两三年前端的人一说到 window.location 都会下意识回答location 是 window 的全局属性。这句话不算错但不完整。实际上 location 在浏览器里同时挂在了 window 和 document 两个对象上window.location 和 document.location 指向的是同一个 Location 对象。你直接写location.href、window.location.href、document.location.href拿到的都是同一份数据。这个特性在面试题里经常被拿来当陷阱但在日常开发中它真正的作用是帮你少纠结一件事任何全局作用域里直接写 location 就行不用费心从哪个对象上去取。理解窗口定位的起点不是先背 API而是先建立一个心智模型location 对象是浏览器地址栏的实时映射。地址栏变了location 跟着变反过来你改 location 的某些属性地址栏也会跟着变甚至直接触发页面跳转。也就是说它不是一份静态的 URL 快照而是一套能双向同步的活动接口。这一点决定了后续所有 API 的行为方式。再看 location 到底由哪些部分组成。我直接给出一段演示代码假设当前页面地址是// 假设当前地址为 // https://user:passwww.example.com:8080/path/to/page.html?namehoorainpage2#section console.log(window.location.href); // https://user:passwww.example.com:8080/path/to/page.html?namehoorainpage2#section console.log(window.location.origin); // https://www.example.com:8080 console.log(window.location.protocol); // https: console.log(window.location.host); // www.example.com:8080 console.log(window.location.hostname); // www.example.com console.log(window.location.port); // 8080 console.log(window.location.pathname); // /path/to/page.html console.log(window.location.search); // ?namehoorainpage2 console.log(window.location.hash); // #section这是拆解窗口定位最常用的一份清单。老实说我在这上面也翻过几次车印象最深的有三个一是混淆 host 和 hostname。host 是带端口的完整主机部分hostname 是纯域名。默认端口 80 或 443 时host 里不会显示端口号所以很多人测试的时候发现两者一样就以为永远一样结果项目一挂到带端口的测试环境就出问题。二是解析参数时习惯性拿 search 字符串去手动 substring、split处理中文和特殊字符时各种转码地狱。这个问题在后面的 URLSearchParams 部分细说。三是有一次我把 toString() 当成取路径来用结果 toString 返回的是整个 href连带 search 和 hash 全带上了跟预期完全不符。记住Location 对象的 toString() 本质上返回的就是 href 的值。1.2 哪些属性可写、哪些只能读后果差异很大把 location 的属性按可写性分开看开发和排查问题时会更清楚属性可读可写典型用途href是是跳转、读取完整地址origin是否跨域判断、接口前缀拼接protocol是是识别 http/httpshost是是判断当前环境主机和端口hostname是是域名白名单判断port是是非默认端口判断pathname是是路由判断、页面标识search是是查询参数读取与追加hash是是锚点定位、hash 路由可写属性里最常用的是 href、search、hash。但可写到底意味着什么不同属性差别很大。给 location.href 赋一个值浏览器会先以当前文档 URL 为基准做相对路径解析然后再发起导航这个过程会新增一条历史记录。给 location.hash 赋值则温和得多只会改变地址栏的 hash 部分不会导致整页刷新但会往历史记录里塞一条并触发 hashchange 事件。给 location.search 赋值则会带着完整路径重新加载页面。有一个细节容易被忽略origin 是只读的。有些人在多环境部署时想通过修改 origin 来切换 API 环境直接在代码里写location.origin https://another.com结果静默失败不说还不报错极难排查。正确做法是直接修改 href 走完整跳转或者用new URL拆解后重新拼接。1.3 兼容性的小尾巴origin 和 ancestorOrigins现代浏览器对 location 的支持已经非常统一但有两个点值得留个心眼。第一个是 origin 属性。它属于相对较新的标准化属性IE 时代完全不支持。如果你维护的老系统还要兼容旧内核浏览器取 protocol host 拼接才是稳妥方案function getOrigin() { if (typeof window.location.origin ! undefined) { return window.location.origin; } return window.location.protocol // window.location.host; }第二个是 location 对象上有一个很少被提到的ancestorOrigins属性它是一个 DOMStringList保存了当前页面所有父级 frame 的 origin 列表。这个属性在做 iframe 多层级嵌套时特别好用可以判断页面是被谁嵌进来的比在 query 参数里手动传来源靠谱得多。不过它只在部分浏览器里可用用之前最好先做能力检测。2. 页面导航三件套href、assign、replace 在真实场景里差在哪2.1 从地址栏到页面location.href 赋值时浏览器做了什么当你执行location.href /login?fromorder时浏览器内部做的是这样几件事先拿当前文档的 URL 作为基准地址把/login?fromorder解析成完整的目标 URL然后检查目标地址的协议是否安全如果协议合法就触发一次导航流程往浏览器的历史记录栈里压入一条新记录最后地址栏更新、页面开始加载。这条链路里最容易被忽视的是压入一条历史记录。这意味着用户点浏览器的后退按钮会回到设置 href 之前的那个页面。很多业务系统里面的跳转逻辑没有考虑这一点导致用户绕来绕去历史记录特别长。于是衍生出了一个高频需求怎么跳转但不产生新的历史记录答案是 location.replace。2.2 assign 与 href 的几乎等价以及我为什么还是坚持区分Location 对象上原生提供了 assign 方法语义上它和给 href 赋值几乎完全等价都会解析相对地址、都会新增历史记录、都会触发导航。所以网上很多资料直接说location.href url 等价于 location.assign(url)。实际开发中我依然会把它们分开用。给 href 赋值看起来更像声明式的写法适合在代码里写死跳转目标assign 则更过程式适合在函数里作为动作来调用阅读代码的人一眼能看出这里正在发起一次显式导航。不过有一个细节值得注意如果参数里的 URL 带有非法字符或者编码不完整assign 和 href 赋值在部分浏览器内核里的容错表现不完全一致。比如 Windows 文件路径那种带反斜杠的字符串直接赋值 href 可能被解析成相对路径而 assign 在某些内核版本里会尝试做更多规范化。这属于极端边界情况但如果你在维护老项目遇到诡异跳转问题时可以往这个方向查一查。2.3 replace 会把 history 记录换掉登录跳转和表单防重提的利器location.replace(url) 与前面两种方式最大的区别在于它不会新增历史记录而是把当前页面在历史栈里的那一条记录直接替换成目标地址。用户点后退不会回到当前页。这个特性在两类场景里是刚需。第一类是登录成功后的跳转用户在登录页完成认证后跳转首页如果用的是 href 或 assign用户按后退键会重新回到登录页此时可能触发回跳逻辑轻则多一次无意义的页面加载重则陷入登录死循环。用 replace 把登录页从历史里顶掉这个问题就消失了。第二类是表单提交成功后的跳转。用户提交订单之后跳转到支付结果页如果保留历史记录用户按后退回到提交页再按前进某些浏览器可能重新提交表单引发重复下单的严重事故。用 replace 跳走后退按钮就直接失效了从根上杜绝了这类问题。我踩过的坑是把 replace 用在页面内滚动定位的诉求上想着替换当前页面不产生新记录会不会更顺滑。结果 replace 是整页级别的跳转跟滚动定位完全不是一回事。页面内滚动定位应该用location.hash配合锚点或者直接用scrollIntoView()跟 replace 没有关系。2.4 reload 与缓存策略的纠缠不要把强制刷新交给用户location.reload() 在 MDN 上的描述是重新加载当前页面。早年它支持一个布尔参数 forceGet传 true 表示绕过缓存强制从服务器拿新资源。现在这个参数已经被标记为非标准大部分浏览器也不再理睬这个布尔值。我现在的经验是生产代码里绝不主动调用 reload尤其是用户正在编辑表单的场景。一次手滑用户填了半天的数据全没了这种体验是最招骂的。而且即使你想强制刷新单纯依赖 reload 也做不到干净利落的缓存控制你需要配合服务端的 Cache-Control 响应头来决定哪些资源走缓存、哪些资源走协商验证。如果确实需要软刷新可以这样// 保留页面状态的情况下重新加载当前地址 location.reload();如果是业务上需要带新参数重新进入页面不要用 reload直接改 location.search// 给当前页面追加一个刷新标记 const url new URL(window.location.href); url.searchParams.set(ts, Date.now()); location.href url.href;这种方式会重新加载页面但参数可控、逻辑清晰比裸调 reload 好排查得多。3. 拆 URL 的十八般武艺search、hash、origin 的组合读取与参数清洗3.1 用 URLSearchParams 替代手工 split 的实践很多老代码在处理地址栏参数时还在用window.location.search.substring(1).split()然后每一项再 split 一次。这套逻辑在参数简单时没问题一旦参数值里出现、、#或中文就会出各种幺蛾子。我接手过一个遗留项目分享出去的链接在部分浏览器里会被截断排查到最后发现就是手工 split 把参数值内部自带的部分切碎了。现代浏览器原生提供的 URLSearchParams 就是为这个场景设计的const url new URL(window.location.href); const params url.searchParams; // 读取单个参数 const keyword params.get(keyword); // 判断参数是否存在 const hasPage params.has(page); // 获取同一参数名下的多个值比如 checkbox 多选 const tags params.getAll(tag);URLSearchParams 最大的好处是它内部自动处理了 encodeURIComponent 和 decodeURIComponent 的双向转换。你设置一个中文值它帮你编码好你读取一个编码过的值它帮你解码还原。手工 split 永远做不到这么干净。它还有几个实用方法set 会覆盖同名参数、append 会追加同名参数、delete 删除指定参数、toString 输出完整查询串。配合 URL 对象使用可以轻松实现对当前地址参数的增删改查。3.2 URL 参数编码顺序和 hash 坑我的一次线上分享事故分享一个真实事故。某个部署环境里有个转发分享功能页面从地址栏 search 参数里取分享人 ID然后拼接到接口上。生成分享链接的代码是这样写的// 错误示范字符串直接拼接 const shareUrl location.origin /goods? shareUserId userId #detail;当时页面首页本身带了 hash 锚点于是拼出来的地址把 hash 放在了 search 后面看起来没问题。但接收方在部分浏览器客户端里打开这条链接时URL 尾部多了一个#原本的 hash 值消失参数解析直接错位后端按错误参数返回了空数据。排查了大半天最终定位到是分享链接生成时把 hash 放错了位置、也没有对参数值做编码。从那以后我定了一条铁律URL 拼接必须使用 URL 对象禁止直接用字符串拼参数。而且 hash 必须放在 search 之后顺序不能乱。function buildShareUrl(base, params, hash) { const url new URL(base, window.location.origin); Object.entries(params).forEach(([k, v]) url.searchParams.set(k, v)); if (hash) { url.hash hash; } return url.href; }用 URL 对象还有一个好处base 既可以传绝对路径也可以传相对路径第二个参数window.location.origin作为基准地址能自动补全协议和域名不用自己再拼接 origin省去了很多 protocol、host 判断的重复代码。3.3 host、hostname、origin 的边界判断多环境管理的姿势在多个环境部署时前端代码里经常要根据当前域名切换接口地址比如// 根据 hostname 判断环境 if (location.hostname.includes(preview)) { apiBase https://preview-api.example.com; } else { apiBase https://api.example.com; }这种includes判断看着聪明实际很脆弱。假设测试环境域名是preview.example.com生产环境是example.com而测试环境的接口返回里恰好包含一些带 preview 字样的业务字段这段逻辑不会误判但如果你写成location.hostname example.com来判断生产环境那就很可能把 preview 域名下的页面也判成生产了。高可用架构下多环境切换我推荐维护一个显式的环境映射表const envMap { preview.example.com: test, staging.example.com: staging, example.com: prod }; const env envMap[location.hostname] || dev;这样环境判断的逻辑完全显性化新环境接入时只加一条配置不会因为字符串匹配的写法隐患导致线上事故。4. 从 location 到 History APISPA 路由背后的定位逻辑切换4.1 hash 路由不刷新页面改变地址的旧方案单页应用兴起后location.hash 在页面内锚点之外多了一个重量级用途路由。原理很简单修改 hash 不会导致整页刷新但地址栏会变同时产生一条历史记录并触发 hashchange 事件。早期 Vue Router 的 hash 模式就建立在这套机制上。window.addEventListener(hashchange, () { const route window.location.hash.slice(1); renderRoute(route); });hash 路由最大的优势是简单、不需要服务端配合部署到任意静态服务器都能直接跑。缺点是 URL 里带着#不够美观而且 hash 部分不会被发送到服务端SEO 约等于没有。现在做面向搜索引擎的站点基本不会用 hash 路由但内部系统、低代码平台、嵌入页里仍然大量使用因为省心。4.2 pushState/replaceState 到底改了什么又与 location 怎么配合HTML5 时代引入了 history.pushState 和 history.replaceState它们可以在不刷新页面的前提下修改地址栏 URL同时更新 location 对象。关键区别是pushState 会新增一条历史记录replaceState 则替换当前记录。history.pushState({ page: detail, id: 123 }, , /goods/123);执行完这一行地址栏会变成/goods/123要求同源页面不刷新location.href同步更新但不会触发 hashchange也不会触发 popstate。这个页面不刷新但 URL 变了的能力正是现代 history 路由的基础。和 location 配合时最容易踩坑的是 URL 拼接顺序。如果你先设置 location.hash 再 pushState或者反着来两者状态可能会互相干扰。稳妥的做法是拼好完整目标地址一次性传给 pushState。const targetUrl new URL(/goods/123?fromlist#comments, window.location.origin); history.pushState( { goodsId: 123 }, , targetUrl.pathname targetUrl.search targetUrl.hash );这样 URL 的 path、query、hash 一次写到位不会出现 hash 被吞或者顺序错乱的问题。4.3 popstate 不触发、还有 state 丢失的那些灵异时刻popstate 事件在用户点击浏览器前进、后退按钮时触发。很多初学者会以为执行 pushState 或 replaceState 后也会触发 popstate实际完全不会。这是面试场上很常见的雷也是业务代码里非常容易出 bug 的地方。正确的做法是pushState 之后手动调用一次自己的渲染逻辑popstate 事件里从 history.state 或当前地址信息里再次渲染。否则你会在 SPA 里看到地址栏变了、页面内容没变的怪象。function renderPage() { const page history.state?.page || window.location.pathname; // ...渲染逻辑 } // pushState 后手动渲染 history.pushState({ page: detail }, , /goods/123); renderPage(); // 浏览器前进/后退后自动渲染 window.addEventListener(popstate, renderPage);另一个隐蔽问题是 history.state 的存储大小限制。虽然现代浏览器给 state 对象留了几兆空间但如果你往里塞大的业务对象在某些移动端 WebView 里会异常。我的经验是 state 里只放路由需要的少量元数据比如页面名、列表查询条件需要大量数据时走 sessionStorage 或 Vuex/Pinia 这类状态库。5. 业务系统里的高端定位登录回跳、埋点上报与跨系统跳转5.1 登录回跳不要取上一页 URL而是要存目标很多新手写登录跳转时会这样做location.href /login?redirect encodeURIComponent(location.href);这个逻辑在用户从目标页被拦截到登录页的场景下能跑通但只要用户是直接输入登录地址进来的或者从外部系统跳进登录页的location.href取到的就是登录页自己或者上一个无关页面登录成功后回跳的目标就完全错了。更稳健的做法是在跳转登录前把真正想回去的地址写入会话存储登录成功后再取出来用。function goLogin() { sessionStorage.setItem(login_redirect, location.href); location.replace(/login); } // 登录成功后 const redirect sessionStorage.getItem(login_redirect) || /; sessionStorage.removeItem(login_redirect); location.href redirect;这里跳登录用 replace 也有讲究可以避免用户按后退键又回到那个因为未登录而被拦截的半成品页面减少无意义的跳转死循环。5.2 埋点上报该上报整段 URL 还是拆开上报做页面浏览埋点时最偷懒的方案是直接把location.href整个上报。但实测下来这样做会有两个问题一是 URL 里经常带着一堆渠道参数和 token 类敏感信息直接上报有泄露风险二是后端做聚合统计时如果整段 URL 里有动态参数每一跳都是一个独立 key页面 PV/UV 被拆得稀碎根本没法看。我现在常用的做法是拆分上报const payload { pageKey: location.origin location.pathname, // 作为聚合维度 query: Object.fromEntries(new URLSearchParams(location.search)), // 参数单独沉淀 hash: location.hash, referrer: document.referrer, ts: Date.now() };这样后端既能按 pageKey 统计页面访问量又能单独分析渠道参数不会因为 URL 里的随机参数污染统计口径。如果你还要区分单页应用内部的路由切换记得在路由改变时也主动上报一次。5.3 跨系统跳转的参数编码与票证安全在多业务系统集成场景里系统之间跳转经常要携带票据、订单号、回跳地址等参数。这里有两个高频的坑。第一个是参数编码。目标系统解析 query 参数时如果回跳地址里又嵌套了一层 query就必须用 encodeURIComponent 做二次编码否则内层的会把外层的参数切碎。const redirect encodeURIComponent(/order/detail?orderId123); location.href https://pay.example.com/checkout?order456redirect redirect;第二个是安全。在任何情况下都不要把长期有效的会话凭证明文放在 URL 参数里。URL 会出现在浏览器历史、服务器访问日志、Referer 头里泄密风险极高。如果一定要跨系统传递身份优先用短时效的一次性票据由后端接口兑换真正的会话信息。6. 运行时报错与面试高频点这些边界才是练真功夫的地方6.1 javascript:void(0) 为什么会出现在地址栏以及正确替代方案搜索引擎热词里大量出现 javascript:void(0)说明很多人在实际项目里受过它的困扰。它的本来用途是把一个a标签的 href 写成javascript:void(0)让点击链接时不产生页面跳转只执行后面的 onclick 脚本。a hrefjavascript:void(0) onclicksubmitOrder()提交订单/a这段代码在绝大多数浏览器里能正常工作但副作用很明显如果用户禁用了 JavaScript链接就彻底死了屏幕阅读器对这类链接的语义解析很差安全扫描工具会直接提示 URL 存在 javascript: 协议风险。更重要的是在一些特殊错误场景下地址栏可能直接显示出javascript:void(0)这种字样让用户一头雾水。现在的推荐做法是href 只放一个合理的 fallback 地址然后在事件回调里用 preventDefault 拦截默认行为。a href/order/confirm onclickreturn submitOrder(event)提交订单/afunction submitOrder(event) { event.preventDefault(); // 执行自己的逻辑 // 需要跳转时再显式调用 location.href }这样即便 JS 加载失败链接本身仍然是一个真实地址用户还能通过 fallback 继续操作容错性好很多。6.2 跨域读取 location 的限制、定位错误的关键路径多域集成场景里最常见的一个报错是Uncaught DOMException: Blocked a frame with origin https://a.com from accessing a cross-origin frame.父页面 A 想读取 iframe 里页面 B 的 location如果两个域名不同浏览器会直接抛异常。这在做客服系统、开放平台嵌入页的时候非常常见。我的排查关键路径是这样的先确认同源策略是否满足不同源就别想着直接读然后看两个页面之间有没有建立通信通道一般用 postMessage。// 父页面监听子页面发送的 location 信息 window.addEventListener(message, (event) { if (event.origin ! https://b.example.com) return; // 白名单校验 console.log(子页面地址:, event.data); });子页面在需要时主动把自身 location 发给父页面这是跨域定位信息共享的标准解法。不要尝试用任何 hack 去绕过同源策略浏览器安全模型不是给你绕的也绕不过去。6.3 面试高频问题速查和一些个人复盘结合近几年的面试经历和技术评审我把 window.location 相关的高频考点整理了一份速查表问题关键回答点location.assign 和 replace 的区别assign 新增历史记录replace 替换当前记录replace 后无法后退回原页如何获取 URL 中指定 query 参数用 URLSearchParams 的 get 方法不要手工 split如何在不刷新页面的情况下改变地址栏history.pushState 或 replaceState仅限同源hashchange 和 popstate 何时触发hash 变化触发 hashchange浏览器前进/后退触发 popstatepushState 本身不触发javascript:void(0) 的替代方案href 放真实地址onclick 里 preventDefault 拦截回答这些问题的加分点在于能主动补充边界条件。比如问 location.replace 时主动提到登录成功跳转和表单防重提的场景比光背定义强得多问 pushState 时主动补充页面需要同时处理手动渲染和 popstate 事件两套路径面试官会觉得你是真写过代码的。6.4 最后分享一个我沉淀下来的 routeUtil 小工具日常开发里我习惯把 location 相关操作收敛到一个工具模块避免团队里每个人各写各的裸调出问题时排查成本高。下面是精简版const routeUtil { // 获取当前纯净路径path search不带 hash cleanPath() { return window.location.pathname window.location.search; }, // 在当前地址上追加/更新参数并跳转 redirectWithParams(params, replace false) { const url new URL(window.location.href); Object.entries(params).forEach(([k, v]) url.searchParams.set(k, v)); if (replace) { window.location.replace(url.href); } else { window.location.href url.href; } }, // 需要保留原始参数时只改 path redirectToPath(path, replace false) { const url new URL(window.location.href); const target new URL(path, window.location.origin); target.search url.search; target.hash url.hash; if (replace) { window.location.replace(target.href); } else { window.location.href target.href; } } };这个工具很小但它统一了三个高频操作的口径纯净路径获取、参数追加跳转、保留参数仅改路径。团队里只要约定好用这个模块就不会再出现有人用 assign、有人写 href、有人字符串拼 URL的混乱场面。回过头看window.location 最核心的价值是它给了我们在浏览器里理解当前在哪、要去哪、怎么去的一把钥匙。很多看起来复杂的问题——登录回跳、路由切换、跨系统跳转、埋点上报——追到根子上都是对 location 的属性组合、可写行为、与 History API 配合方式理解不到位。把基础打牢上面这些场景基本都能迎刃而解。
返回列表