分类 碎碎念 下的文章

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

介绍

“转换” 意思是将"小程序"不支持的东西转换成它支持的东西。我在开发的小程序的过程中遇到了两种需要做“转换”的场景:

  • html 转换成 wxml
  • svg 转换成 canvas

我将在下文详细介绍我是怎么处理这两种情况的。

html 转换成 wxml

我们的产品在某些场景下,后端接口会直接传 html 字符串给前端。在 ReactJs 中,我们可以用 dangerouslySetInnerHTML 直接渲染 html 字符串(不一定安全),而 ”小程序“不支持 html ,因此必须对 html 进行处理。解决这个问题的步骤主要是:1. 将 html 转换成 json ( 树结构) ;2. 将 json 转换成 wxml 。我在对问题做了调研后发现,现有一个库 wxParse 满足该转换的目的,但是在我看来,这个库做的事情太多,需要依赖文件过多,不满足只需要简单处理的需要,所以我决定自己写。

html 转换成 json

在参考了 html2jsonhimalaya 两个库的处理思路的基础上,我写了一个简单的解析库 htmlParserhtmlParser 处理 html字符串分两步:

lexer: 生成标记(token

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,然后,我们通过遍历所有的标识来构建树

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
}

整个程序的运行结果举例:

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字符串的转换,完整代码请戳 htmlParser

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

参考

底层学习路线图

上个礼拜六,与“耳语”产品经理聊天,引发了我的很多思考。
“python的垃圾回收机制底层是怎么实现的?”
----这个问题困扰了我,于是这两天,我不断搜集资料,才对此有了一点点了解。
这之后,我决定开始好好地研究一下底层。
借助chrome的应用"思维导图"我画了如下学习路线图,以此来作为学习导向。

每日阅读

这是最新版的界面设计,我决定暂时收工不再进行设计开发。

  自从8月28日上线测试以来,开发了几个版本的“每日阅读”。从最先开始的单页面设计,到后来的多页面,详情阅读页面。从一开始的即访问即抓取数据,到现在,定时数据存储,到日增百条数据。从最开始的效果展开,收起,到现在的概要,点击进入详情阅读。移动端的体验一开始就列入考量,另有考虑,往原生app的方向靠近,现在浏览体验也是极好的。

总之能够做好成现在这样子,我是很满足,很有成就感的。

   可是说实话,一个人的设计,开发,运维,实在是心塞不过。功能开发的过程想到体验于是停下改设计,还要关注定时执行的程序的执行状况。程序bug不喜欢,页面色彩搭配不当不开心,爬虫程序error不顺利没法不盯着。然后到现在算是有了体会,设计,开发,运维的工作状况。

然而我希望有更多的人来使用它

  这款产品我做不来像

鲜果 zaker feedly 类似产品如:今日头条 之类

  这是显然,别人做的已经很成熟了,所以我的应用是没什么竞争力的。但或许有别的出路,毕竟这些数据是极有用的。以后再想怎么发展好了。

给身边的朋友推荐吧。

  希望有很多人来阅读,来使用这个应用。

关于"每日阅读"这款应用

“每日阅读”这款应用已经上线了,链接:http://myreading.sinaapp.com

这款应用的目的是实现资讯的获取与阅读,资讯内容有简单介绍也有深度阅读。

互联网的资讯太多太杂,有时候不知道该阅读什么,有时候也找不到好的阅读源。

“每日阅读”的技术是python爬虫,考虑到原信息出处网站的版权,因此只爬和解析它们的rss源。

网上确实有很多rss阅读器,但是提供的内容太繁而杂,而且阅读体验也未必好。

我想做好阅读体验,因此设计了一个模板。

我也人工地对这些rss源做了筛选,只爬该爬的,但说到底是基于我自己的考虑。

我选了四大栏目,分别是:“互联网”、“豆瓣”、“阅读”、“图赏”。

每个栏目下精选几个rss源,它们有知乎的,豆瓣的,一个 的,leica中文杂志的等等。

总结起来就是文字、书籍、电影、问答、美图。

目前这些选择看起来是对我这种类型的人考虑,不知道大家喜不喜欢。

但是这些栏目和rss源以后不只这些,我会不断收集和增加。

关于界面,现在就只做了一个模板,我觉得用户体验还行。

 

简单介绍一下这款应用的开发过程:

离线下载版-> sae html版 -> bootstrap前端优化 -> django功能扩充 ->sae上线

离线下载版:当时是直接download下来成.doc和.html文件,然后用office和浏览器打开,不过当时只有标题和链接,内容是没有的。

sae html版:想要把它做成应用,于是就放到sae上,当时只有一个单页。

bootstrap前端优化:为了前端更好的显示效果,继续采用熟悉的bootstrap,之前设计html的时候琢磨着花了几天,用boot优化不过用了一两个小时。

django功能扩充:如果要做大做成应用怎么可能不使用框架呢?在django的使用体验上面感觉比php的框架舒服的多,代码少,功能明了。遇到最多的问题是它的模板系统,挺糊涂的。

sae上线:把本地写好的django都搬上去,参照了网上的做法,很快就搞定了。