静态博客如何高性能插入评论

Featured Image

🌏 前言

我们知道,静态博客由于不带有动态功能,所以针对评论这种动态需求比较大众的做法就是使用第三方评论系统。第三方评论的本质其实就是使用 JS 去调取第三方服务接口获取评论后动态渲染到页面中。虽然它很好的解决了这个问题,但是由于需要请求接口,在体验上远比动态博客的直出效果要差很多。所以当我把博客从动态博客 Typecho 迁移到静态博客 Hugo 上来时,就一直在思考这个问题。直到我看到了 Hugo 的 getJSON 方法,发现原来静态博客也是能够像动态博客一样直出评论的。

大部分的静态博客的原理是解析存储内容的文件夹,使用一些模板语言遍历数据生成一堆 HTML 文件。而 Hugo 除了解析 Markdown 内容之外,还支持额外的数据获取方法 getJSON。由于有了 getJSON 方法的出现,我们可以实现在博客编译构建过程中动态的去获取评论接口数据,将其渲染到页面中,实现评论数据的直出效果。关于 getJSON 的更多介绍,可以查看 Hugo 文档数据模板一节。

🎃 方案

高性能方案基本思路是在需要评论数据的地方通过 getJSON 方法调用接口获取评论数据并进行模板渲染。当评论更新的时候,我们需要触发重新构建。实现这个方案依赖三个关键要素:

  1. 构建过程支持调取接口获取数据
  2. 评论服务提供 HTTP 接口返回数据
  3. 博客部署服务支持钩子触发重新构建

我的博客使用的是 Hugo 静态博客系统,如上文所说通过 getJSON 即可解决第一个问题。而我的评论服务使用的是自研的 Waline 评论系统,它提供了评论数、评论列表、最近评论等基础接口满足我们的数据获取需求。并且 Waline 提供了丰富的钩子功能,支持在评论发布的时候触发自第一方法。我的博客部署在 Vercel 上,它提供了 Deploy Hooks 功能,通过 URL 即可触发重新构建。也就是说我只要在 Waline 评论发布的钩子中调用 Vercel 的钩子 URL 触发重新构建即可解决第三个问题。

🥪 实现

我的博客上有三处地方和评论有关,分别是首页侧边栏的最近评论,文章标题下方的评论数,以及文章详情页底部的评论列表展示。

🍞 最近评论

Waline 最近评论接口:文档

{{ $walineURL := .Site.Params.comment.waline.serverURL }}
<h2 class="widget-title ">最近回复</h2>
<ul class="widget-list recentcomments">
  {{ $resp := getJSON $walineURL "/comment?type=recent" }}
  {{ range $resp }}
  <li class="recentcomments">
    <a href="{{.Site.BaseURL}}{{ .url }}">{{ .nick }}</a>:{{ .comment | safeHTML | truncate 22 }}
  </li>
  {{ end }}
</ul>

🧀 文章评论数

Waline 获取文章对应的评论数接口:文档

{{ $walineURL := .Site.Params.comment.waline.serverURL }}
{{ $count := getJSON $walineURL "/comment?type=count&url=/" .Slug ".html" }}
<a href="{{ .Permalink }}#comments" title="{{ .Title }}">
  <i class="fas fa-comment mr-1"></i>
  <span>{{- if gt $resp 0}}{{$resp}} 条评论{{else}}暂无评论{{end -}}</span>
</a>

🍯 评论列表

评论列表由于有分页的存在,不像最近评论和评论数一样简单的调用接口即可。先获取评论数,发现有评论时先获取第一页的评论,主要是用来获取总共有多少页评论。之后再从第二页开始循环获取评论数据。最终将获取到的数据全部存到 {{$scratch.Get "comments"}} 数组中,使用模板语法渲染该数组数据即可。

{{$baseUrl := .Site.Params.comment.waline.serverURL}}
{{$slug := .Slug}}
{{$count := getJSON $baseUrl "/comment?type=count&url=/" $slug ".html" }}
{{$scratch := newScratch}}
{{$scratch.Add "comments" slice}}

