「轉換」 意思是將"小程序"不支持的東西轉換成它支持的東西。我在開發的小程序的過程當中遇到了兩種須要作「轉換」的場景:javascript
html
轉換成 wxml
html
svg
轉換成 canvas
前端
我將在下文詳細介紹我是怎麼處理這兩種狀況的。java
html
轉換成 wxml
咱們的產品在某些場景下,後端接口會直接傳 html
字符串給前端。在 ReactJs
中,咱們能夠用 dangerouslySetInnerHTML
直接渲染 html
字符串(不必定安全),而 」小程序「不支持 html
,所以必須對 html
進行處理。解決這個問題的步驟主要是:1. 將 html
轉換成 json
( 樹結構) ;2. 將 json
轉換成 wxml
。我在對問題作了調研後發現,現有一個庫 wxParse
知足該轉換的目的,可是在我看來,這個庫作的事情太多,須要依賴文件過多,不知足只須要簡單處理的須要,因此我決定本身寫。node
html
轉換成 json
在參考了 html2json
與 himalaya
兩個庫的處理思路的基礎上,我寫了一個簡單的解析庫 htmlParser
。htmlParser
處理 html
字符串分兩步:git
lexer
: 生成標記(token
)github
function lex(html) { let string = html let tokens = [] while (string) { // 先處理以 "</" 開始的結束標籤 if (string.indexOf("</") === 0) { const match = string.match(REGEXP.endTag) if (!match) continue // 經過 substring 截斷這個標籤的字符串長度 string = string.substring(match[0].length) tokens.push({ tag: match[1], type: 'tag-end', }) continue } // 處理以 "<" 開始的標籤 if (string.indexOf("<") === 0) { const match = string.match(REGEXP.startTag) if (!match) continue string = string.substring(match[0].length) const tag = match[1] const isEmpty = !!MAKER.empty[tag] const type = isEmpty ? 'tag-empty' : 'tag-start' const attributes = getAttributes(match[2]) tokens.push({ tag, type, attributes }) continue } // 每一個處理過程的其餘部分字符串被當作 "text" 文本處理(暫時不處理其餘狀況) const index = string.indexOf('<') const text = index < 0 ? string : string.substring(0, index) string = index < 0 ? "" : string.substring(index) tokens.push({ type: "text", text }) } return tokens }
parser
: 根據標記生成樹
上面的 lexer
將 html
字符串分隔成了一個一個 token
,而後,咱們經過遍歷全部的標識來構建樹web
function parse(tokens) { let root = { tag: "root", children: [] } let tagArray = [root] tagArray.last = () => tagArray[tagArray.length - 1] for (var i = 0; i < tokens.length; i++) { const token = tokens[i] if (token.type === 'tag-start') { // 構建節點 const node = { type: "Element", tagName: token.tag, attributes: Object.assign({}, { class: token.tag }, token.attributes), children: [] } tagArray.push(node) continue } if (token.type === 'tag-end') { let parent = tagArray[tagArray.length - 2] let node = tagArray.pop() // 將該節點加入父節點中 parent.children.push(node) continue } if (token.type === 'text') { // 往該節點中加入子元素 tagArray.last().children.push({ type: 'text', content: replaceMark(token.text) }) continue } if (token.type === 'tag-empty') { // 往該節點中加入子元素 tagArray.last().children.push({ type: "Element", tagName: token.tag, attributes: Object.assign({}, { class: token.tag }, token.attributes), }) continue } } return root }
整個程序的運行結果舉例:json
var html = '<div style='height:10rpx;width: 20rpx;'><img src="http://xxx.jpg class="image"/></div>' htmlParser(html) # 轉換結果 { "tag": "root", "children": [{ "type": "Element", "tagName": "div", "attributes": { "style": "height:10rpx;width: 20rpx;" }, "children": [ { "type": "Element", "tagName": "img", "attributes": { src: "http://xxx.jpg", class: "image" } }] }] }
以上,咱們完成了 html
字符串的轉換,完整代碼請戳 htmlParsercanvas
json
轉換成 wxml
在熟悉了「小程序」框架的基礎上,發現須要藉助模板 template
,將 json
數據填充進 template
,並根據元素類型渲染相應的 wxml
組件以達到轉換目的。好比:
# 定義一個名稱爲 html-image 的模板 <template name="html-image"> <image mode="widthFix" class="{{attributes.class}}" src="{{attributes.src}}"></image> </template> /* 使用模板 其中 json 的結構爲: { "type": "Element", "tagName": "img", "attributes": { src: "http://xxx.jpg", class: "image" } } */ <template is="html-image" data={{json}}></template>
這樣,咱們就能轉化成功了。
而由於模板沒有引用自身的能力,只能使用笨辦法:使用多個一樣內容,可是模板名稱不同的模板來解決嵌套的層級關係,而嵌套的層級取決於使用的模板個數。
<template name="html-image"> <image mode="widthFix" class="{{attributes.class}}" src="{{attributes.src}}"></image> </template> <template name="html-video"> <video class="{{attributes.class}}" src="{{attributes.src}}"></video> </template> <template name="html-text" wx:if="{{content}}"> <text>{{content}}</text> </template> <template name="html-br"> <text>\n</text> </template> <template name="html-item"> <block wx:if="{{item.type === 'text'}}"> <template is="html-text" data="{{...item}}" /> </block> <block wx:elif="{{item.tagName === 'img'}}"> <template is="html-image" data="{{...item}}" /> </block> <block wx:elif="{{item.tagName === 'video'}}"> <template is="html-video" data="{{...item}}" /> </block> <block wx:elif="{{item.tagName === 'br'}}"> <template is="html-br"></template> </block> <block wx:else></block> </template> // html 引用 html1 兩個模板同樣 <template name="html"> <block wx:if="{{tag}}"> <block wx:for="{{children}}" wx:key="{{index}}"> <block wx:if="{{item.children.length}}"> <template is="html1" data="{{...item}}"/> </block> <block wx:else> <template is="html-item" data="{{item}}"/> </block> </block> </block> </template> <template name="html1"> <view class="{{attributes.class}}" style="{{attributes.style}}"> <block wx:for="{{children}}" wx:key="{{index}}"> <block wx:if="{{item.children.length}}"> <template is="html2" data="{{...item}}"/> </block> <block wx:else> <template is="html-item" data="{{item}}"/> </block> </block> </view> </template>
如上處理過程當中,有些須要注意的細節,好比:要對 html
實體字符轉換,讓模板的 image
組件支持 mode
等等。總之,通過如上的處理,html
字符串對 wxml
組件的轉換基本功能完成。
svg
轉換成 canvas
在咱們的產品 web
版本中,因爲須要在頁面元素中使用 svg
做爲 dom
元素,而「小程序」 沒有 svg
組件的支持,如此一來,咱們也須要對後端接口傳來的 svg
字符串作轉換。「小程序」沒有svg
組件可是有 canvas
組件,因而我決定使用 canvas
來模擬 svg
繪製圖形,並將圖形作必定的修改以知足基本需求。
作這個「轉換」的關鍵也有兩點:1. 提取 svg
字符串中的元素;2.canvas
模擬元素功能進行繪製
svg
元素的提取由於 svg
字符串是一個 xml
, 用上面的 htmlParser
能夠將其生成 json
,問題解決。
canvas
模擬繪製在 web
中 svg
的元素有不少,好在咱們須要的只有一些基本的元素:image
, rect
, path
。rect
用 canvas
模擬不算難事,canvas
繪製起來很簡單,代碼以下:
// draw rect ctx.save() ctx.setFillStyle(attr.fill) ctx.fillRect(attr.x, attr.y, attr.width, attr.height) ctx.restore()
然而,在開發過程當中,遇到了一個難點:不知道對 path
的 d
屬性如何進行模擬。d
屬性涉及移動、貝塞爾曲線等等。好比:<path d="M250 150 L150 350 L350 350 Z" />
這個例子定義了一條路徑,它開始於位置 250 150,到達位置 150 350,而後從那裏開始到 350 350,最後在 250 150 關閉路徑(M = moveto
, L = lineto
, Z = closepath
)。用 canvas
進行繪製,須要先提取 d
各屬性值,再依據各屬性值與 canvas
的對應繪製關係依次進行繪製。在我爲此犯難的時候,好在發現有前人有作了這樣事情。在 gist
上,發現了對 d
的解析,與 canvas
繪製 d
的相關代碼,困難問題也得以解決。
/** * svg path * <path d="M250 150 L150 350 L350 350 Z" /> * d 屬性值 "M250 150 L150 350 L350 350 Z" * 咱們提取屬性的的結構爲: [ * { marker: 'M', values: [250, 150]} * ] * https://gist.github.com/shamansir/0ba30dc262d54d04cd7f79e03b281505 * 如下代碼爲 d 屬性的提取部分,已在源代碼基礎上修改, */ _pathDtoCommands(str) { let results = [], match; while ((match = markerRegEx.exec(str)) !== null) { results.push(match) } return results .map((match) => { return { marker: str[match.index], index: match.index } }) .reduceRight((all, cur) => { let chunk = str.substring(cur.index, all.length ? all[all.length - 1].index : str.length); return all.concat([{ marker: cur.marker, index: cur.index, chunk: (chunk.length > 0) ? chunk.substr(1, chunk.length - 1) : chunk }]) }, []) .reverse() .map((command) => { let values = command.chunk.match(digitRegEx); return { marker: command.marker, values: values ? values.map(parseFloat) : [] }; }) }
完成了如上的步驟後,圖形基本繪製出來了,可是在後期,出現了 svg
image
位置的問題。svg
中的圖片除了會有 x
, y
座標關係,還會根據視窗大小,以短邊爲準,保持寬高比,長邊作縮放,視窗中居中顯示。這是我以前不清楚的部分,爲此多花了點時間和精力。此外,還有些細節須要注意,好比須要調整 canvas
的縮放比例,以讓圖形徹底顯示。
以上,就是我在開發「小程序」中對 html
與 svg
作的一些「轉換」的經歷。總結起來就是,對字符串解析,轉換成「小程序」語言。在此延伸一下,如需在 wxml
中支持 wxml
字符串,藉助 htmlParser
作解析,再寫一個 wxml
模板,咱們也就能「轉換」 wxml
了。