基于virtual dom 的canvas渲染

项目详情

github 地址: githubcss

demo实例:demohtml

背景

起初,在公司作一些活动页的时候,常常须要用到截图分享的千人千面的功能,并且这种需求并不止一两次,而是常常会出如今各类各样的截图场景。第一次碰到这种需求的时候,基本上都会去手撸canvasAPI去作渲染功能,这种状况的步骤大体以下:vue

  1. 写一大串 dom template 标签
  2. 渲染templatedom标签
  3. 开始捕捉dom元素,绘制canvas
  4. canvas 渲染图片

面临的主要问题是复用性太差,其次是性能上也有问题,用户看到的界面不必定和正式渲染出的界面一致,可能存在渲染差别。 由于我工做中主要使用的是vue,对vue核心思想也有必定研究,vue经过vnode实现了对不一样端的渲染工做,那有没有可能经过vnode实现对canvas的渲染呢?也就是说,没有vnode -> html -> canvas 而是直接vnode -> canvas。 同时利用vue的数据驱动,来达到绘制的数据驱动。想法有了,下面开始实施。node

调研

这篇文章对此有详细的介绍:60 FPS on the mobile web 这里简单的归纳一下:webpack

canvas是一种当即模式的渲染方式,不会存储额外的渲染信息。Canvas 受益于当即模式,容许直接发送绘图命令到 GPU。但若用它来构建用户界面,须要进行一个更高层次的抽象。例如一些简单的处理,好比当绘制一个异步加载的资源到一个元素上时会出现问题,如在图片上绘制文本。在HTML中,因为元素存在顺序,以及 CSS 中存在 z-index,所以是很容易实现的。 dom渲染是一种保留模式,保留模式是一种声明性API,用于维护绘制到其中的对象的层次结构。保留模式 API 的优势是,对于你的应用程序,他们一般更容易构建复杂的场景,例如 DOM。一般这都会带来性能成本,须要额外的内存来保存场景和更新场景,这可能会很慢。git

开始!

canvas 的渲染其实也是一种尝试,既然前人以及作了充分的实践,那么咱们便站在巨人的肩膀上去基于vue来实现一个数据驱动的canvas渲染。说作就作!github

处理vnode

熟悉Vue源码的应该都知道,Vue经过render函数,传入createElement方法来构造出一个vnode,经过发布--订阅模式来实现对数据的监听,从新生成vnode。咱们要作的就是在vnode这一层开始。因此,咱们基于Vue源码的方式,实现一个监听函数,并混入Vue实例中:web

Vue.mixin({
    // ...
    created() {
      if (this.$options.renderCanvas) {
        // ...
        // 监听vnode中引用的变化,从新渲染
        this.$watch(this.updateCanvas, this.noop)
        // ...
      }
    },
    methods: {
      updateCanvas() {
        // 模拟Vue render 函数
        // 寻找实例中定义的 renderCanvas 方法,并传入createElement方法
        let vnode = this.$options.renderCanvas.call(this._renderProxy, this.$createElement)
      }
})
复制代码

这样咱们就能够愉快的在组件内部使用:canvas

renderCanvas (h) {
  return h(...)
}
复制代码

canvas 元素处理

render的vnode咱们须要作额外的一些约束,好比domdiv 应该对应canvas里面的什么,dom里面的文本,对应canvas里面的什么... 也就是说咱们能够这样作一些约束:性能优化

自定义标签 绘制形式 类比dom
view/scrollView/scrollItem rect div
text text span
image img img

其中这些元素类分别都继承于一个Super类,而且因为它们各有不一样的展现方式,所以它们分别实现本身的draw方法,作定制化的展现。

绘制对象的布局机制实现

绘制 canvas 布局最基础的写法是为canvas 元素传入一系列坐标点和相关的基础宽高,这样写到实际项目中多是这样的:

renderCanvas(h) {
  return h('view', {
     style: {
       left: 10,
       top: 10,
       width: 100,
       height: 100
     }
  })
}
复制代码

这样写确实有点不方便维护,目前有好几种解决方案,一种是使用css-layout去作管理。css-layout支持的转换属性以下:

image

这样也只是作了一层转换,帮咱们更好的用css思惟去写canvas,可是若是咱们很不爽css in js的写法,其实咱们还能够写一个webpack loader 来加载外部css:

const css = require('css')
module.exports = function (source, other) {
  let cssAST = css.parse(source)
  let parseCss = new ParseCss(cssAST)
  parseCss.parse()
  this.cacheable();
  this.callback(null, parseCss.declareStyle(), other);
};

class ParseCss {
  constructor(cssAST) {
    this.rules = cssAST.stylesheet.rules
    this.targetStyle = {}
  }

  parse () {
    this.rules.forEach((rule) => {
      let selector = rule.selectors[0]
      this.targetStyle[selector] = {}
      rule.declarations.forEach((dec) => {
        this.targetStyle[selector][dec.property] = this.formatValue(dec.value)
      })
    })
  }

  formatValue (string) {
    string = string.replace(/"/g, '').replace(/'/g, '')
    return string.indexOf('px') !== -1 ? parseInt(string) : string
  }

  declareStyle (property) {
    return `window.${property || 'vStyle'} = ${JSON.stringify(this.targetStyle)}`
  }
}
复制代码

主要也就是将 css 文件转成AST语法树,以后再对语法树作转换,转成canvas须要的定义形式。并以变量的形式注入到组件中。

实现列表滚动

若是咱们的元素不少,须要滚动时,咱们必须解决canvas内部元素滚动的问题。这里我选择了使用Zynga Scroller 来模拟用户滚动方法,经过他返回的滚动坐标点,来对canvas进行重绘。

详细的参考这里

事件模拟

对于click,touch等dom事件的模拟,咱们采用的方案是根据点击区域进行检测,并找出最底层的元素,递归寻找父元素并触发对应事件处理程序,从而模拟事件冒泡。

详细的实现能够参考这里

最后

canvas绘制页面也是一种创新的尝试,但愿这里的研究对你有启发,也欢迎您的PR。这里也作了不少性能优化,限于篇幅不在赘述了,有兴趣也能够一块儿探讨。

最后:它并不意味着彻底取代基于DOM的渲染,这仍然须要文本输入,复制/粘贴,可访问性和SEO。 出于这些缘由,咱们可使用canvas和基于DOM的渲染的组合。

相关文章
相关标签/搜索