{{if gt $count 0}}
  {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&page=1&pageSize=100"}}
  {{range $cmt := $comments.data}}
    {{$scratch.Add "comments" $cmt}}
  {{end}}

  {{$totalPages := $comments.totalPages}}
  {{if gt $totalPages 1}}
    {{range $page := seq 2 $totalPages}}
      {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&pageSize=100&page=" $page}}
      {{range $cmt := $comments.data}}
        {{$scratch.Add "comments" $cmt}}
      {{end}}
    {{end}}
  {{end}}
{{end}}

<div class="vcards">
  {{range $cmt := $scratch.Get "comments"}}
  <div class="vcard" id={{$cmt.objectId}}>
    <img class="vimg" src="https://gravatar.loli.net/avatar/{{$cmt.mail}}?d=mp">
    <div class="vh">
      <div class="vhead">
        <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a>
        <span class="vsys">{{$cmt.browser}}</span>
        <span class="vsys">{{$cmt.os}}</span>
      </div>
      <div class="vmeta">
        <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span>
        <span class="vat">回复</span>
      </div>
      <div class="vcontent" data-expand="查看更多...">
        {{$cmt.comment | safeHTML}}
      </div>
      <div class="vreply-wrapper"></div>
      <div class="vquote">
        {{range $cmt := $cmt.children}}
        <div class="vh" id="{{$cmt.objectId}}">
          <div class="vhead">
            <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a>
            <span class="vsys">{{$cmt.browser}}</span>
            <span class="vsys">{{$cmt.os}}</span>
          </div>
          <div class="vmeta">
            <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span>
            <span class="vat">回复</span>
          </div>
          <div class="vcontent" data-expand="查看更多...">
            {{$cmt.comment | safeHTML}}
          </div>
          <div class="vreply-wrapper"></div>
        </div>
        {{end}}
      </div>
    </div>
  </div>
  {{end}}
</div>

🍳 构建触发

Waline 在评论发布、更新和删除阶段都支持自定义钩子,在钩子中触发 Vercel 的构建钩子即可完成发布评论重新构建的流程。

按照如下内容修改服务端部署的 index.js 文件,查看文档了解全部的 Waline 钩子。

const Waline = require('@waline/vercel');
const https = require('https');
const buildTrigger = _ => https.get('https://api.vercel.com/v1/integrations/deploy/xxxxx');

module.exports = Waline({
  async postSave(comment) {
    if(comment.status !== 'approved') {
      return;
    }
    buildTrigger();
  },
  async postUpdate() {
    buildTrigger();
  },
  async postDelete() {
    buildTrigger();
  }
});

🍾 后记

通过以上操作,就能在不损失用户体验的情况下实现评论数据的动态支持了。有些人可能会担心是否会在构建阶段造成超多的接口请求。这里大可不用担心,Hugo 自己会在构建的时候做接口的缓存,同 URL 的接口调用会走缓存数据而不会重新调用。

除了用户体验之外,由于只会在构建的时候触发数据的获取,针对有调用次数配额的第三方评论服务也能节省额度。当然,理论上构建次数是远小于访问次数的,所以额度节省的结论是能成立的。如果说你的构建次数要比访问次数还要大的话,那这种方法就无法节省额度了。

当然这种方式也会有带来些问题,主要是评论的更新没那么快。好在 Hugo 的构建速度非常快,一两分钟的时间也能接受。而针对用户评论的发布,则可以通过评论发布后先假插入缓解该问题。

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

15 评论

json format Chrome 88.0.4324.182 Windows 10
2021-02-23T09:13:47.000Z 回复

学习了。

json format Chrome 88.0.4324.182 Windows 10
2021-02-23T09:09:32.000Z 回复

还是Disqus简单啊=0=

怡红公子 Chrome 88.0.4324.192 Mac OS 10.15.6
2021-03-02T14:58:18.375Z 回复

@json format , 嗯,我这个是比较极客的接入方式了,Waline 也支持像 Disqus 一样通过 JS 脚本载入的。

小柚子 Edge 87.0.664.75 Windows 10
2021-01-11T05:37:20.221Z 回复

由我来触发一次构建。构建好了CDN缓存还得刷新啊,又要回源了。

怡红公子 Mobile Safari 14.0.3 iOS 14.4
2021-01-11T05:43:08.837Z 回复

@小柚子 , 我的博客现在设置的是评论需要人工审核,审核通过之前是不会触发构建的。CDN 刷新可以找一下看看有没有接口,如果没有的话那可能这个方案就不太合适了。

屠夫9441 Edge 87.0.664.66 Windows 10
2021-01-04T02:35:24.122Z 回复

太强大了……技术小白还在傻呵呵地用Disqus😂

怡红公子 Chrome 87.0.4280.88 Mac OS 10.15.6
2021-01-04T02:47:40.357Z 回复

@屠夫9441 , 哈哈,Disqus 如果不用代理的话,其实倒是首选。我在这篇文章里讲的是比较 Geek 的做法了,其实可以像 Disqus 一样通过 JS 脚本的形式引入 Waline 的,你感兴趣的话可以试试。https://waline.js.org 有问题可以随时反馈哦~

Hien Mobile Safari 14.0.2 iOS 14.3
2021-01-01T06:44:11.084Z 回复

Can it show images in comments?

怡红公子 Chrome 87.0.4280.88 Mac OS 10.15.6
2021-01-01T06:48:03.685Z 回复

@Hien , Of course! You can use Markdown grammar ![]() to show a image. And we also support upload Image when you paste a image into comment box.

Hien Mobile Safari 14.0.2 iOS 14.3
2021-01-01T11:54:01.663Z 回复

@怡红公子 , Really interesting! Please make an English version for the installation :) I like this a lot.

怡红公子 Chrome 87.0.4280.88 Mac OS 10.15.6
2021-01-01T12:02:09.731Z 回复

@Hien , I’m very happy you are like it! Waline have a full english documentation at https://waline.js.org/en/quick-start.html, and it supports i18n at comment box.

If you have any question about it, you can post your question into issue or discussion, also our telegram group walinejs is opening all times!

阳光笔记 Firefox 84.0 Windows 7
2020-12-31T04:42:52.779Z 回复

发现这几年出了好多博客程序,都是开源免费,不像刚接触的时候只有WP

怡红公子 Chrome 87.0.4280.88 Mac OS 10.15.6
2020-12-31T04:59:03.103Z 回复

@阳光笔记 , 嗯,现在做静态博客比较简单,只要能生成列表页和文章页那就能算博客了,所以造轮子的人也多。之前也有,只是出名的比较少。 ps. 你的博客的皮肤是我之前用了好多年的皮肤,哈哈~

老陳网志 Chrome 86.0.4240.193 Windows 10
2020-12-29T08:36:00.746Z 回复

哈哈,也入坑Hugo了啊~

怡红公子 Chrome 87.0.4280.88 Mac OS 10.15.6
2020-12-29T11:06:10.167Z 回复

@老陳网志 , 嗯,是的,看了下你的,发现你也换 Hugo 了啊,不过老数据没了有点可惜了。