我在开发"小程序"中作的一些"转换"的工做

介绍

“转换” 意思是将"小程序"不支持的东西转换成它支持的东西。我在开发的小程序的过程当中遇到了两种须要作“转换”的场景: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

参考

相关文章
相关标签/搜索