URL哈希控制网页文字突出高亮显示技术

Chrome 80 是一个重大版本。它包含许多备受期待的功能,例如 Web Workers 中的 ECMAScript 模块、null 合并可选链等。和往常一样,该版本通过 Chromium 博客上的博文公布。您可在下面的屏幕截图中查看这篇博文的摘录。

Chromium 博文,在具有 id 属性的元素周围使用了红色框。

您可能在问自己,各个红色方框都表示什么意思。它们是在开发者工具中运行以下代码段的结果。它会突出显示具有 id 属性的所有元素。

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

得益于 fragment 标识符,我可以将深层链接用于网页网址的哈希值中,之后,该标识符指向用红色框突出显示的任何元素。假设我想深层链接到产品论坛框中的“提供反馈意见”框,可以通过手动创建网址 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1 来实现。正如您在开发者工具的“元素”面板中看到的,相关元素的 id 属性值为 HTML1

显示元素的 id 的开发者工具。

如果我使用 JavaScript 的 URL() 构造函数解析此网址,便会看到不同的组成部分。请注意值为 #HTML1 的 hash 属性。

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 Workers 中的 ECMAScript 模块标题。如下面的屏幕截图所示,相关 <h1> 没有 id 属性,这意味着我无法链接到此标题。这就是文本 Fragment 需要解决的问题。

开发者工具显示标题,但未显示 id。

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

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

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

#:~:text=start

例如,假设我想链接介绍 Chrome 80 中功能的博文中的 ECMAScript Modules in Web Workers 标题,在这种情况下,该网址将如下所示:

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

文本片段的强调形式如下所示。如果您在 Chrome 等支持性浏览器中点击该链接,文本 fragment 将突出显示并滚动到视图中:

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

现在,如果我想要链接到标题为 ECMAScript Modules in Web Workers 的整个部分,而不只是其标题,该怎么办?对该版块的整个文本进行百分号编码会导致生成的网址变得很长。

幸运的是,有更好的方法。我可以使用 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 等支持性浏览器上点击时,系统会突出显示整个部分并滚动到视图中:

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

现在,你可能想知道我是如何选择 start 和 end 的。实际上,使用稍短的网址 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers.(每一侧仅包含两个单词)也可以正常发挥作用。将 start 和 end 与先前的值进行比较。

如果我更进一步,现在只对 start 和 end 使用一个单词,就会发现我遇到了问题。现在,网址 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers. 缩短了,但突出显示的文本片段不再是最初所需的文本片段。突出显示会在单词 Workers. 首次出现时停止,这虽然正确,但并不是我想要突出显示的内容。问题在于,所需的部分不能由当前的单字词 start 和 end 值唯一标识:

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

为 start 和 end 使用足够长的值是获取唯一链接的一种解决方案。但在某些情况下无法实现。顺便说一句,为什么我选择了 Chrome 80 版本博文作为示例?答案是,此版本中引入了文本 Fragment:

文本 Fragment 公告博文摘录。

请注意在上面的屏幕截图中,“text”一词出现了四次。第四个结果以绿色代码字体编写。如果我想链接到这个特定单词,可以将 start 设为 text。由于“text”一词只有一个单词,因此不能有 end。接下来该做些什么?网址 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=text 将与标题中已有的单词“Text”首次出现的位置匹配:

文本 Fragment 匹配第一次出现“Text”。

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

在所需的“文本”出现位置处匹配文本 Fragment。

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

#:~: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>

手动创建文本片段网址非常繁琐,尤其是在确保网址唯一性时。如果您确实需要,该规范会提供一些提示并列出了生成文本片段网址的确切步骤。我们提供了一个名为 Link to Text Fragment 的开源浏览器扩展程序,可让您选择任意文本,然后点击上下文菜单中的“Copy Link to Selected Text”(复制所选文本的链接),链接到该文本。此扩展程序适用于以下浏览器:

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

请注意,一个网址中可以显示多个文本片段。特定文本片段需要用和号字符 & 分隔。以下是包含三个文本片段的示例链接: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

一个网址中包含三个文本片段。

传统的元素片段可以与文本片段结合使用。在同一个网址中同时包含这两个元素完全没有问题,例如,为了在网页上的原始文本发生变化时提供有意义的回退,使文本片段不再匹配。链接到产品论坛部分的网址 https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1:~:text=Give%20us%20feedback%20in%20our%20Product%20Forums. 同时包含元素 fragment (HTML1) 和文本片段 (text=Give%20us%20feedback%20in%20our%20Product%20Forums.):

与元素片段和文本片段进行关联。

