大胆链接前所未有的地方:文本片段

文本片段允许您在 URL 片段中指定文本片段。当导航到具有此类文本片段的 URL 时,浏览器可以强调和/或引起用户注意。

片段标识符

Chrome 80 是一个重要的版本。它包含许多备受期待的功能,例如 Web Worker 中的 ECMAScript 模块空值合并可选链等等。像往常一样,此版本的发布通过 Chromium 博客上的博文进行宣布。您可以在下面的屏幕截图中看到该博文的摘录。

Chromium 博客文章,其中带有红色框,框住了具有 id 属性的元素。

您可能在问自己所有红色框是什么意思。它们是在 DevTools 中运行以下代码段的结果。它突出显示了所有具有 id 属性的元素。

document.querySelectorAll('[id]').forEach((el) => {
  el.style.border = 'solid 2px red';
});

借助片段标识符,我可以放置指向任何用红色框突出显示的元素的深度链接,然后我在页面的 URL 的 hash 中使用它。假设我想深度链接到侧边栏中的在我们的产品论坛中给我们反馈框,我可以通过手动制作 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1 来做到这一点。正如您在开发者工具的“元素”面板中看到的那样,有问题的元素具有 id 属性,其值为 HTML1

Dev Tools 显示元素的 id

如果我使用 JavaScript 的 URL() 构造函数解析此 URL,则会显示不同的组件。请注意值为 #HTML1hash 属性。

new URL('https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1');
/* Creates a new `URL` object
URL {
  hash: "#HTML1"
  host: "blog.chromium.org"
  hostname: "blog.chromium.org"
  href: "https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1"
  origin: "https://blog.chromium.org"
  password: ""
  pathname: "/2019/12/chrome-80-content-indexing-es-modules.html"
  port: ""
  protocol: "https:"
  search: ""
  searchParams: URLSearchParams {}
  username: ""
}
*/

不过,我必须打开开发者工具才能找到元素的 id 这一事实,充分说明了页面作者是否打算链接到页面的特定部分。

如果我想链接到没有 id 的内容怎么办?假设我想链接到Web Worker 中的 ECMAScript 模块标题。正如您在下面的屏幕截图中看到的那样,有问题的 <h1> 没有 id 属性,这意味着我无法链接到此标题。这就是文本片段要解决的问题。

Dev Tools 显示没有 id 的标题。

文本片段

文本片段提案增加了对在 URL 哈希中指定文本片段的支持。当导航到具有此类文本片段的 URL 时,用户代理可以强调和/或引起用户注意。

浏览器兼容性

浏览器支持

  • Chrome: 89.
  • Edge: 89.
  • Firefox: 131.
  • Safari: 18.2.

来源

出于安全原因,该功能要求链接在 noopener 上下文中打开。因此,请确保在您的 <a> 锚点标记中包含 rel="noopener",或将 noopener 添加到您的 Window.open() 窗口功能特性列表中。

start

文本片段的最简单形式的语法如下:哈希符号 # 后跟 :~:text=,最后是 start,它表示我要链接到的百分比编码文本。

#:~:text=start

例如,假设我想链接到宣布 Chrome 80 中功能的博文中的Web Worker 中的 ECMAScript 模块标题,在这种情况下,URL 将是

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers

文本片段像这样被强调。如果您在支持的浏览器(如 Chrome)中单击链接,则文本片段将被突出显示并滚动到视图中

文本片段滚动到视图中并突出显示。

startend

现在,如果我想链接到标题为Web Worker 中的 ECMAScript 模块的整个部分,而不仅仅是其标题,该怎么办?对该部分的整个文本进行百分比编码将使生成的 URL 变得不切实际地长。

幸运的是,有一种更好的方法。我可以不使用整个文本,而是使用 start,end 语法来框定所需的文本。因此,我指定所需的文本开头处的几个百分比编码的单词,以及所需的文本结尾处的几个百分比编码的单词,并用逗号 , 分隔。

看起来像这样

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers,ES%20Modules%20in%20Web%20Workers..

