上个月,官网面经迁移踩的一些坑

大家好,我是雷布斯。

我们的公众号上发布过很多面经,而且在手机版微信中访问时,点击面经中的题目链接,能跳到对应的题目直接查看答案,确实很方便。

但很多同学也在反馈,平时在公司摸鱼刷题时,如果通过我们的官网查看这些面经,会提示需要去微信中打开,不太友好。由于我们有 PC 上的刷题网站,能不能在 PC 上直接跳转到官网上对应的题目呢?

上个月,我花了半天的时间解决了这个问题,今天就给大家分享下怎么具体的方案。

同学们可以先在电脑上访问:https://fe.ecool.fun/articles/technology/414 查看最终的效果。

方案制定

明确下我们的最终目标:为了实现公众号中的链接,在不同的平台上,跳转到不同的地址。也就是手机微信中访问,就跳转到小程序,PC 上访问,就跳转到官网。

大家首先会想到,可以做个中间页,公众号都加这个中间页的地址,在中间页识别平台后,做不同的跳转。

这个方案其实在几年前就考虑过,但由于我们的文章需要支持在小程序中访问(小程序中有面经、技术文章等栏目),如果用中间页的方案,由于小程序属于个人,不支持使用 webview 访问自己的中间页,这个方案只能无奈放弃。

之前官网上的文章,都是跳转到微信公众号进行查看,如果改为自己渲染文章内容,就可以自定义跳转链接这块的逻辑,实现我们的目标,而且还有利于网站的 SEO。

按这个思路,我们做以下工作:

  1. 爬取和存储文章内容,也就是将文章内容存到自己的数据库。
  2. 处理文章中的跨域资源,比如文章中有不少图片资源,微信的图片会因为防盗链策略,限制第三方的网站引用,我们需要解决这个问题。
  3. 支持 SSR 访问,这是做 SEO 比较重要的一点。

数据爬取

首先是要获取文章内容。

随便打开一篇公众号的文章,以 最近的一篇文章 为例,直接查看网页源代码,我们将文章内容作为关键字进行查找,可以在页面中发现对应的区域。

正常情况下,我们可以用正则等方式,获取到完整的文章内容,但后来发现在上面的文章链接后拼上 f=json 后,就会直接以 json 的形式返回完整的文章内容:

这样就能很简单地获取到文章信息了。

接下来应该就是枯燥的爬取全量数据的环节,但我们换个思路,完全可以先读数据库,如果没有正文内容,就通过上面的接口获取并存储到数据库。

用这个思路,可以节省一些写爬虫脚本的时间,完美~

图片跨域

接下来就是将页面渲染出来。

我们拿到正文内容其实是一段 html 代码,在 React 中展示也很建单,直接放到标签的 dangerouslySetInnerHTML 属性中就行。

<div dangerouslySetInnerHTML={{ __html: finalHtml }}></div>

但事情往往不会这么一帆风顺,首先是遇到了页面中的图片因跨域无法加载的问题,页面中的图片都变成了这个:

根据我们之前分享的《防盗链的矛与盾》的介绍,这种在个人网站上使用第三方的图片资源的行为,就属于盗链了。

解决的方法也很多,最简单的使用空白的 referer 访问。

<meta name="referrer" content=“never”>
// or 
<img referrer="no-referrer" src="xxxx"/>

我选择的是修改 img 标签的 referrer 属性,直接对 html 片段中的相关属性值进行设置:

const parser = new DOMParser();
// 解析 HTML 文本为 DOM 文档对象
const doc = parser.parseFromString(html, 'text/html');
// 找到所有的 img 标签
const imgElements = doc.getElementsByTagName('img');
// 遍历每个 img 标签并修改其 src 属性
for (let i = 0; i < imgElements.length; i++) {
  const img = imgElements[i];
  // 将 img 的 src 属性设置为您想要的自定义内容
  img.setAttribute('src', img.getAttribute('data-src') || '');
  img.setAttribute('referrerPolicy''no-referrer');
}

const finalHtml = doc.documentElement.outerHTML

改完后就能正常加载大部分的图片了,但页面上的背景图都没法展示。

背景图处理

这就是在《防盗链的矛与盾》 中提到的:

在 Chrome 中,即使页面上设置 ,background-image 中的图片请求还是会带上 referer,但火狐中不会带上。

既然 referrer 属性对 background-image 不生效,那我们就用代理的方法解决。

新增一个图片转发的接口,以 egg.js 中的实现为例:

async transmit(url) {
  const { ctx } = this;

  const originRes = await ctx.curl(url, {
    responseType: "arraybuffer",
  });

  ctx.set("content-type", originRes.res.headers["content-type"]);

  return originRes.data;
}

同时对 html 片段中的图片地址进行处理:

// 获取所有元素节点,解决微信图片作为 background-image 被拦截的问题
const elements = doc.querySelectorAll('*');
// 循环遍历元素
for (let i = 0; i < elements.length; i++) {
  const element = elements[i];
  // 检查元素是否设置了 background-image 属性
  const backgroundImage = element.style.backgroundImage;
  if (backgroundImage?.indexOf('https://mmbiz.qpic.cn/') > -1) {
    // 使用正则表达式在原有的属性值前添加前缀
    const modifiedValue = backgroundImage.replace(
      /url\(['"]?(.*?)['"]?\)/,
      'url("
' + 'https://xx.xxx.xxx/xxx?url=' + '$1")',
    );
    // 修改 background-image 属性值为新的值
    element.style.backgroundImage = modifiedValue;
  }
}

跳转链接修改

页面的渲染问题已经基本解决了,还需要处理面经中的跳转链接。

这块的处理也与上面比较类似,就是先从 a 标签中读取到小程序中的跳转地址,解析出题目 id 等参数,然后拼接成 PC 端的访问地址后进行回填,直接看代码:

// 找到所有的 img 标签
const aElements = doc.getElementsByTagName('a');
// 遍历每个 img 标签并修改其 src 属性
for (let i = 0; i < aElements.length; i++) {
  const a = aElements[i];
  const path = a.getAttribute('data-miniprogram-path');
  if (path?.indexOf('pages/exerciseDetail/index') === 0) {
    const srcParams = new URLSearchParams(path.split('?')?.[1] || '');
    if (!!srcParams.get('exerciseKey')) {
      a.setAttribute('href''/topic/' + srcParams.get('exerciseKey'));
      a.setAttribute('target''_blank');
    }
  }
}

SSR 踩坑

改到这儿,已经基本达成了我们的目标。

但在线上部署后,却出现了页面无法加载的问题,经过排查,是因为我们使用了 SSR ,而在 Node.js 环境中,没有上面使用的 DOMParser 对象。

确实很遗憾了,改了半天却发现 SSR 不好用。

那就只好使用大招:正则表达式替换。

以图片链接为例:

// 使用正则表达式匹配 img 标签的 data-src 属性
const imgRegex = /<img\s+[^>]*?data-src=['"](.*?)['"][^>]*?>/g;

finalHtml = finalHtml.replace(imgRegex, function (match, dataSrcValue{
  // 将 data-src 的值赋给 src
  return match.replace(
    'data-src="' + dataSrcValue + '"',
    'src="' + dataSrcValue + '" referrerPolicy="no-referrer"',
  );
});

最后

到这儿,就已经完成了迁移的全部工作。

我们在日常的工作中,解决问题时也可以参考这样的思路,想清楚自己面临的问题,思考各种解决方案的优缺点。在具体的技术问题上,也需要先了解问题产生的原因,做好个人的技术沉淀。

最后,还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。

老规矩,也给我们团队的辅导服务打个广告。