基於git的博客(含站點與小程序)

1 效果

2 需求分析

2.1 作

  • 專一於寫markdown文檔,或者說專一於內容。
  • 一端書寫,多端同步:小程序、靜態站點
  • 快速的靜態託管、持續集成、頁面資源加載

2.2 不作

  • 不須要知足隨時隨地寫文章,由於隨時隨地寫的大部分是隨筆、記錄一類的帖子,若要呈現出來,必然要通過整理。
  • 不須要自定義主題風格,博客就主體業務類型(除了評論、點贊、收藏)而言受衆個性色彩不強

3 系統設計

3.1 概要設計

3.1.1 架構設計

主要思路是經過git管理文章(markdown類型),發佈到小程序和靜態站點(適用於構建md文檔的框架如hexo、jeklly等)。
技術路線:javascript

  1. 更新 => 經過git進行源端CURD操做
  2. 解析 => 經過serverless完成md解析至html
  3. 同步 => 經過CI完成構建和部署到對象存儲(靜態託管));經過webhook拉取git倉庫更新到雲存儲(小程序)
  4. 瀏覽 => 經過對象存儲觸發雲函數刷新CDN(靜態託管);經過rich-text組件解析html(小程序)

3.1.2 技術選型與開發框架

在開發框架上,因爲初期面向微信小程序開發且可能存在未知問題,故使用原生開發,不使用多端或其餘預編譯框架。在小程序UI上,參考但不依賴WeUI組件庫,因因爲封裝沒必要要的特性可能形成代碼包的冗餘。html

類型 方案 備註
代碼託管 Coding github api訪問較大機率慢且不穩定
雲開發 騰訊雲TCB 含小程序雲開發服務
持續集成 Coding CI 使用Jenkinsfile定義pipeline
靜態託管 騰訊雲COS 也可以使用阿里雲OSS,或直接使用雲開發提供的靜態網站託管,使用對象存儲配合內容分發加速。
Markdown解析 markdown-it 也可以使用markdjs,但markdown-it支持拓展插件
富文本渲染 parser 比原生rich-text功能豐富且效果穩定

3.1.3 界面設計

因爲是內容類應用,須要格外注意視覺規範,以使用戶獲取較好的閱讀體驗。如下規範參考了WEDESIGNAnt Design,根據實際須要進行了修改和補充。
字體:java

字號pt 像素px 顏色 用途
17 17 #000000 頁面內首要層級信息,列表標題
17 17 #B2B2B2 時間戳與表單缺省值
14 14 #888888 頁面內次要描述信息,搭配列表標題
14 14 #353535 大段文本
13 13 #576b95 頁面輔助信息,需弱化的內容如連接
13 13 #09bb07 完成字樣
13 13 #e64340 出錯字樣
11 11 rgba(0, 0, 0, 0.3) 說明文本,如版權信息等不須要用戶關注的信息

圖標:node

類別 顏色 大小
導航類 可多色,但很少於三色,主色一致 28px
菜單操做類 單色,顏色統一 22px
操做提示類 與提示類型相關 30px
展現區分類 圖標固有色彩 與跟隨字體大小一致

響應式設計:
主要經過改變px爲rpx實現,因爲基本不涉及列表項目,不考慮自適應佈局變換,僅作不一樣屏幕下元素呈現比例保持一致,以iphone-6做爲標準,對於iphone-x類異形屏,重點考慮操做菜單(如貼頂、貼底、懸浮)的安全區域問題,主要經過CSS中calc(env(safe-area-inset-bottom))方式實現。ios

  • 圖片橫向鋪滿屏幕
  • 主體文字不須要
  • 因爲已經有靜態站點,暫不考慮PC端適配

3.1.4 開發規範

  • 漸進式,先實現基本功能,再考慮抽離和組件化。
  • 能用簡單的邏輯實現就不抽離組件,能使用成熟庫就不自行建立組件,能經過配置或遷就性使用就不修改外部庫以保證平滑更新。
  • 對於功能實現的方式,要考慮服務角色,權衡計算複雜度、網絡延時和用戶感知程度:

小程序端作簡單計算git

  • canvas繪製海報
  • 基本格式轉換

