靜態博客如何高性能插入評論

🌏 前言

咱們知道,靜態博客因爲不帶有動態功能,因此針對評論這種動態需求比較大衆的作法就是使用第三方評論系統。第三方評論的本質其實就是使用 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 的構建速度很是快,一兩分鐘的時間也能接受。而針對用戶評論的發佈,則能夠經過評論發佈後先假插入緩解該問題。

相關文章
相關標籤/搜索