将你的 Virtual dom 渲染成 Canvas

项目概述

一个基于Vue的virtual dom插件库,按照Vue render 函数的写法,直接将Vue生成的Vnode渲染到canvas中。支持常规的滚动操作和一些基础的元素事件绑定。

github 地址: github

demo实例:demo

背景

从一个小的需求说起:某一天,产品提了一个这样的需求,需要制作一个微信活动页,活动页可以分享包含用户相关信息的图片。这些信息是需要从接口取的,而且每个人都不一样。第一次碰到这种需求的时候,基本上都会去手撸canvasAPI去做渲染功能,这种情况的步骤大致如下:

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

面临的主要问题是复用性太差,其次是性能上也有问题,用户看到的界面不一定和正式渲染出的界面一致,可能存在渲染差异。作为一个有追求的前端,当然得想想看有没有更好的法子。于是乎了解到了一个html2canvas 这样一个库。但是总是感觉还是要转成dom再去绘制,而且感觉性能和稳定性也不是很好。

我们知道vue通过vnode实现了对不同端的渲染工作,那有没有可能通过vnode实现对canvas的渲染呢?也就是说,没有vnode -> html -> canvas 而是直接vnode -> canvas。 同时利用vue的数据驱动,来达到绘制的数据驱动。想法有了,下面开始实施。

调研

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

canvas是一种立即模式的渲染方式,不会存储额外的渲染信息。Canvas 受益于立即模式,允许直接发送绘图命令到 GPU。但若用它来构建用户界面,需要进行一个更高层次的抽象。例如一些简单的处理,比如当绘制一个异步加载的资源到一个元素上时会出现问题,如在图片上绘制文本。在HTML中,由于元素存在顺序,以及 CSS 中存在 z-index,因此是很容易实现的。

dom渲染是一种保留模式,保留模式是一种声明性API,用于维护绘制到其中的对象的层次结构。保留模式 API 的优点是,对于你的应用程序,他们通常更容易构建复杂的场景,例如 DOM。通常这都会带来性能成本,需要额外的内存来保存场景和更新场景,这可能会很慢。

看来canvas绘制页面的研究,很久之前就已经有人付出过研究了。而且性能还是很不错的。那我们更要试试看,到底我们的想法能不能实现了!越来越期待....

开始

canvas 的渲染其实也是一种尝试,既然前人以及做了充分的实践,那么我们便站在巨人的肩膀上去基于vue来实现一个数据驱动的canvas渲染。说做就做!(我们这里只提供思路,不做具体实现细节的讨论,因为实现起来有点复杂,如果有兴趣可以参考我的项目实现,或者一起交流探讨 )

处理vnode

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

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)
      }

})

这样我们就可以愉快的在组件内部使用:

renderCanvas (h) {
  return h(...)
}

canvas 元素处理

render 的vnode我们需要做额外的一些约束,也就是说我们需要怎么样的渲染标签,来渲染对应的canvas元素(举个🌰):

  1. view/scrollView/scrollItem --> fillRect
  2. text --> fillText
  3. image --> drawImage

其中这些元素类分别都继承于一个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的渲染的组合。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CDA数据分析师

那人的Excel技巧好烂,他做1个小时,别人29秒搞定

本文为简书作者傲看今朝原创,CDA数据分析师已获得授权 ”智能表格“在Excel中就是叫表格,恐怕是Excel当中最不起眼最受人忽视,却极其实用的功能之一,可以...

2155
来自专栏大数据钻研

欢迎来到HTML5.2时代!

21世纪,2016年6月,HTML 5.1从工作草案变为了候选标准。正如你了解的那样,这是将提案变为标准的第二步,Web的如此发展也将影响我们的日常生活。作为候...

3297
来自专栏编程

8个用于编写可维护,简化的前端代码的CSS策略

前言 代码质量不仅适用于后端的Java或C语言,它也适用于CSS。继续阅读,了解如何编写出色的CSS! 编写基本的CSS和HTML是我们作为Web前端开发人员学...

2119
来自专栏河湾欢儿的专栏

移动端常用的meta总结

声明viewport视口 viewport对于移动端设备来说非常的重要,用于定义视口的各种行为。其中最为重要的就是要设定一个展示页面的宽度width=devi...

1123
来自专栏芋道源码1024

Github改版,宣布放弃jQuery || 你的青春里,是否有过 JQuery 的身影?

2018年7月25日,Mislav Marohnić在推文中宣布GitHub前端已经彻底删除jQuery依赖,并全部依赖原生API。

1590
来自专栏程序员互动联盟

【专业文章】六种常见的HTML5写法误用(一)

一、不要使用section作为div的替代品 人们在标签使用中最常见到的错误之一就是随意将HTML5的<section>等价于<div>——具体地说,就是直接用...

3375
来自专栏web前端教室

ReactJs的虚拟dom是个啥情况?

这个周末的先行者课程要讲React的一些东西,所以今天写一些React的内容。 话说前端操作中最消耗资源的是啥?如果我说dom操作,那应该没有人会反对吧。 在以...

2125
来自专栏进击的君君的前端之路

React学习笔记—React组件

1144
来自专栏web前端教室

聊一下JavaScript定时器

image.png 话说JS的定时器,常用的其实就是setTimeout和setInterval这二个。它们俩一个是运行一次就拉倒,另一个是你不叫我停我就一...

2389
来自专栏知道一点点

bootstrap快速入门笔记(二)-栅格系统,响应式类

1,行row必须包含在 .container (固定宽度)或 .container-fluid (100% 宽度)中,一行有12列

2153

扫码关注云+社区

领取腾讯云代金券