服務端(雲開發)作複雜處理,非實時性計算,或可預生成內容github

  • markdown轉html
  • TOC目錄
  • AI識別、處理
  • 對於讀寫數據庫,儘可能將寫操做放在雲函數中。

3.2 詳細設計

3.2.1 數據源

安全校驗,保證雲函數觸發來源及方式可信:web

// 查看請求頭
if (!req.headers['user-agent'].includes('Coding.net Hook') || 
    !('x-coding-signature' in req.headers) || req.headers['x-coding-signature'].indexOf('sha1=')
    !('x-coding-event' in req.headers) || 'POST' !== req.httpMethod ) {
  return false;
}
// 計算和比對簽名
const theirSignature = req.headers['x-coding-signature'];
const payload = req.body;
const secret = process.env.HOOKTOKEN;
const ourSignature = `sha1=${crypto.createHmac('sha1', secret).update(payload).digest('hex')}`;
return crypto.timingSafeEqual(Buffer.from(theirSignature), Buffer.from(ourSignature));

在每次commit推送新的代碼時,WebHook會push如下信息(限於篇幅,略去非必要信息)數據庫

{
  "ref": "refs/heads/master",
  "commits": [
    {
    "id": "8a175afab1cf117f2e1318f9b7f0bc5d4dd54d45",
    "timestamp": 1592488968000,
    "author": {
      "name": "memakergytcom",
      "email": "me@makergyt.com",
      "username": "memakergytcom"
    },
    "committer": {
      "name": "memakergytcom",
      "email": "me@makergyt.com",
      "username": "memakergytcom"
    },
    "added": [
      "source/_drafts/site.md"
    ],
    "removed": [],
    "modified": [
      "package.json",
      "scripts/fix.js",
      "source/_posts/next.yml",
      "source/_posts/typesetting.md"
    ]}
  ],
  "head_commit":{...},
  "pusher",
  "sender",
  "repository"
}

保持最新狀態故關注head_commit.這些信息包含了本次提交產生的變動,能夠基於遍歷這些變動狀態,同步雲數據庫。但因爲可能包含了非文章文件的變動,也可能非目標分支,故須要篩選:npm

if ('refs/heads/' + branch === ref) {
  if (filePath.indexOf(dirPrefix) || filePath.slice(-3) !== '.md') { // 路徑前綴和文章後綴
    continue;
  }
}

要創建數據庫文件與git倉庫文件的關聯,因爲每次commit的文件沒有惟一id信息,能夠經過文件名來創建聯繫,將文件名做爲slug字段(主鍵)

let slug = filePath.match(new RegExp(dirPrefix + "([\\s\\S]+)\\.md"))[1];

因爲Push 事件不包含文件內容,須要經過api發起請求

await axios({
  url: `${baseUrl}/${branch}/${filePath}`,
  method: 'get',
  headers: {
    'Authorization': `token ${process.env.CODINGTOKEN}` // 我的令牌
  }
});

3.2.2 數據處理

提取文章信息:
因爲要求在markdown開頭經過yaml格式寫明基本信息,故在獲取到文件內容(String)後須要轉json。

const matter = require('hexo-front-matter');
let { title, date, tags, description, categories, _content, cover } = matter.parse(data);

其中cover字段(封面圖)也可不聲明,而經過文章首圖來獲取

let cover = _content.match(/!\[.*\]\((.+?)\)/);

markdown解析html:
小程序端環境與傳統網頁有區別,讓markdown渲染在本地進行,其中還須要先轉爲html,爲了減小渲染時間,這一步在雲端提早進行:

const md = require('markdown-it')({
  html: true,// 容許渲染html
}).use(require('markdown-it-footnote')) // 腳註引用

生成目錄
爲了保持一致,章節自行標號。目錄放在側邊欄不解析到html中,需另行處理。而markdown-it-anchor插件會使用header的值做爲id(markdown-it-anchor),但id不能以數字開頭,不能含中文及encodeURIComponent(中文),但能夠含-

// 爲<h>標籤插入id
id = 'makergyt-' + crypto.createHash('md5').update(title).digest('hex');
// 獲取全部h2-h4生成目錄列表
const { tocObj } = require('hexo-util');
const data = tocObj(str, { min_depth:2, max_depth: 4 });

3.2.3 數據同步

