我在開發"小程序"中作的一些"轉換"的工做

介紹

「轉換」 意思是將"小程序"不支持的東西轉換成它支持的東西。我在開發的小程序的過程當中遇到了兩種須要作「轉換」的場景:javascript

  • html 轉換成 wxmlhtml

  • svg 轉換成 canvas前端

我將在下文詳細介紹我是怎麼處理這兩種狀況的。java

html 轉換成 wxml

咱們的產品在某些場景下,後端接口會直接傳 html 字符串給前端。在 ReactJs 中,咱們能夠用 dangerouslySetInnerHTML 直接渲染 html 字符串(不必定安全),而 」小程序「不支持 html ,所以必須對 html 進行處理。解決這個問題的步驟主要是:1. 將 html 轉換成 json ( 樹結構) ;2. 將 json 轉換成 wxml 。我在對問題作了調研後發現,現有一個庫 wxParse 知足該轉換的目的,可是在我看來,這個庫作的事情太多,須要依賴文件過多,不知足只須要簡單處理的須要,因此我決定本身寫。node

html 轉換成 json

在參考了 html2jsonhimalaya 兩個庫的處理思路的基礎上,我寫了一個簡單的解析庫 htmlParser htmlParser 處理 html字符串分兩步:git

lexer: 生成標記(tokengithub

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: 根據標記生成樹
上面的 lexerhtml 字符串分隔成了一個一個 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 模擬繪製

websvg 的元素有不少,好在咱們須要的只有一些基本的元素:image, rect, pathrectcanvas 模擬不算難事,canvas 繪製起來很簡單,代碼以下:

// draw rect
 ctx.save()
 ctx.setFillStyle(attr.fill)
 ctx.fillRect(attr.x, attr.y, attr.width, attr.height)
 ctx.restore()

然而,在開發過程當中,遇到了一個難點:不知道對 pathd 屬性如何進行模擬。d 屬性涉及移動、貝塞爾曲線等等。好比:<path d="M250 150 L150 350 L350 350 Z" /> 這個例子定義了一條路徑,它開始於位置 250 150,到達位置 150 350,而後從那裏開始到 350 350,最後在 250 150 關閉路徑(M = movetoL = linetoZ = 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縮放比例,以讓圖形徹底顯示。

總結

以上,就是我在開發「小程序」中對 htmlsvg 作的一些「轉換」的經歷。總結起來就是,對字符串解析,轉換成「小程序」語言。在此延伸一下,如需在 wxml 中支持 wxml 字符串,藉助 htmlParser 作解析,再寫一個 wxml 模板,咱們也就能「轉換」 wxml

參考

相關文章
相關標籤/搜索