JavaScript 中的 location.hash 是什么?读取与修改 URL 锚点

星座4小时前发布 jdkjadmin
0 00
JavaScript 中的 location.hash 是什么?读取与修改 URL 锚点

JavaScript 中的 location.hash 是什么?读取与修改 URL 锚点

在地址栏中,网址问号后面的内容通常是查询参数,井号后面的内容则是 URL 片段,也常被称为锚点。例如:

https://example.com/products?sort=price#details

其中,?sort=price 是查询部分,#details 是片段部分。在浏览器 JavaScript 中,location.hash 用于读取或设置当前地址的片段。它既能配合页面中的锚点导航,也能用于实现轻量级的页面状态切换。

不过,location.hash 并不是一个通用的参数存储器,也不应被当作安全机制。它的编码方式、历史记录行为和浏览器事件都有明确边界。本文将从可运行示例出发,介绍如何正确读取、修改和监听它。

一、什么是 URL 片段

URL 中井号 # 及其后面的内容称为片段标识符(fragment)。在地址 https://example.com/page#section-2 中,片段标识符是 section-2。

片段通常用于定位当前文档中的某个位置。例如,页面存在下面的元素时,打开带有 #contact 的地址,浏览器会尝试滚动到该元素:

<h2 id="contact">联系我们</h2>

片段的处理主要发生在浏览器端。一般情况下,浏览器向服务器请求资源时不会把片段部分发送给服务器。因此,服务器收到的通常是类似 /page 的路径,而不是 /page#section-2。这也是片段常用于页面内导航和前端状态的原因之一。

在浏览器中,location.hash 返回当前 URL 的片段。若 URL 中包含非空片段,返回值通常以 # 开头;若没有片段,返回空字符串:

// 当前地址为 https://example.com/page#contact
console.log(location.hash); // "#contact"

// 当前地址为 https://example.com/page
console.log(location.hash); // ""

location 指向当前浏览上下文的地址信息。本文示例使用 window.location 或简写的 location,在普通浏览器页面中两者指向同一对象。

二、读取 location.hash

最直接的读取方式是:

const hash = window.location.hash;
console.log(hash);

注意,读取结果包含井号。若需要去掉井号,可以使用 slice(1):

const hash = window.location.hash;
const fragment = hash.startsWith("#") ? hash.slice(1) : hash;

console.log(fragment);

不建议在不检查的情况下直接对任意输入调用 decodeURIComponent()。如果片段中包含格式不正确的百分号编码,该函数会抛出 URIError。确实需要解码时,可以捕获异常:

function decodeFragment(value) {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

const fragment = decodeFragment(location.hash.slice(1));
console.log(fragment);

是否需要解码,取决于应用约定。浏览器的锚点导航和 DOM ID 匹配有自身的 URL 处理规则,而业务代码读取到的字符串可能仍带有百分号编码。不要假设“读取到的值一定已经解码”,也不要在不清楚编码规则时重复解码。

三、用 URL API 读取片段

如果需要同时处理完整 URL 的路径、查询参数和片段,可以使用 URL 对象。它比手动拆分字符串更可靠:

const url = new URL(window.location.href);

console.log(url.pathname); // 例如:"/products"
console.log(url.search);   // 例如:"?sort=price"
console.log(url.hash);     // 例如:"#details"

在测试或处理一个尚未导航到的地址时,也可以显式传入基准地址:

const url = new URL("https://example.com/products?sort=price#details");

console.log(url.pathname); // "/products"
console.log(url.searchParams.get("sort")); // "price"
console.log(url.hash); // "#details"

URLSearchParams 适合解析查询字符串,但它不会自动把片段当作查询参数解析。比如片段为 #page=2&tab=overview 时,应先取出片段,再按应用约定解析:

const rawHash = location.hash.slice(1);
const hashParams = new URLSearchParams(rawHash);

console.log(hashParams.get("page")); // "2"
console.log(hashParams.get("tab"));  // "overview"

这段代码只有在片段确实采用 key=value&key2=value2 这种格式时才有意义。URL 标准并未规定所有片段都必须是键值参数。#about、#chapter-3、#/users/42 都是常见且有效的应用约定。

四、修改 location.hash

可以给 location.hash 赋值来修改当前 URL 的片段:

location.hash = "contact";

浏览器会把井号加入地址,地址变成类似 https://example.com/page#contact。通常不需要自己拼接井号,也可以显式包含:

location.hash = "#contact";

设为与当前值相同的片段,通常不会产生新的导航变化。设置为不同片段时,浏览器会更新地址,并可能根据页面中的匹配锚点进行滚动。它通常也会产生一条浏览历史记录,因此用户按“后退”可能回到之前的片段状态。

对于包含空格或非 ASCII 字符的业务值,应按一致的编码规则处理。一个简单做法是先对片段内容编码,再赋值:

const sectionName = "产品介绍";
location.hash = encodeURIComponent(sectionName);

// 读取
const decodedSection = decodeURIComponent(location.hash.slice(1));
console.log(decodedSection);

编码与解码应成对使用,并且要避免对已编码内容再次编码、或对未编码内容盲目解码。若需要构造结构化状态,可用 URL 对象改写地址,并明确规定应用自己的片段格式。

五、修改片段而不触发页面滚动

给 location.hash 赋值可能触发浏览器的锚点定位。如果只想更新地址而不希望浏览器尝试滚动,可以使用 history.pushState() 或 history.replaceState():

const url = new URL(window.location.href);
url.hash = "panel";

history.pushState(null, "", url);

pushState() 会添加一条历史记录;replaceState() 会替换当前历史记录:

const url = new URL(window.location.href);
url.hash = "panel";

// 添加历史记录
history.pushState(null, "", url);

// 或者替换当前记录
// history.replaceState(null, "", url);

重要区别是:通过 pushState() 或 replaceState() 改变 URL,不会触发 hashchange 事件。若应用需要在这种改写后更新自身界面,应直接调用状态更新函数,或实现相应的历史状态管理逻辑。

六、监听 hashchange 事件

当片段因常见的浏览器导航或脚本设置而发生变化时,可以监听 hashchange 事件:

window.addEventListener("hashchange", (event) => {
  console.log("旧地址:", event.oldURL);
  console.log("新地址:", event.newURL);
  console.log("当前片段:", location.hash);
});

下面是一个完整的最小示例。点击链接会改变片段,事件处理函数读取片段并更新页面内容:

<nav>
  <a href="#home">首页</a>
  <a href="#about">关于</a>
</nav>

<p id="status">当前页面状态:尚未选择</p>

<script>
  const status = document.querySelector("#status");

  function renderFromHash() {
    const name = location.hash.slice(1) || "尚未选择";
    status.textContent = `当前页面状态:${name}`;
  }

  window.addEventListener("hashchange", renderFromHash);
  renderFromHash();
</script>

代码在页面初次加载时主动调用了一次 renderFromHash()。这是必要的:注册 hashchange 监听器并不会自动补发一个“当前片段已变化”的事件。若页面加载时地址已经带有片段,应用应主动读取并初始化状态。

如果链接指向页面上实际存在的元素,浏览器还会执行原生锚点导航。对于只用于前端状态、没有对应 DOM 锚点的片段,通常应确保页面滚动行为符合预期,避免状态变化意外把用户带到不相关的位置。

七、用片段实现简单的选项卡

下面示例将片段约定为 #overview 或 #settings,根据地址显示对应选项卡。将代码放入普通 HTML 页面即可运行:

<nav>
  <a href="#overview">概览</a>
  <a href="#settings">设置</a>
</nav>

<section id="overview" class="panel">这是概览内容。</section>
<section id="settings" class="panel" hidden>这是设置内容。</section>

<script>
  const panels = {
    overview: document.querySelector("#overview"),
    settings: document.querySelector("#settings")
  };

  function renderTab() {
    const requestedTab = location.hash.slice(1);
    const activeTab = Object.hasOwn(panels, requestedTab)
      ? requestedTab
      : "overview";

    for (const [name, panel] of Object.entries(panels)) {
      panel.hidden = name !== activeTab;
    }
  }

  window.addEventListener("hashchange", renderTab);
  renderTab();
</script>

这里没有把片段直接用作任意 CSS 选择器,而是先在固定的 panels 对象中检查允许的键。这样能避免未知片段造成错误选择器或意外行为。若项目需要支持大量动态 ID,也应先验证、规范化输入,再决定如何访问 DOM。

八、片段参数没有统一的业务格式

有时开发者会把多个状态放进片段,例如 #tab=profile&page=3。这可以作为应用自定义格式使用,但要记住:

  • URL 规范定义了片段的位置和基本语义,不规定它必须是某种参数格式。
  • location.hash 返回的是片段字符串,不会自动生成对象,也不会自动按业务规则校验字段。
  • URLSearchParams 可以处理约定为查询字符串格式的片段,但它不会判断参数是否合理、是否重复或是否允许。
  • 空值、重复键、字符编码和默认值都需要由应用明确规定。

例如,可以这样解析并检查页码:

function readPageFromHash() {
  const params = new URLSearchParams(location.hash.slice(1));
  const rawPage = params.get("page");
  const page = Number(rawPage);

  if (!Number.isInteger(page) || page < 1) {
    return 1;
  }

  return page;
}

console.log(readPageFromHash());

这段逻辑把缺失、非整数和小于 1 的页码统一回退为 1。校验规则应来自具体业务,而不是认为片段中的内容天然可信。

九、片段的长度和字符限制

不要把 URL 片段想象成有一个适用于所有浏览器、所有环境的固定容量。相关标准、浏览器实现、操作系统、复制粘贴方式以及中间工具,都可能影响 URL 可用长度。实际项目应避免把大量数据塞入 URL,并在目标浏览器和使用场景中测试。

片段适合放置简短、可分享、可恢复的界面状态,例如当前章节、选项卡或筛选条件标识。它不适合承载大块文档、图片数据、访问令牌或其他敏感信息。特别是把 JSON、长文本或编码后的二进制数据塞进片段,会增加地址管理和兼容性问题。

片段中可以包含多种字符,但某些字符具有 URL 语法意义,非 ASCII 字符也涉及编码。不要依赖手工拼接时对每个字符的直觉判断。构造 URL 时优先使用 URL 对象;编码单个片段值时使用与读取逻辑匹配的编码方案。

十、安全边界:片段不是秘密,也不是可信输入

由于片段通常不会发送给服务器,有时会被误认为“不会泄露”。准确说法是:它通常不在普通的 HTTP 请求中发送给服务器,但仍然会出现在用户地址栏、浏览器历史记录、书签、复制的链接以及页面 JavaScript 可读取的 location.hash 中。页面脚本、浏览器扩展和获得页面访问能力的代码也可能读取它。

因此,不要把密码、会话令牌、API 密钥或其他敏感数据放进片段。片段不是访问控制手段,也不适合用来证明用户身份。用户可以手动修改地址,其他页面脚本也可以更改片段。

片段来源于 URL,属于不可信输入。不要将它未经处理地拼接进 HTML:

// 不安全:输入可能包含 HTML 标记或其他恶意内容
element.innerHTML = location.hash.slice(1);

如果只是显示文本,应使用 textContent:

// 安全性更合适:作为纯文本显示
element.textContent = location.hash.slice(1);

如果片段要决定要打开的页面、组件或资源,应通过允许列表进行映射和校验。若要把片段交给选择器,也要考虑特殊字符会造成的选择器语法错误;可以使用固定映射,或在适用环境中使用 CSS.escape() 对选择器标识符进行转义。转义不能代替业务层的允许列表和权限检查。

十一、常见错误

1. 忘记返回值包含井号

location.hash 通常返回 #section,而不是 section。解析时可在确认首字符后使用 slice(1)。

2. 把片段当作查询参数读取

location.search 对应问号后的查询部分,location.hash 对应井号后的片段。两者是不同字段。new URLSearchParams(location.search) 不会自动读取片段。

3. 认为服务器一定能收到片段

片段通常不会随普通 HTTP 请求发送给服务器。如果后端必须根据某个值返回不同内容,应使用路径、查询参数或其他明确的请求机制,而不是依赖片段。

4. 认为任何 URL 改变都会触发 hashchange

hashchange 只针对片段变化相关的导航。通过 History API 的 pushState() 和 replaceState() 修改地址,不会触发这个事件。应用使用 History API 时需要自行同步界面状态。

5. 忘记处理初始状态

注册监听器只会处理之后发生的变化,不会替代初次渲染。页面加载后应立即读取当前片段并初始化界面。

6. 对片段重复编码或解码

encodeURIComponent() 和 decodeURIComponent() 应在清楚输入格式的情况下使用。重复编码会出现类似 %25 的额外转义;对格式错误的编码调用解码函数则可能抛出异常。

7. 把片段内容直接当作可信 HTML 或权限依据

片段可被用户任意修改。用 textContent 展示纯文本,并在执行导航、数据加载或权限相关操作前进行独立验证。客户端片段校验不能替代服务器端授权。

十二、如何选择合适的导航方式

  • 页面内章节定位:优先使用带有 id 的 HTML 元素和普通锚点链接,例如 <a href="#faq">常见问题</a>。
  • 少量、非敏感、可分享的前端状态:可以用片段,并清晰约定格式、默认值和校验规则。
  • 需要服务器读取的参数:使用路径或查询参数,不要依赖片段。
  • 复杂单页应用路由:根据需求选择 History API 或片段路由,并统一管理导航、历史记录和渲染逻辑。
  • 敏感数据或授权状态:不要放在 URL 片段中;使用适当的认证与服务端校验机制。

总结

location.hash 是读取和设置当前 URL 片段的浏览器接口。它常用于原生锚点导航,也可以承载简短的前端状态。读取时要留意返回值通常带有 #;需要结构化解析时,应由应用定义格式并进行校验;修改地址时,要区分直接设置片段与使用 History API 的行为。

最重要的边界是:片段通常不会发送给服务器,但并非秘密;它没有统一的业务参数格式和可依赖的固定容量;用户可以任意修改它。因此,不要把它当作安全凭据,也不要未经处理地将其插入 HTML。遵循这些原则,location.hash 就能在页面内导航和轻量级状态管理中发挥稳定、清晰的作用。

© 版权声明

相关文章