在小程序的文檔中,觸發雲函數能夠經過http api(invokeCloudFunction)的方式。可是invokeCloudFunction須要關鍵的access_token,須要兩小時內刷新獲取,webhook沒法提早獲知。考慮設置中控服務器統一獲取和刷新 access_token,webhook首先向中控服務器發起請求,再向雲函數請求,但這樣顯然是不可能的,因其只能push一個地址一次,沒有上下文。其間再加一箇中間函數,那麼這個中間函數又放在哪裏,如何請求...(一樣須要access_token)

這時,在騰訊雲-雲開發控制檯,發現能夠直接經過"雲接入HTTP觸發方式"觸發雲函數,這樣就能夠直接該地址做爲WebHook的Url。但須要關注業務和資源安全[1],上文在處理webhook push事件時已經作了安全檢驗,能夠再將Coding的request domain加入到WEB安全域名列表中。

獲取到文章信息和內容後就能夠同步到雲數據庫的相應集合中,這裏循環中使用async/await遍歷,爲了在每一個調用解析以前保持循環,只使用for...of進行異步[2]

for (const file of added) {
  await db.collection('sync_posts').add({
    data
  })
}
for (const file of modified) {
  await db.collection('sync_posts').where({
    slug
  }).update({
    data
  })
}
for (const file of removed) {
  await syncPosts.where({
    slug
  }).remove();
}

3.2.4 文本渲染

幾乎不太可能將原內容原封不動顯示出來, 通過markdown-it渲染後的html字符串沒有插入任何樣式,直接測試(根據標籤默認提供樣式)效果以下:

方案 效果
rich-text 代碼塊缺失,長內容被截斷
wxparser 間距過大,表格、代碼塊被截斷
towxml 代碼塊被截斷
wemark 代碼塊與引用部分不換行拉寬
Parser 表格溢出

Tips: 注意到騰訊Omi團隊開發的小程序代碼高亮和markdown渲染組件Comi,實際上採用模板引入的方式使用。考慮隨後實測效果和對比渲染速度。

相比之下,都會出現溢出組件邊界,產生橫向滾動條問題。在使用上,存在不支持解析style標籤缺陷[3]
圖3-1 表格溢出

而Parser能夠經過控制源html樣式的方法解決這種問題

var document = that.selectComponent("#article").document;
document.getElementsByTagName('table').forEach(tableNode => {
  var div=document.createElement("div");
  div.setStyle("overflow", "scroll");
  div.appendChild(tableNode);
  div._path = tableNode._path;
  tableNode = div;
});

Parser也提供了經過控制源html中標籤樣式來影響渲染效果,這樣就能夠改變字體大小、行高、行間距等,以適應手機屏幕。

//post.wxml
<parser id="article" tag-style="{{tagStyle}}"/>
// post.js
tagStyle: {
  p: 'font-size: 14px;color: #353535;line-height: 2;font-family: "Times New Roman";',
  h2: 'font-size: 18.67px;color: #000;text-align:center;margin: 1em auto;font-weight: 500;font-family: SimHei;',
  h3: 'font-size:16.33px;color: #000;line-height: 2em;font-family: SimHei;',
  h4: 'font-size:14px;color: #000;font-family: SimHei;',
}

對於代碼高亮,使用prism ,引入到該組件中。

const Prism = require('./prism.js');
...
highlight(content, attrs) {
  content = content.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/quot;/g, '"').replace(/&amp;/g, '&'); // 替換實體編碼
  attrs["data-content"] = content; // 記錄原始文本,可用於長按複製等操做
  switch (attrs[lan]) {
    case "javascript":
    case "js":
      return Prism.highlight(content, Prism.languages.javascript, "javascript");
  }
}

數學公式Latex
對於latex渲染引擎,主要有兩種

引擎 特色
mathjax 語法豐富,渲染較慢
katex 支持語法較少,迅速,只能輸出mathml或html,須要搭配其CSS and font files使用

