文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结 投诉

文章更新提示

对于 Vue 动态生成的部分属性做了兼容处理,解决了样式错乱问题

这是什么

如标题所说,一个跳转提示页,可以防止用户直接访问站外链接,而是跳转到一个提示页,让用户先确认是否进入。许多知名站点,如 知乎掘金 等,都提供了这样的提示页。

本站的跳转提示页本站的跳转提示页

那为什么需要这个功能?我问了下 GPT,具体有以下几点:

  • 当你的博客直接链接到质量低下或者和内容不相关的外部网站时,搜索引擎可能会对你的网站质量评分产生负面影响。通过中转页面,外部链接并不是从你的主要内容页面直接指出,这样可以在一定程度上降低直接关联的负面影响。
  • 搜索引擎优化( SEO )越来越关注用户体验。中转页面上,如果能够提供清晰的提示,告诉用户他们即将离开原网站,可以避免用户感到迷惑,从而提高用户对网站的印象。搜索引擎会识别到用户在你的网站上的正面互动,可能帮助提升你的网站排名。
  • 过多的直接外部链接有可能被搜索引擎视为链接农场的一部分,尤其是如果那些链接没有提供实质性帮助或与内容不相关时。通过中转页面,可以减少主要内容页面上的直接外部链接数,帮助维持网站的专业形象。
  • 通过中转页面还可以防止部分风险或违反相关法律的链接被用户直接访问到,也可以在一定程度上帮助网站管理员追踪和审核通过其网站传播的链接,及时发现和处理违规内容,以符合相关的法律法规。

至于我,其实是看到许多博主都搞了这样的跳转提示页,就觉得挺有意思的,于是想自己也搞一个。

失败的尝试

说干就干,但是问题来了,其他博主要么使用的是 Typecho,要么是 Hexo,基本上都有对应的插件来实现这个功能,但是我使用的是 Vitepress,插件?根本没有,那就只能自己搞了。

由于 Vitepress 是基于 Vue 的,所以,最初打算在 onMounted 这个生命周期中,查找到全部的 <a> 标签,然后遍历这些标签,判断是否具有 target="_blank" 属性,如果有,则证明这个链接是站外链接,那么就进行替换。

但是这样有一个问题,在搜索引擎爬取网站时,并不能触发 Vue 的生命周期函数,所以,爬取到的页面仍旧为原链接,这样可不行,只能另寻他法。

最后,在翻看 vp 的文档时,找到了名为 transformHtml构建钩子

transformHtml 是一个构建钩子,用于在保存到磁盘之前转换每个页面的内容。

那就好办了,这个钩子是在渲染页面时触发的,那就可以在 transformHtml 中替换标签内容了。

放弃正则替换

最初想的是用正则替换,于是请教了下 GPT ,结果给出了下面这一大坨:

js
const regex =
  /<a(?=[^>]*?href=['"]([^'"]*)['"])(?=[^>]*?target=['"]_blank['"])(?:(?=[^>]*?class=['"]([^'"]*)['"]))?[^>]*?>(.*?)<\/a>/g;

说实话,我一直没搞懂正则怎么写,甚至看不太懂这个正则 (●'◡'●),并且后面用正则的查找和替换也是个麻烦事,那就只能另寻他法。

最佳实践

由于不用正则,那就只能用我热榜项目用来解析 htmlcheerio 库了,试了一下,还真可以,并且代码清晰了不少。

准备就绪,那让我们来实现这个方法:

js
/**
 * 跳转中转页
 * @param {string} html - 页面内容
 * @param {boolean} isDom - 是否为 DOM 对象
 */
export const jumpRedirect = (html, isDom = false) => {
  try {
    // 是否启用
    if (!themeConfig.jumpRedirect.enable) return html;
    // 中转页地址
    const redirectPage = "/redirect.html?url=";
    // 排除的 className
    const excludeClass = themeConfig.jumpRedirect.exclude;
    if (isDom) {
      if (typeof window === "undefined" || typeof document === "undefined") return false;
      // 所有链接
      const allLinks = [...document.getElementsByTagName("a")];
      if (allLinks?.length === 0) return false;
      allLinks.forEach((link) => {
        // 检查链接是否包含 target="_blank" 属性
        if (link.getAttribute("target") === "_blank") {
          // 检查链接是否包含排除的类
          if (excludeClass.some((className) => link.classList.contains(className))) {
            return false;
          }
          const linkHref = link.getAttribute("href");
          // 存在链接且非中转页
          if (linkHref && !linkHref.includes(redirectPage)) {
            // Base64
            const encodedHref = btoa(linkHref);
            const redirectLink = `${redirectPage}${encodedHref}`;
            // 保存原始链接
            link.setAttribute("original-href", linkHref);
            // 覆盖 href
            link.setAttribute("href", redirectLink);
          }
        }
      });
    } else {
      const $ = load(html);
      // 替换符合条件的标签
      $("a[target='_blank']").each((_, el) => {
        const $a = $(el);
        const href = $a.attr("href");
        const classesStr = $a.attr("class");
        const innerText = $a.text();
        // 检查是否包含排除的类
        const classes = classesStr ? classesStr.trim().split(" ") : [];
        if (excludeClass.some((className) => classes.includes(className))) {
          return;
        }
        // 存在链接且非中转页
        if (href && !href.includes(redirectPage)) {
          // Base64 编码 href
          const encodedHref = Buffer.from(href, "utf-8").toString("base64");
          // 获取所有属性
          const attributes = el.attribs;
          // 重构属性字符串,保留原有属性
          let attributesStr = "";
          for (let attr in attributes) {
            if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
              attributesStr += ` ${attr}="${attributes[attr]}"`;
            }
          }
          // 构造新标签
          const newLink = `<a href="${redirectPage}${encodedHref}" original-href="${href}" ${attributesStr}>${innerText}</a>`;
          // 替换原有标签
          $a.replaceWith(newLink);
        }
      });
      return $.html();
    }
  } catch (error) {
    console.error("处理链接时出错:", error);
  }
};

说下这个函数大致都做了什么:

  • isDom:由于 transformHtml 钩子是在 Node.js 环境下执行的,所以需要判断当前环境是否为 Node.js。从而采用不同方案( 详见下文 )
  • excludeClass:将要排除的标签类名,比如友链的类名
  • encodedHref:将真实地址改为 base64 编码格式

中转页面

说了这么多,唯独缺少了最重要的 —— 跳转页面,其实这个页面就是一个静态页面,你可以自行实现,或者直接使用第三方的页面,比如知乎。( 我觉得不好看 )

这里给出本站的跳转页面,你可以参考或者直接使用:

引用站外地址,请注意甄别链接安全性

评论系统兼容

由于评论系统都是在页面渲染后再生成的,并且对于评论区的外链进行中转也是必要的,所以需要对这个特殊的情况做特殊处理。

通常情况下,各个评论系统都有相应的 Event 事件,以本站使用的 Artalk 举例,你可以在文档中找到关于 Event 事件的说明。

Artalk 提供了一个名为 list-loaded 的事件,当评论列表加载完成后会触发该事件:

js
artalk.on("list-loaded", () => {
  // 在此调用替换函数
  jumpRedirect();
});

如你所见,我上方提供的函数也对这种情况进行了兼容处理,只需在调用函数时将 isDom 参数传入 true 即可。

js
jumpRedirect(null, true);

完美收工!( 可能还会有问题,等出现了在修吧 )

赞赏博主
评论 隐私政策