对于 start,我有 ECMAScript%20Modules%20in%20Web%20Workers,然后是一个逗号 ,,后跟 ES%20Modules%20in%20Web%20Workers. 作为 end。当您在支持的浏览器(如 Chrome)中单击链接时,整个部分将被突出显示并滚动到视图中

文本片段滚动到视图中并突出显示。

现在您可能想知道我选择 startend 的原因。实际上,稍微短一点的 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers. 仅在每一侧使用两个单词也同样有效。将 startend 与先前的值进行比较。

如果我更进一步,现在对 startend 都只使用一个单词,您可以看到我遇到了麻烦。URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers. 现在甚至更短了,但是突出显示的文本片段不再是最初想要的那个。突出显示在单词 Workers. 的第一次出现处停止,这是正确的,但不是我想要突出显示的。问题是,当前的一个单词 startend 值无法唯一标识所需的部分

非预期文本片段滚动到视图中并突出显示。

prefix--suffix

startend 使用足够长的值是获得唯一链接的一种解决方案。但是,在某些情况下,这是不可能的。顺便说一句,我为什么选择 Chrome 80 版本博文作为我的示例?答案是,在此版本中引入了文本片段

Blog post text: Text URL Fragments. Users or authors can now link to a specific portion of a page using a text fragment provided in a URL. When the page is loaded, the browser highlights the text and scrolls the fragment into view. For example, the URL below loads a wiki page for 'Cat' and scrolls to the content listed in the `text` parameter.
文本片段公告博文摘录。

请注意,在上面的屏幕截图中,“text”一词出现了四次。第四次出现是用绿色代码字体书写的。如果我想链接到这个特定的单词,我会将 start 设置为 text。由于单词“text”只是一个单词,因此不能有 end。现在怎么办?URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=text 在标题中单词“Text”的第一次出现处已经匹配

文本片段在“Text”的第一次出现处匹配。

幸运的是,有一个解决方案。在这种情况下,我可以指定一个 prefix- 和一个 -suffix。绿色代码字体“text”之前的单词是“the”,之后的单词是“parameter”。其他三个“text”单词的出现都没有相同的周围单词。有了这些知识,我可以调整先前的 URL 并添加 prefix--suffix。与其他参数一样,它们也需要进行百分比编码,并且可以包含多个单词。https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=the-,text,-parameter。为了使解析器能够清楚地识别 prefix--suffix,需要用短划线 - 将它们与 start 和可选的 end 分隔开。

文本片段在“text”的所需出现处匹配。

完整语法

下面显示了文本片段的完整语法。(方括号表示可选参数。)所有参数的值都需要进行百分比编码。这对于短划线 -、与号 & 和逗号 , 字符尤其重要,这样它们就不会被解释为文本指令语法的一部分。

#:~:text=[prefix-,]start[,end][,-suffix]

prefix-startend-suffix 中的每一个都只会匹配单个块级元素中的文本,但完整的 start,end 范围可以跨越多个块。例如,:~:text=The quick,lazy dog 在以下示例中将无法匹配,因为起始字符串“The quick”不会出现在单个不间断的块级元素中

<div>
  The
  <div></div>
  quick brown fox
</div>
<div>jumped over the lazy dog</div>

但是,它在此示例中匹配

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>

使用浏览器扩展程序创建文本片段 URL

手动创建文本片段 URL 非常繁琐,尤其是在确保它们是唯一的情况下。如果您真的想这样做,该规范提供了一些提示并列出了生成文本片段 URL 的步骤。我们提供了一个名为 Link to Text Fragment 的开源浏览器扩展程序,它允许您通过选择任何文本,然后在上下文菜单中单击“复制选定文本的链接”来链接到任何文本。此扩展程序适用于以下浏览器

Link to Text Fragment 浏览器扩展程序。

一个 URL 中的多个文本片段

请注意,一个 URL 中可以出现多个文本片段。特定的文本片段需要用与号字符 & 分隔。这是一个包含三个文本片段的示例链接:https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=Text%20URL%20Fragments&text=text,-parameter&text=:~:text=On%20islands,%20birds%20can%20contribute%20as%20much%20as%2060%25%20of%20a%20cat's%20diet