我还没解释过这个语法中的一个元素:fragment 指令 :~:。为避免与现有网址元素 Fragment(如上所示)的兼容性问题,“文本 Fragment”规范引入了 Fragment 指令。fragment 指令是由代码序列 :~: 分隔的网址片段的一部分。它专供用户代理指令(例如 text=)使用,并在加载过程中从网址中删除,以便作者脚本无法直接与其互动。用户代理指令也称为指令。因此,在具体情况下,text= 称为“文本指令”。

如需检测支持情况,请在 document 上测试只读 fragmentDirective 属性。fragment 指令是一种机制,可让网址指定指向浏览器(而不是文档)的指令。这样做是为了避免与作者脚本直接互动,这样日后便可以添加用户代理说明,而不必担心会对现有内容造成破坏性更改。未来新增的此类功能的一个潜在示例可能是翻译提示。

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

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

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

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

如您所见,浏览器提供了一个伪选择器 ::target-text,可用于自定义已应用的突出显示。例如,您可以将文本 fragment 设计为带红色背景的黑色文本。与往常一样,请务必检查色彩对比度,以便替换样式不会导致无障碍功能问题,并确保突出显示效果能在视觉上与内容的其余部分明显区分开。

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

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

polyfill 包含一个文件 fragment-generation-utils.js,您可以导入该文件并将其用于生成文本 Fragment 链接。如以下代码示例所示:

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);
}

很多网站使用 fragment 进行路由,因此浏览器会去除文本 fragment,以免破坏这些网页。有公认的需求(例如出于分析目的)公开文本片段链接,但建议的解决方案尚未实现。目前,作为临时解决方法,您可以使用以下代码提取所需信息。

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

只有在用户激活导致的完整(非同一页面)导航时,系统才会调用文本 fragment 指令。 此外,如果导航的出发地与目的地不同,那么导航需要在 noopener 上下文中进行,这样才能将目标网页视为已充分隔离。文本 fragment 指令仅适用于主框架。这意味着,系统不会在 iframe 内搜索文本,并且 iframe 导航不会调用文本 fragment。

请务必确保文本片段规范的实现不会泄露是否在网页上发现文本片段。虽然元素片段完全由原始网页作者控制,但文本片段可以由任何人创建。还记得吗,在上面的示例中,无法链接到 Web Workers 中的 ECMAScript 模块标题,因为 <h1> 没有 id,但包括我在内的任何人如何通过精心编写文本 fragment 来链接到任意位置?

假设我运营着一个恶意的广告联盟 evil-ads.example.com。我们进一步假设,在用户与广告互动后,我在一个广告 iframe 中,以文本片段网址 dating.example.com#:~:text=Log%20Out 动态创建了一个指向 dating.example.com 的隐藏跨源 iframe。如果找到“Log Out”文字,我就知道受害者当前已登录 dating.example.com,我可以使用它进行用户分析。由于原生文本 fragment 实现可能会决定成功的匹配应导致焦点切换,因此我可以在 evil-ads.example.com 上监听 blur 事件,从而了解匹配何时发生。在 Chrome 中,我们实现了文本 Fragment,以避免上述情况发生。

另一种攻击可能是基于滚动位置利用网络流量。假设我有权访问受害者的网络流量日志,例如作为公司内网的管理员。现在,假设网站上存在一篇很长的人力资源文档《What to Do If You Suffer From…》,然后列出一系列状况,例如 燃烧焦虑 (anxiety) 等。我可以在列表中的各项旁边放置一个跟踪像素。然后,如果我确定加载文档时与加载烧屏项旁边的跟踪像素一起暂时进行了,那么作为内网管理员,我可以确定某员工点击了包含 :~:text=burn%20out 的文本 fragment 链接,而员工可能认为该链接是机密的,对任何人均不可见。由于此示例是精心设计的,而且利用它需要满足非常特定的前提条件,因此 Chrome 安全团队评估了在导航上实现滚动的风险是否易于管理。其他用户代理可能会决定显示手动滚动界面元素。

对于希望停用此功能的网站,Chromium 支持文档政策标头值,以便用户代理不要处理文本片段网址。

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 会提供来自相关网站的内容摘要的快速答案或摘要。当用户以问题的形式进行搜索时,这些精选摘要最有可能显示。用户点击精选摘要后,会直接转到源网页上的精选摘要文本。这要得益于自动创建的文本片段网址。

显示精选摘要的 Google 搜索引擎结果页。状态栏会显示文本片段网址。

点击后,系统会将页面的相关部分滚动到视野范围内。

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

阅读余下内容
 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号