固然,這兩種都是網頁客戶端渲染,在小程序端天生不可用,考慮採用服務端渲染。問題有:

  • 服務端渲染若是使用外部接口,需encodeUrl(公式),但內部\被轉義消失,須要\\,replace(/\/g,'\')無效
  • 服務端渲染若是使用mathjax-node,其依賴項mathjax版本^2.7.2,需將全部\替換爲\\,會常常性出現SVG - Unknown character: U+C in MathJax_Main,MathJax_Size1,MathJax_AMS, 矩陣解析錯誤TeX parse error: Misplaced &
  • 如何比較精準的識別markdown中特定標記的Latex,不形成誤操做。

考慮在markdown解析html階段將其轉化爲<img>,也是不少內容平臺採起的方式,較爲可靠可控。這裏使用markdown-it-latex2img插件

const md = require('markdown-it')({
  html: true,// Enable HTML tags in source
}).use(require('markdown-it-latex2img'),{
    style: "filter: opacity(90%);transform:scale(0.85);text-align:center;" // 優化顯示樣式
  })

圖3-2 markdown-it-latex2img效果

3.3 靜態託管

爲git庫設置構建計劃,以使每次提交後同步到對象存儲。這裏使用hexo做爲構建框架。

pipeline {
  agent any
  stages {
    stage('檢出') {
      steps {
        checkout([
          $class: 'GitSCM',
          branches: [[name: env.GIT_BUILD_REF]],
          userRemoteConfigs: [[url: env.GIT_REPO_URL, credentialsId: env.CREDENTIALS_ID]]
        ])
      }
    }
    stage('構建') {
      steps {
        echo '構建中...'
        sh 'npm install -g cnpm --registry=https://registry.npm.taobao.org'
        sh 'cnpm install'
        sh 'npm run build'
        echo '構建完成.'
      }
    }
    stage('並行階段') {
      parallel {
        stage('部署到騰訊雲存儲') {
          steps {
            echo '部署中...'
            sh "coscmd config -a $TENCENT_SECRET_ID -s $TENCENT_SECRET_KEY -b $TENCENT_BUCKET -r $TENCENT_REGION"
            sh 'coscmd upload -r public/ /'
            echo '部署完成'
          }
        }
        stage('打包') {
          steps {
            sh 'tar -zcf blog.tar.gz public'
            archiveArtifacts(artifacts: 'blog.tar.gz', defaultExcludes: true, fingerprint: true, onlyIfSuccessful: true)
          }
        }
      }
    }
  }
}

構建後自動刷新CDN,

// refresh_cdn
const Key = decodeURIComponent(event.Records[0].cos.cosObject.key.replace(/^\/[^/]+\/[^/]+\//,""));
const cdnUrl = `${process.env.CDN_HOST}/${Key}`;
CDN.request('RefreshCdnUrl', {
    'urls.0': cdnUrl
}, (res) => {
  ...
})

4 系統實現

4.1 數據庫

文章:

sync_posts = [
  {
    _id: String,
    createTime: String,
    slug: String,
    title: String,
    tags: Array,
    description: String,
    cover: String, // url
    content: String, // html
  }
]
// 安全規則
{
  "read": true, // 公有讀
  "write": "get('database.user_info.${auth.openid}').isManager", // 僅管理員能夠寫
}

用戶收藏

user_favorite = [
  {
    _id:String,
    userId:String,// openid
    postId: String,// 在表中加入冗餘數據直接查詢
    createTime: Date
  }
]
// 安全規則
{
  "read": "doc._openid == auth.openid",// 私有讀
  "write": "doc._openid == auth.openid"// 私有寫
}

用戶信息

user_info = [
  {
    _id: String,
    _openid: String,
    ...userInfo,
    isManager: Boolean,
  }
]
// 安全規則
{
  "read": "doc._openid == auth.openid", // 私有讀
  "write": "doc._openid == auth.openid"// 私有寫
}

4.2 登陸

4.2.1 普通登陸

使用雲開發後,無需經過wx.login獲取登陸憑證(code)進而換取用戶登陸態信息,因每次調用雲函數時會附帶調用者openid。同時因爲能夠直接經過open-data展現用戶信息(不管是否受權),一些小程序所以繞過用戶登陸。有些小程序經過受權用戶信息後保存到數據庫,後續操做均使用數據庫信息,沒法在用戶變動信息後更新。若是用戶主動經過設置頁取消受權,但返回後卻還在展現使用用戶的信息(顯示已登陸)。這是由於用戶態信息是經過onLoad獲取的,返回操做時是onShow,故此時會產生矛盾。用戶在從新受權登陸時選擇使用其餘暱稱和頭像,這時一些小程序會認爲是新用戶登陸。還有一部分小程序不論業務中是否須要用戶信息,均要求受權纔可以使用。實際上微信小程序最大的特色就是能夠方便地獲取微信提供的用戶身份標識,快速創建小程序內的用戶體系,但上述情形均沒有妥善處理用戶登陸這一基本策略。

基於"來去自如"的原則,能夠遊客瀏覽,也可登陸和登出。在涉及一些須要採集和輸入用戶信息、或保存用戶記錄的功能時纔要求用戶跳至登陸頁受權獲取信息,會經過雲函數將其與上下文中的openid保存到數據庫,同時在回調中將用戶標識生成自定義登陸態緩存到本地,若是用戶點擊退出會將其置空。

// cloudfunction/login
const openid = wxContext.OPENID
db.collection('user_info').where({
  _openid: openid
}).get().then(async (res)=> {
  if (res.data.length === 0) {
    db.collection('user_info').add({
      data: {
        _openid: openid,
        ...event.userInfo,
        createTime: db.serverDate(),
      }
    })
  }

在下次打開小程序時,會經過檢查緩存中的自定義登陸態來判斷用戶是否登陸,一樣調用雲函數來更新用戶信息和使用信息(如打開時間、打開次數用於後續用戶分析)。在下次登陸時將不會彈出受權提示,當用戶自行取消受權(或者wx.openSetting時誤操做),這種狀況機率很小,但一旦出現就是Bug。若是在onShow中檢測用戶,會與正常onLaunch產生重複的邏輯,但又須要檢測這種行爲。實際上,打開設置頁必然會進入onHide,能夠:

// app.js
onHide:function() {
  wx.onAppShow(()=> {
    if(this.globalData.hasLogin) {
      wx.getSetting({
        success: res => {
          if (!res.authSetting['scope.userInfo']) { // 取消了受權
            this.logout() // 返回後直接登出
          }
        }
      })
    }
    wx.offAppShow();
  })
},

4.2.2 管理員鑑權

管理員即文章做者,對於管理員標識,考慮到

  • 手機號: 目前該接口針對非我的開發者,且完成了認證的小程序開放
  • openid: 不使用前是未知的,沒法提早綁定
  • 其餘用戶信息、密碼等會暴露管理入口

因而採起了最簡單直接的數據字段標記isMaganer:true,這一字段也用於數據庫的安全規則設定。

4.3 分享

分享無非兩種,直接分享到聊天和生成海報後引導分享到朋友圈,對於前者,須要考慮圖片大小爲5:4,其餘比例會產生空白或者裁切。這裏主要分析後者。在小程序端經過canvas繪製到倒出圖片比較慢,因爲每篇文章分享內容基本固定,能夠考慮預生成。但若是分享二維碼和分享者關聯,就仍然須要本地生成。這裏使用組件mini-share。對於小程序碼,目前採用雲調用方式,這種方式只能由小程序端觸發。

// 處理參數
const path = page +'?' + Object.keys(param).map(function (key) {
    return encodeURIComponent(key) + "=" + encodeURIComponent(param[key]);
}).join("&");
// 組織文件名
const fileName = 'wxacode/limitA-' + crypto.createHash('md5').update(path).digest('hex');
// 查找文件,若是找到直接返回路徑
let getFileRes = await cloud.getTempFileURL({
  fileList: [fileID]
});
// 若未找到從新生成
const wxacodeRes = await cloud.openapi.wxacode.get({
  path,
  isHyaline:true
})
// 上傳到雲存儲
const uploadRes = await cloud.uploadFile({
  cloudPath: fileName + fileSuffix,
  fileContent: wxacodeRes.buffer,
});
// 獲取返回臨時路徑
getFileRes = await cloud.getTempFileURL({
  fileList: [uploadRes.fileID]
});

生成二維碼方式有三種,分析特性

類型 特色 適用場景
A+ C 個數有限、參數較長 生成後儲存 用於長期有效業務,可用於邀請碼一類用戶可長期關注使用的操做。
B 個數無限、參數較短 生成後可不保存,其scene與用戶短時間行爲關聯(如活動)。活碼,與數據庫關聯後能夠轉換含義再次使用。

這裏因爲文章的數據庫_id默認是32位,達到了B類的限制,而且還須要關聯其餘信息,故使用了A類(wxacode.get)

4.4 訂閱消息

對於我的主體,只能用戶經小程序發起訂閱(獲取下發權限)後下發一次消息,這裏當用戶留言時,會訂閱一次回覆通知,但沒法發給做者(除非做者長期訂閱)。因爲同時須要保存到數據庫,這裏使用雲調用實現。

// post.js
wx.requestSubscribeMessage({
  tmplIds: [TEMPLATE.REPLY]
})
// cloudfunction/sengMsg
let sendRes = await db.collection('user_msg').add({
  data: {
    _openid: wxContext.OPENID,
    msg:inputMsg,
    createTime:Date.parse(new Date())
  }
});
await cloud.openapi.subscribeMessage.send({
  data: format(data), // 因爲各類類型信息有長度格式限制,須要處理
  touser: wxContext.OPENID,
  templateId: TEMPLATE.REPLY
});

5 拓展總結

5.1 結合語雀

5.1.1 同步到語雀

  • 會在標題前插入<a name="tqO5w"></a>標籤
  • 編輯界面直接複製會圖片外鏈轉化,可是直接導入的不會轉化
  • 只可本地引入文件(圖片),均不支持外部連接引入,除了加入的第三方服務
  • 能夠input任意類型,但output都是特有lake格式,且在更新文檔接口調用時,會返回
{
  "status": 400,
  "message": "抱歉,語雀不容許經過 API 修改 Lake 格式文檔,請到語雀進行操做。"
}

5.1.2 從語雀同步

能夠藉助語雀良好的編輯體驗來寫文章,同步到其餘平臺。yuque的webhook會發送webhook.doc_detail能夠直接獲取到內容。可是,在豐富文檔內容類型方面,語雀作了不少卓有成效的努力,使用這些特性,也就沒法保證其餘平臺的兼容性。刪除操做返回的slug會變爲trash-EJA8tL7W,與原slug無關,沒法經過slug創建其餘平臺的關聯,即僅增改操做能夠同步。所以,在語雀寫做,自動部署到其餘平臺的方案是不切實際和沒必要要的。

5.1.3 workflow

同步至語雀後,能夠利用其豐富的支持類型完善文檔內容,好比將文本內容轉化爲更直觀的流程圖、思惟導圖,將demo和代碼合併到codepen直觀演示,將可能涉及的資料直接以附件上傳方便獲取。
但要注意:

  • 不少內容平臺每每會在擁有必定用戶基數後作圖片防盜鏈。
  • 目前的webhook設計不安全,沒有簽名驗證,可能因爲Webhooks URL泄露被僞造請求

5.2 小程序開發已知問題

  • 真機初始動畫卡頓500ms
  • 原生TabBar隱藏會跳動,加動畫會黑屏,自定義TabBar切換時全部圖標會閃動,自動隱藏會顯示白條.
  • 簡單幾回來回navigate後,listeners of event onBeforeUnloadPage_17 have been added, possibly causing memory leak.
  • 在調用CameraFrameListener.start開始監聽幀數據後,必然有對像素data的獲取和處理,但這會致使界面全部的點擊(bindtap)事件失效,也就不能經過點擊觸發CameraFrameListener.stop中止函數
  • 雲控制檯數據庫管理頁中數組更新操做符addToSet無效,對象元素傳入後不穩定,或生效或不生效

  1. Tencent Cloud.雲開發CloudBase文檔[EB/OL].https://cloud.tencent.com/document/product/876/41136. 2020 ↩︎

  2. Tory Walker.The-Pitfalls-of-Async-Await-in-Array-Loops[EB/OL].https://medium.com/dailyjs/the-pitfalls-of-async-await-in-array-loops-cf9cf713bfeb. 2020 ↩︎

  3. 金煜峯.小程序富文本能力的深刻研究與應用[EB/OL].https://developers.weixin.qq.com/community/develop/article/doc/0006e05c1e8dd80b78a8d49f356413. 2019 ↩︎

相關文章
相關標籤/搜索