主要思路是经过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 ↩︎