一个 URL 中的三个文本片段。

混合元素和文本片段

传统的元素片段可以与文本片段组合使用。在同一个 URL 中同时包含两者是完全可以的,例如,为了在页面上的原始文本发生更改时提供有意义的后备,以便文本片段不再匹配。URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1:~:text=Give%20us%20feedback%20in%20our%20Product%20Forums. 链接到在我们的产品论坛中给我们反馈部分,其中同时包含元素片段 (HTML1) 和文本片段 (text=Give%20us%20feedback%20in%20our%20Product%20Forums.)

链接到元素片段和文本片段。

片段指令

我还没有解释语法中的一个要素:片段指令 :~:。为了避免与如上所示的现有 URL 元素片段发生兼容性问题,文本片段规范引入了片段指令。片段指令是 URL 片段的一部分,由代码序列 :~: 分隔。它保留用于用户代理指令(例如 text=),并在加载期间从 URL 中剥离,以便作者脚本无法直接与之交互。用户代理指令也称为指令。在具体情况下,text= 因此称为文本指令

功能检测

要检测支持,请测试 document 上的只读 fragmentDirective 属性。片段指令是一种供 URL 指定定向到浏览器而不是文档的指令的机制。它旨在避免与作者脚本直接交互,以便可以添加未来的用户代理指令,而无需担心对现有内容引入重大更改。未来添加的一个潜在示例可能是翻译提示。

if ('fragmentDirective' in document) {
  // Text Fragments is supported.
}

功能检测主要用于动态生成链接(例如由搜索引擎生成)的情况,以避免向不支持文本片段的浏览器提供文本片段链接。

样式化文本片段

默认情况下,浏览器以与样式化 mark 相同的方式样式化文本片段(通常为黄底黑字,CSS 系统颜色用于 mark)。用户代理样式表包含如下所示的 CSS

:root::target-text {
  color: MarkText;
  background: Mark;
}

如您所见,浏览器公开了一个伪选择器 ::target-text,您可以使用它来自定义应用的高亮显示。例如,您可以将文本片段设计为红底黑字。与往常一样,请务必检查颜色对比度,以便您的覆盖样式不会导致无障碍功能问题,并确保高亮显示实际上在视觉上从其余内容中脱颖而出。

:root::target-text {
  color: black;
  background-color: red;
}

Polyfill 能力

文本片段功能可以在一定程度上进行 polyfill。我们提供了一个 polyfill扩展程序在内部使用它,用于在 JavaScript 中实现功能的、不提供对文本片段内置支持的浏览器。

polyfill 包含一个文件 fragment-generation-utils.js,您可以导入和使用它来生成文本片段链接。这在下面的代码示例中进行了概述

const { generateFragment } = await import('https://unpkg.com/text-fragments-polyfill/dist/fragment-generation-utils.js');
const result = generateFragment(window.getSelection());
if (result.status === 0) {
  let url = `${location.origin}${location.pathname}${location.search}`;
  const fragment = result.fragment;
  const prefix = fragment.prefix ?
    `${encodeURIComponent(fragment.prefix)}-,` :
    '';
  const suffix = fragment.suffix ?
    `,-${encodeURIComponent(fragment.suffix)}` :
    '';
  const start = encodeURIComponent(fragment.textStart);
  const end = fragment.textEnd ?
    `,${encodeURIComponent(fragment.textEnd)}` :
    '';
  url += `#:~:text=${prefix}${start}${end}${suffix}`;
  console.log(url);
}

出于分析目的获取文本片段

许多站点使用片段进行路由,这就是浏览器剥离文本片段的原因,以避免破坏这些页面。存在公认的需求,即向页面公开文本片段链接,例如,出于分析目的,但提议的解决方案尚未实现。作为目前的权宜之计,您可以使用下面的代码提取所需的信息。

new URL(performance.getEntries().find(({ type }) => type === 'navigate').name).hash;

安全性

