寫在前面
Comi 讀 ['kəʊmɪ],類似中文 科米,是騰訊 Omi 團隊開發的小程式程式碼高亮和 markdown 渲染元件。有了這個元件加持,小程式技術社群可以開始搞起來了。
體驗
感謝【小程式•雲開發】提供技術支援。
預覽
Comi 基於下面的 5 個元件進行開發:
- prismjs
- wxParse
- remarkable
- html2json
- htmlparser
先看 Comi 使用,再分析原理。
使用
先拷貝 此目錄 到你的專案。
js:
const comi = require('../../comi/comi.js');
Page({
onLoad: function () {
comi(`你要渲染的 md!`, this)
}
})
複製程式碼
wxml:
<include src="../../comi/comi.wxml" />
複製程式碼
wxss:
@import "../../comi/comi.wxss";
複製程式碼
簡單把!
在 omip 中使用
先拷貝 此目錄 到你的專案。
js:
import { WeElement, define } from 'omi'
import './index.css'
import comi from '../../components/comi/comi'
define('page-index', class extends WeElement {
install() {
comi(`你要渲染的 md`, this.$scope)
}
render() {
return (
<view>
<include src="../../components/comi/comi.wxml" />
</view>
)
}
})
複製程式碼
WeElement 裡的 this 並不是小程式裡的 this,需要使用 this.$scope
訪問小程式 Page或 Component 的 this。
css:
@import '../../components/comi/comi.wxss';
複製程式碼
原理
在開發 Comi 之前,我們進行了預研,是否有必要造這個輪子。
程式碼高亮預研
- wxParse 只是用標籤包括程式碼,並未處理程式碼轉成 WXML,所以渲染出的程式碼是沒有顏色
- 老牌的 highlightjs 沒有 WXML 對應的方案
- 老牌的 highlightjs 對 JSX 高亮支援太差
- prismjs 是 react 官方使用的高亮外掛,對 JSX 支援高亮很好
- prismjs 支援幾乎所有的語言,並且支援自定義擴充套件語言
- prismjs 擁有 Line Highlight 外掛(目前還未移植到 Comi)
綜合上面資訊,決定基於 prismjs 二次開發。
markdown 渲染預研
- wxParse 老牌的渲染元件,支援 markdown
- wxParse 內建的 showdownjs 不滿足程式碼高亮的格式需求(比如語言種類也會生成一個標籤,當然可以通過 wxss 隱藏)
- 小程式基礎庫 1.4.0 開始支援
rich-text
元件展示富文字,但是格式需要轉成 json - 高效能 remarkable,Facebook 和 Docusaurus 都在使用,支援 md 語法修改和擴充套件
<rich-text nodes="{{nodes}}" bindtap="tap"></rich-text>
複製程式碼
Page({
data: {
nodes: [{
name: 'div',
attrs: {
class: 'div_class',
style: 'line-height: 60px; color: red;'
},
children: [{
type: 'text',
text: 'Hello World!'
}]
}]
},
tap() {
console.log('tap')
}
})
複製程式碼
綜合上面資訊,放棄 rich-text,決定基於 wxParse + remarkable 二次開發,移除 showdownjs。Comi 需要 remarkable 的高效能和靈活性。markdown 會持久化存在 db, 在小程式內執行時轉換成 wxml,所以對效能還是有一定要求。
劫持 prismjs tokens
tokens: function(text, grammar, language) {
var env = {
code: text,
grammar: grammar,
language: language
};
_.hooks.run('before-tokenize', env);
env.tokens = _.tokenize(env.code, env.grammar);
_.hooks.run('after-tokenize', env);
for (var i = 0, len = env.tokens.length; i < len; i++) {
var v = env.tokens[i]
if (Object.prototype.toString.call(v.content) === '[object Array]') {
v.deep = true this._walkContent(v.content)
}
}
return env.tokens
},
複製程式碼
這段程式碼增加 tokens 方法到 prismjs 中,原庫自帶的 prism.highlight 的會把 tokens 轉成 html,因為我們的目標的 wxml,所以這裡提前把 tokens 作為方法返回值。當然還做了一件事,就是擴充套件了 token item 的 deep 屬性來決定是否需要繼續向下遍歷生成 wxml。
原始的 jsx:
render() {
const { tks } = this.data
return (
<view class='pre language-jsx'>
<view class='code'>
{tks.map(tk => {
return tk.deep ? <text class={'token ' + tk.type}>{
tk.content.map(stk => {
return stk.deep ? stk.content.map(sstk => {
return <text class={'token ' + sstk.type}>{sstk.content || sstk}</text>
}) : <text class={'token ' + stk.type}>{stk.content || stk}</text>
})}</text> : <text class={'token ' + tk.type}>{tk.content || tk}</text>
})}
</view>
</view>
)
}
複製程式碼
jsx 編譯出生成的 wxml,把這段 wxml 嵌入到 wxparse 裡:
<!-- 千萬 不要格式化下面的 wxml,不然 text 巢狀 text 導致換行全部出來了 -->
<template name="wxParseCode">
<view class="pre language-jsx">
<view class="code">
<block wx:for="{{item.tks}}" wx:for-item="tk">
<block wx:if="{{tk.deep}}"><text class="{{'token ' + tk.type}}"><block wx:for="{{tk.content}}" wx:for-item="stk"><block wx:if="{{stk.deep}}"><text class="{{'token ' + sstk.type}}" wx:for="{{stk.content}}" wx:for-item="sstk">{{sstk.content || sstk}}</text>
</block>
<block wx:else><text class="{{'token ' + stk.type}}">{{stk.content || stk}}</text>
</block>
</block>
</text>
</block>
<block wx:else><text class="{{'token ' + tk.type}}">{{tk.content || tk}}</text>
</block>
</block>
</view>
</view>
</template>
複製程式碼
這段 wxml 不能進行格式化美化,不然多出許多換行符,因為 text 巢狀 text 會保留換行符!!
修改 wxparse 裡的分支邏輯:
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:if="{{item.tag == 'pre'}}">
<template is="wxParseCode" data="{{item}}" />
</block>
<block wx:elif="{{item.tag != 'pre'}}" >
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</block>
</view>
</block>
複製程式碼
當 item.tag
為 pre
的時候使用 wxParseCode 模板,資料傳入 item。item 的資料從哪裡來?
先修改 md 渲染器為 Remarkable:
} else if (type == 'md' || type == 'markdown') {
var converter = new Remarkable()
var html = converter.render(data)
transData = HtmlToJson.html2json(html, bindName);
}
複製程式碼
使用上面的 prism.tokens 計算出程式碼片段的 tokens,用於 wxparse 的模板渲染:
function transPre(transData) {
transData.nodes.forEach((node, index) => {
if (node.tag == 'pre') {
var lan = 'markup'
if (node.nodes[0].classStr) {
lan = node.nodes[0].classStr.split(' ')[0].replace('language-', '')
}
var tks = prism.tokens(node.nodes[0].nodes[0].text, prism.languages[lan], lan)
transData.nodes[index].tks = tks
}
})
}
複製程式碼
language- 支援多少種呢?目前 comi 預設支援:
- markup
- css
- clike
- javascript
- bash
- json
- typescript
- jsx
- tsx
預設使用的主題 css 是 okaidia。如果 comi 預設的配置不支援你的需求,你可以:
- 進 prismjs.com/download.ht… 這裡自行下載
- 劫持 prismjs tokens 拷貝進你下載的 prismjs 裡
- 把 prismjs 拷貝替換掉 comi 自帶的 prismjs
精簡 comi 使用流程
WXML 提供兩種檔案引用方式 import 和 include。和 import 不同,include 可以將目標檔案除了 template 和 wxs 外的整個程式碼引入,相當於是拷貝到 include 位置,如:
<!-- index.wxml -->
<include src="header.wxml" />
<view>body</view>
<include src="footer.wxml" />
複製程式碼
<!-- header.wxml -->
<view>header</view>
複製程式碼
<!-- footer.wxml -->
<view>footer</view>
複製程式碼
comi 利用了 import 和 include 特性簡化使用流程:
comi.wxml
<import src="./wxParse.wxml"/>
<template is="wxParse" data="{{wxParseData:article.nodes}}"/>
複製程式碼
comi.js
var WxParse = require('./wxParse.js');
module.exports = function comi(md, scope) {
WxParse.wxParse('article', 'md', md, scope, 5);
}
複製程式碼
comi.wxss
@import './wxParse.wxss';
@import './prism.wxss';
複製程式碼
使用時,只需要 :
- import
comi.js
- include
comi.wxml
- import
comi.wxss
另外,在 omip 使用 comi 時候發現不會拷貝 include 的檔案到 dist,發現 taro/omip 的正則沒有去匹配 include 檔案,所以,把:
exports.REG_WXML_IMPORT = /<[import](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi
複製程式碼
改成:
exports.REG_WXML_IMPORT = /<[import|inculde](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi
複製程式碼
搞定。