主要思路是經過git管理文章(markdown類型),發佈到小程序和靜態站點(適用於構建md文檔的框架如hexo、jeklly等)。
技術路線:javascript
在開發框架上,因爲初期面向微信小程序開發且可能存在未知問題,故使用原生開發,不使用多端或其餘預編譯框架。在小程序UI上,參考但不依賴WeUI組件庫,因因爲封裝沒必要要的特性可能形成代碼包的冗餘。html
類型 | 方案 | 備註 |
---|---|---|
代碼託管 | Coding | github api訪問較大機率慢且不穩定 |
雲開發 | 騰訊雲TCB | 含小程序雲開發服務 |
持續集成 | Coding CI | 使用Jenkinsfile定義pipeline |
靜態託管 | 騰訊雲COS | 也可以使用阿里雲OSS,或直接使用雲開發提供的靜態網站託管,使用對象存儲配合內容分發加速。 |
Markdown解析 | markdown-it | 也可以使用markdjs,但markdown-it支持拓展插件 |
富文本渲染 | parser | 比原生rich-text功能豐富且效果穩定 |
因爲是內容類應用,須要格外注意視覺規範,以使用戶獲取較好的閱讀體驗。如下規範參考了WEDESIGN和Ant 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
小程序端作簡單計算git
服務端(雲開發)作複雜處理,非實時性計算,或可預生成內容github
安全校驗,保證雲函數觸發來源及方式可信: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}` // 我的令牌 } });
提取文章信息:
因爲要求在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 });
在小程序的文檔中,觸發雲函數能夠經過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(); }
幾乎不太可能將原內容原封不動顯示出來, 通過markdown-it渲染後的html字符串沒有插入任何樣式,直接測試(根據標籤默認提供樣式)效果以下:
方案 | 效果 |
---|---|
rich-text | 代碼塊缺失,長內容被截斷 |
wxparser | 間距過大,表格、代碼塊被截斷 |
towxml | 代碼塊被截斷 |
wemark | 代碼塊與引用部分不換行拉寬 |
Parser | 表格溢出 |
Tips: 注意到騰訊Omi團隊開發的小程序代碼高亮和markdown渲染組件Comi,實際上採用模板引入的方式使用。考慮隨後實測效果和對比渲染速度。
相比之下,都會出現溢出組件邊界,產生橫向滾動條問題。在使用上,存在不支持解析style標籤缺陷[3]
而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(/</g, '<').replace(/>/g, '>').replace(/quot;/g, '"').replace(/&/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使用 |
固然,這兩種都是網頁客戶端渲染,在小程序端天生不可用,考慮採用服務端渲染。問題有:
\
被轉義消失,須要\\
,replace(/\/g,'\')無效\
替換爲\\
,會常常性出現SVG - Unknown character: U+C in MathJax_Main,MathJax_Size1,MathJax_AMS
, 矩陣解析錯誤TeX parse error: Misplaced &
考慮在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;" // 優化顯示樣式 })
爲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) => { ... })
文章:
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"// 私有寫 }
使用雲開發後,無需經過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(); }) },
管理員即文章做者,對於管理員標識,考慮到
因而採起了最簡單直接的數據字段標記isMaganer:true
,這一字段也用於數據庫的安全規則設定。
分享無非兩種,直接分享到聊天和生成海報後引導分享到朋友圈,對於前者,須要考慮圖片大小爲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)
對於我的主體,只能用戶經小程序發起訂閱(獲取下發權限)後下發一次消息,這裏當用戶留言時,會訂閱一次回覆通知,但沒法發給做者(除非做者長期訂閱)。因爲同時須要保存到數據庫,這裏使用雲調用實現。
// 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 });
<a name="tqO5w"></a>
標籤{ "status": 400, "message": "抱歉,語雀不容許經過 API 修改 Lake 格式文檔,請到語雀進行操做。" }
能夠藉助語雀良好的編輯體驗來寫文章,同步到其餘平臺。yuque的webhook會發送webhook.doc_detail能夠直接獲取到內容。可是,在豐富文檔內容類型方面,語雀作了不少卓有成效的努力,使用這些特性,也就沒法保證其餘平臺的兼容性。刪除操做返回的slug會變爲trash-EJA8tL7W
,與原slug無關,沒法經過slug創建其餘平臺的關聯,即僅增改操做能夠同步。所以,在語雀寫做,自動部署到其餘平臺的方案是不切實際和沒必要要的。
同步至語雀後,能夠利用其豐富的支持類型完善文檔內容,好比將文本內容轉化爲更直觀的流程圖、思惟導圖,將demo和代碼合併到codepen直觀演示,將可能涉及的資料直接以附件上傳方便獲取。
但要注意:
Tencent Cloud.雲開發CloudBase文檔[EB/OL].https://cloud.tencent.com/document/product/876/41136. 2020 ↩︎
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 ↩︎
金煜峯.小程序富文本能力的深刻研究與應用[EB/OL].https://developers.weixin.qq.com/community/develop/article/doc/0006e05c1e8dd80b78a8d49f356413. 2019 ↩︎