文本片段指令仅在完整的(非同页)导航上调用,这些导航是用户激活的结果。此外,源自与目标不同的来源的导航将要求导航在 noopener 上下文中进行,以便已知目标页面已充分隔离。文本片段指令仅应用于主框架。这意味着文本不会在 iframe 内部搜索,并且 iframe 导航不会调用文本片段。

隐私

重要的是,文本片段规范的实现不会泄露是否在页面上找到了文本片段。虽然元素片段完全在原始页面作者的控制之下,但文本片段可以由任何人创建。还记得我在上面的示例中,由于 <h1> 没有 id,因此无法链接到Web Worker 中的 ECMAScript 模块标题,但是包括我在内的任何人都可以通过仔细制作文本片段来链接到任何位置吗?

假设我运行了一个邪恶的广告网络 evil-ads.example.com。进一步假设在我的一个广告 iframe 中,一旦用户与广告互动,我动态创建了一个隐藏的跨域 iframe 到 dating.example.com,其中包含文本片段 URL dating.example.com#:~:text=Log%20Out。如果找到文本“Log Out”,我知道受害者当前已登录到 dating.example.com,我可以使用它进行用户画像。由于一个幼稚的文本片段实现可能会决定成功的匹配应导致焦点切换,因此在 evil-ads.example.com 上,我可以监听 blur 事件,从而知道何时发生匹配。在 Chrome 中,我们以这样一种方式实现了文本片段,即上述情况不会发生。

另一种攻击可能是利用基于滚动位置的网络流量。假设我可以访问受害者的网络流量日志,就像公司内联网的管理员一样。现在假设存在一份冗长的人力资源文档如果您患有...该怎么办,然后列出了一些情况,例如倦怠焦虑等。我可以在列表中的每个项目旁边放置一个跟踪像素。如果我随后确定加载文档在时间上与加载 倦怠 项目旁边的跟踪像素同时发生,那么作为内联网管理员,我可以确定员工点击了文本片段链接,其中包含 :~:text=burn%20out,该员工可能认为该链接是机密的,并且任何人都看不到。由于此示例从一开始就有些牵强附会,并且由于其利用需要满足非常具体的先决条件,因此 Chrome 安全团队评估了实施导航滚动的风险是可控的。其他用户代理可能会决定显示手动滚动 UI 元素来代替。

对于希望选择退出的站点,Chromium 支持 文档策略标头值,他们可以发送该值,以便用户代理不会处理文本片段 URL。

Document-Policy: force-load-at-top

禁用文本片段

禁用该功能的最简单方法是使用可以注入 HTTP 响应标头的扩展程序,例如 ModHeader(非 Google 产品),以插入如下所示的响应(请求)标头

Document-Policy: force-load-at-top

另一种更复杂地选择退出的方法是使用企业设置 ScrollToTextFragmentEnabled。要在 macOS 上执行此操作,请在终端中粘贴以下命令。

defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false

在 Windows 上,请按照 Google Chrome 企业版帮助支持站点上的文档进行操作。

对于某些搜索,搜索引擎 Google 会提供快速解答或摘要,其中包含来自相关网站的内容片段。当搜索以问题的形式出现时,这些精选摘要最有可能出现。单击精选摘要会将用户直接带到源网页上的精选摘要文本。这得益于自动创建的文本片段 URL。

Google 搜索引擎结果页面,显示精选摘要。状态栏显示文本片段 URL。
点击后,页面的相关部分将滚动到视图中。

结论

文本片段 URL 是一项强大的功能,可链接到网页上的任意文本。学术界可以使用它来提供高度准确的引文或参考链接。搜索引擎可以使用它来深度链接到页面上的文本结果。社交网站可以使用它来让用户共享网页的特定段落,而不是无法访问的屏幕截图。我希望您开始使用文本片段 URL,并发现它们与我一样有用。请务必安装 Link to Text Fragment 浏览器扩展程序。

致谢

文本片段由 Nick BurrisDavid Bokan 实现和指定,并由 Grant Wang 贡献。感谢 Joe Medley 对本文的全面审核。英雄图片由 Greg RakozyUnsplash 上提供。