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

🌏 前言

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

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

🎃 方案

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

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

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

🥪 实现

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

🍞 最近评论

Waline 最近评论接口:文档app

{{ $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 获取文章对应的评论数接口:文档async

{{ $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"}} 数组中,使用模板语法渲染该数组数据便可。post

{{$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 钩子。ui

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 的构建速度很是快,一两分钟的时间也能接受。而针对用户评论的发布,则能够经过评论发布后先假插入缓解该问题。

相关文章
相关标签/搜索