<a>元素支持一个名为rel的属性,rel由简简单单三个字母构成,看起来其貌不扬,但它其实是个大家族,包含各种各样的功能和作用。
其支持的属性值参见表2-1,总共有30多个。
表2-1 浏览器支持的rel属性值
每一个rel属性值都对应一个Web应用场景,比如资源的预加载、网站图片和样式的显示等。
对于<a>元素,我们需要重点关注如下几个值。
· nofollow
· noopener和opener
· noreferrer
1.rel="nofollow"
给链接元素设置rel="nofollow"是SEO中的常用策略,用来告诉搜索引擎不要追踪这个链接。
在以下两种情况下需要设置rel="nofollow":
· 目标页面显示的均是无效信息,或者含有敏感信息;
· 目标页面属于外站,不希望共享权重。
前者好理解,那么后者是什么意思呢?
SEO中有个策略,如果一个权重很高的网站,直接外链一个权重不高的中小网站,同时没有设置rel="nofollow",那么这个中小网站的权重有一定的概率会被提高。
注意,是有一定的概率被提高,而并非一定,因为搜索引擎的权重策略是保密且动态调整的。以谷歌为例,在过去,页面的权重只会分配给允许跟踪的链接,但是现在谷歌使用了一种更为复杂的分配规则,即使某个链接设置了不允许跟踪,也有可能会有一定的权重分配。
因此,大型网站,尤其是新闻、问答或社区类的网站,通常会对外链设置rel="nofollow",以防止权重被外泄。
过去,这是前端开发中的常识,而如今却是冷知识,知者甚少。
一方面是因为前端知识图谱更广了,从业者学习的精力有限,无暇顾及这部分内容;另一方面是产品形态更加多样化,例如移动端产品、工具类产品是不需要SEO知识的;当然,还有一部分原因是,这些知识的匮乏对业务收益的影响是不可见的,或者可以通过其他策略规避因开发人员不懂rel="nofollow"给产品带来的损失。
比如,将所有的外链都换成本站地址并进行中转,A网站外链了B网站,则可以专门申请一个域名,例如link.aaa.com,然后将所有外链都换成这个域名。假设原来的链接是https://www.bbb.com/xxx,现在就可以换成https://link.aaa.com?target=https://www.bbb.com/xxx,这样就可以避免权重被外泄,同时也可以统计外链的点击量。
2.rel="noopener"与rel="opener"
例如,有这样一个需求场景,主页面有一个“登录”按钮,点击后会开启一个新窗口进行登录,现在希望用户登录成功后,主页面自动刷新,该如何实现?
有人会想到使用visibilitychange事件。
也有人会使用postMessage实现跨页面通信。
但这两种方法,要么有运行冗余外加执行时机滞后的问题,要么学习成本高,上手不易。
那有没有执行时机精准,同时又能轻松实现的方法呢?
有,只需要给链接元素设置rel="opener"即可,例如:
此时,只需要在登录成功后,执行下面的JavaScript代码就可以了:
这里的window.operer就是主页面的全局window对象。
是不是简单得超出想象?仅仅通过一个其貌不扬的HTML属性设置,就让原本两个独立的浏览器窗口有效通信。
然而,window对象的权限太高了,不仅挂载在window对象上的属性和方法可以被执行,任何document对象的属性和方法也可以被执行,因为document对象可以通过window.document访问。
这其实非常危险,有安全隐患,比如会被恶意修改主页面的内容,或者被人通过window.opener.document.cookie获取用户的敏感信息并进行身份伪造等。
这么说肯定有人还是不以为意,所以,我专门做了个演示页面,你一定要打开这个页面点击里面的链接,绝对会被惊讶到。
可以通过在浏览器中输入地址https://www.htmlapi.cn/2/1-1.html或扫码访问来体验。
演示页面的展示区中心有一个“目标页面”的<a>元素链接,点击这个链接,会在新标签页打开一个新页面,此时,无视这个页面,再切换回刚才的入口页面,就会发现页面中多了一些内容,如图2-1所示,同时地址栏中也多了#hack字符。
图2-1 页面新增内容示意
这种Web攻击在技术上很低级,但效果却很显著,因为其就是利用开放的Web特性实现的,非常隐蔽,防不胜防,尤其在2021年之前。
为何是2021年之前呢?因为在2021年,Chrome浏览器调整了对新窗口页面的operer策略。
大家可能不知道,在2021年之前,不仅IE浏览器,就连Chrome浏览器,针对新窗口打开的链接,默认都是放开window.operer的。
也就是说,如果你给任意的一个外站链接设置target="blank",就像下面这样:
那么,只要这个域名是bbb.com的网站,就可以通过window.opener对来源页面为所欲为,无论是跳转到恶意网站,或是注入恶意信息,还是伪造用户身份,都只能看bbb.com网站的脸色。
所以,在前端圈子中有这么一个技术常识,设置了target="blank"的外站一定要设置rel="noopener",这样,外站页面的window.operer就是null,也就没有安全隐患了。
然而,还是那句话,过去的技术常识在如今的年轻前端开发群体中属于冷知识,又有几个人知道新窗口打开的外站链接一定要设置rel="noopener"呢?
所以,导致在整个互联网中绝大多数的网页都有这个安全隐患。
这显然是会出大问题的呀,既然改变不了开发者,那么浏览器自己改,于是从2020年开始,Safari、Firefox及Chrome相继调整了operer策略,即设置了target="blank"的链接元素默认不再是operer,而是noopener。
但是,目前在市面上占据一定份额的IE浏览器和Edge浏览器(非Chromium渲染内核的Edge浏览器)仍旧存在此安全隐患。
因此,即使是现在,也要切记,外链不信任的链接地址,只要是新窗口打开的,就一定要设置rel="noopener"。如果链接地址信得过,同时有跨窗体通信的需求,则可以设置rel="opener",这是最近几年才支持的新特性,虽然IE浏览器并不认识,但丝毫不影响其使用,因为IE浏览器新窗口打开链接的默认特性就是rel="opener"。
3.rel="noreferrer"
要想了解rel="noreferrer"的作用,就必须先了解document.referrer这个API。
document.referrer可以返回当前页面的来源地址。如果用户直接在浏览器的地址栏输入URL地址进行访问,或者通过设置了rel="noreferrer"的链接元素访问,那么document.referrer就会是空字符串。
所有知名的网站分析工具,如百度统计和谷歌分析,都是通过document.referrer来判断多少PV(页面访问量)来自搜索引擎,多少PV来自社交媒体,以及多少PV来自直接访问的。图2-2所示的就是某流量分析工具中各种流量来源对应的名称。
图2-2 某流量分析工具中各种流量来源对应的名称
当然,我们也可以借用document.referrer来完成一些日常开发任务,都是与细节体验相关的。
举两个例子。
例一,在移动端开发中,页面左上角往往会有一个“返回”按钮,但如果用户是通过点击某个分享链接进入的,那么这个返回的逻辑就不对,因为并没有上一页,此时这个“返回”按钮显示为“主页”按钮更合适。此时,我们就可以使用document.referrer来优化此细节,如果document.referrer是空字符串,则点击左上角的按钮会回到首页;如果不是,则会返回上一页。
例二,在某列表页面,点击任意列表会进入详情页,然后希望再次返回(通过页面内链接,而非浏览器的“后退”按钮)到列表页的时候,页面依然定位在之前的滚动位置,但如果是从其他页面进入的,则滚动置顶。
有经验的前端开发人员应该遇到过类似的需求,我想,肯定有人通过在href地址上做文章来实现此需求,例如加标志量:
这也能满足需求,但不够优雅,何不直接使用浏览器自带的API呢?比如(假设详情页的地址是detail.html):
关于此需求,我专门做了个演示页面,可通过在浏览器中输入地址https://www.htmlapi.cn/2/1-2.html或扫码访问来体验。
可以看到,从详情页返回和从首页返回时的列表的滚动状态是不一样的,就是因为列表页使用document.referrer判断页面来源并做了不同的处理。
那么什么时候需要给链接设置rel="noreferrer"呢?
首先,可以肯定的是,对于所有的本站链接都不要设置rel="noreferrer",因为会影响用户访问路径的追踪,进而影响页面的流失率等数据的统计。
其次,对于外站链接,个人建议是,全部都设置rel="noreferrer",因为URL地址中也包含了大量的信息,甚至有隐私内容(如搜索结果落地页的URL会包含搜索关键词),泄露出去会增加不必要的风险。
由于对于外站链接必须设置rel="noopener",因此,我们会经常看到如下所示的HTML代码,也就是noopener和noreferrer值同时设置,彼此使用空格分隔:
再次,如果你的产品是社交媒体类的,例如微博、知乎等,那么可以不设置rel="noreferrer",因为这些产品本身是开放的,以社交为主,更看重信息的传播,暴露referrer信息反而有助于第三方网站溯源,可以间接增加访问量和热度。
上面两种说法好像有矛盾,难道社交网站就不担心隐私信息泄露了吗?隐私泄露对于用户也是不友好的,不是吗?
没错,你所忧虑的情况确实存在,rel="noreferrer"的确存在不足,其设置与否的效果就像是0和1,并没有折中的说法,也就是说,无法兼顾信息传播和隐私保护。
所以,各大浏览器开发商遵循规范指引,从2016年开始,陆续支持一个全新的关于referrer策略的HTML属性referrerpolicy,它可以让我们兼顾信息传播和隐私保护,关于这一点,会在第2.1.5节介绍,不容错过。
最后,虽然以下这个使用场景很少见,但确实存在,那就是链接地址直接是外部图片,而这个图片设置了防外链,此时,给链接元素设置rel="noreferrer"会有神奇的作用。
HTML代码示意:
点击第一个链接,会发现图片无法显示,但是点击第二个链接,图片就可以显示了,如图2-3所示。
可以通过在浏览器中输入地址https://www.htmlapi.cn/2/1-3.html或扫码访问来体验对应的演示页面,需要注意的是,一旦图片被浏览器缓存,点击第一个链接依然可以显示,所以体验的时候,一定要先点击第一个链接,或者及时清空浏览器的缓存。
至于原因,这里暂时卖个关子,会在第4.1.4节做详细说明。
图2-3 noreferrer的设置与图片的显示
4.relList对象
前文出现过一段HTML属性设置,即rel="noopener noreferrer",对于这种写法,不知道大家有没有觉得有些不自然或者有些特别。
如果你有这样的感觉,那就对了。
因为rel属性是所有HTML标准属性中为数不多的支持使用空格分隔多个值的HTML属性。另外一个常见的支持空格分隔的属性就是class属性,大家肯定都用过它,毕竟绝大多数的CSS样式都是使用类选择器进行匹配的。
rel属性和class属性不仅在HTML层面的语法上一致,在DOM API层面也是极其相似的。
比如,页面中有如下HTML元素:
我们可以使用名为classList的属性对类名进行增删操作,例如下面这行JavaScript代码就可以移除类名active。
同样,如果我们希望移除rel属性中的noreferrer,也使用类似的语法,只是属性名不再是classList,而是relList,示意如下。
无论是classList,还是relList,都有一个专门的接口,叫作DOMTokenList,支持如下属性和方法,上面演示的remove方法只是其中之一。
属性:
DOMTokenList.length
DOMTokenList.value
方法:
DOMTokenList.item()
DOMTokenList.contains()
DOMTokenList.add()
DOMTokenList.remove()
DOMTokenList.replace()
DOMTokenList.supports()
DOMTokenList.toggle()
DOMTokenList.entries()
DOMTokenList.forEach()
DOMTokenList.keys()
DOMTokenList.values()
限于篇幅,这些属性和方法的含义就不在这里展开介绍了,有兴趣的读者可以去查找对应的文档。
可见,rel属性要比很多人预想的要强大得多,具有其他HTML属性所没有的、独立的API。