禁止网页在新窗口打开链接

2022-11-05 ⏳3.1分钟(1.3千字)

很多网页(尤其是简中互联网)强行在新窗口(或者新标签页,下面统称新窗口)打开链接。有人觉得这种做法很方便,可以保留原始网页。但我不喜欢这种方式。因为我用 Firefox,如果是在新窗口或者标签页打开网页就无法使用触摸板滑动返回到上一个页面。今天就整理一下强制在当前窗口或者网页打开链接的方法,分享给大家。

在分享方法之前,我们先看看有哪些方法可以让浏览器在新窗口打开标签。

新窗口开标签的方案

最简单的办法是给所有的超链接<a>添加target="_blank"属性。这也是最常见的办法。

给每一个<a>标签都添加target有点浪费,修改的时候也不方便,还会增加 HTML 文件的体积。一个优化方案是给<base>标签添加target属性。修改<base>标签会影响到所有的相对链接。也就是说这种方法只对内链接有效。

第三类方案是使用 JavaScript 动态修改超链接的行为。

最简单的是给每一个<a>标签指定onclick回调函数,在函数里调用window.open(url, '_blank')打开新页面。

稍微优化一点的方案是给document注册click事件处理函数,根据事件的target提取<a>标签的href属性,然后再调用open()函数打开新页面。这种方案需要点击事件冒泡document结点才行。

以上都是通过改变<a>标签的行为来打开新页面,<a>标签的href属性就是目标链接。但是有部分内容平台不会把目标网址写到href属性,而是把网址写成形如https://example.com?target=real-url的链接。用户点击的时候会先跳转到一个中间页,二次点击才能跳转到目标网页,非常不方便。

解决思路

我希望用统一的方式处理上述几类问题。最早是用了一个叫 Death To _blank 的 Chrome 插件。这个插件功能非常简单,就是删除<a>标签的target属性。但是它不能覆盖剩余几种情况。后来我迁移到 Firefox 浏览器,没有找到类似的扩展。最终我采用油猴脚本来实现全部功能。

油猴脚本框架

脚本框架如下:

// ==UserScript==
// @name     Remove target="_blank"
// @version  1
// @grant    none
// @match    *://*/*
// @run-at   document-end
// ==/UserScript==
(function (window) {
  "use strict";
  // todo...
})(window);

@run-at document-end 表示页面加载完成后才运行该脚本,@match *://*/*表示对所有网页都执行该脚本。

处理target属性

然后就是真正在处理逻辑。首先要清除<base>标签的target属性:

let base = document.querySelector('base');
if (base) removeAttribute('target');

并不是所有网页都会设置<base>标签,所以需要判断一下。

接下来就需要清理每个<a>标签上的target属性。但是网页的上的超链接可能比较多,而我们真正要点击的可能只有少数,或者根本不会打开。如果一上来就遍历所有链接并清理target属性就有点浪费。为此,我采用延迟的处理方式。简单来说就是监听documentmousedown事件,事件触发后判断当前节点是不是<a>标签。有的网页会在<a>标签中嵌入图片、行内代码等标签,所以mouseover事件的target不一定是<a>标签。为此还要遍历当前节点的父节点,看有没有<a>标签。

完整检查逻辑如下:

document.addEventListener('mouseover', function (event) {
  var a = event.target, depth = 3;
  
  // 当前 html 标签可能不是 a,需要递归查询其父标签。
  while (a && a.tagName != 'A' && depth-- > 0) { a = a.parentNode; }
  if (a && a.tagName == 'A') {
    // todo...
  }
}, true);

如果只设置了target属性,我们可以直接清除:

a.removeAttribute('target');

处理onclick回调

如果是设置了onclick回调或者在document监听所有<a>点击事件,我们可以清除回调并禁止click事件冒泡:

a.onclick = (e) => e.stopImmediatePropagation();

不过这种方法副作用很大。有些网页和框架会把<a>当按钮用,通常它的href会是#,如果禁止鼠标事件冒泡会影响正常功能。但确实有少数网站(某乎)会通过 JavaScript 拦截<a>的跳转。我是在代码里根据document.domain来判断是否禁用冒泡。大家可以根据自己具体情况调整代码。

处理中间跳转页面

对于那些有中间跳转页面的链接,我们需要提取目标链接并更新href属性:

var u = new URL(a.href);
var p = u.searchParams
var t = p.get("target") || p.get("to");
if (t) { a.href = t; }

完整代码

把所有功能都组合起来得到完整代码:

// ==UserScript==
// @name     Remove target="_blank"
// @version  1
// @grant    none
// @match    *://*/*
// @run-at   document-end
// ==/UserScript==
(function (window) {
  "use strict";
  let base = document.querySelector('base');
  if (base) removeAttribute('target');

  document.addEventListener('mousedown', function (event) {
    var a = event.target, depth = 3;

    while (a && a.tagName != 'A' && depth-- > 0) {
      a = a.parentNode;
    }
    
    if (a && a.tagName == 'A') {
      a.removeAttribute('target');
      a.removeAttribute('onclick');
      if (document.domain.endsWith(".zhihu.com")) {
        a.onclick = (e) => e.stopImmediatePropagation();
      }
      var u = new URL(a.href);
      var p = u.searchParams
      var t = p.get("target")||p.get("to");
      if (t) { a.href = t; }
    }
  }, true);
})(window);

最后主是在油猴插件里安装并启用上面的脚本。一切都清静了😄