Description of project

A virtual DOM plug-in library based on Vue, according to the writing method of Vue render function, directly render Vnode generated by Vue to canvas. Support for general scrolling and some basic element event binding.

Making address:

github

The demo instance:

vnode2canvas


background

Let’s start with a small request: one day, the product came up with a request to create a wechat activity page that could share pictures containing user information. This information needs to be fetched from the interface, and everyone is different. The first time you encounter such a requirement, you will basically go to hand lift canvasAPI to do the rendering function. The steps in this case are roughly as follows:

  1. Write a stringdom templateThe label
  2. Apply colours to a drawingtemplateintodomThe label
  3. To capturedomElement, drawcanvas
  4. canvasApply colours to a drawing pictures

The main problem is poor reusability, followed by performance problems. The interface seen by users may not be consistent with the formal rendered interface, and there may be differences in rendering. As an ambitious front-end, of course, you have to see if there is a better way. So I got to know a library like HTML2Canvas. But it always feels like it has to be converted to the DOM to draw, and the performance and stability are not very good.

We know that Vue uses VNode to render to different ends, is it possible to render to Canvas using VNode? That is, there is no vnode -> HTML -> canvas, but vnode -> canvas. At the same time, using vUE data driven, to achieve the data driven rendering. I have an idea. Let’s do it.

research

60 FPS on the Mobile Web

Canvas is an immediate mode of rendering and does not store additional rendering information. Canvas benefits from immediate mode, allowing drawing commands to be sent directly to the GPU. But using it to build user interfaces requires a higher level of abstraction. Some simple processes, such as drawing an asynchronously loaded resource to an element, are problematic, such as drawing text on a picture. In HTML, this is easy to implement because of the order in which elements exist and the z-index in CSS. Dom rendering is a reserved mode, which is a declarative API for maintaining a hierarchy of objects drawn to it. The advantage of the reserved pattern API is that it is often easier for your application to build complex scenarios, such as DOM. Often this has a performance cost, requiring extra memory to save scenes and update scenes, which can be slow.

It seems that the research on canvas drawing pages has been done for a long time. And the performance is still very good. All the more reason to see if our idea can be realized! More and more expect….

start

Canvas rendering is actually an attempt. Since our predecessors have done sufficient practice, we will stand on the shoulders of giants to achieve a data-driven canvas rendering based on VUE. Just do it! (We only provide ideas here, not specific implementation details, because the implementation is a bit complicated, if you are interested, you can refer to my project implementation, or discuss with us 🙂)

Processing vnode

Those familiar with Vue source code should know that Vue uses the render function, passing the createElement method to construct a Vnode, using publish-subscribe mode to realize the monitoring of data, regenerate vNode. The VNode is finally converted into the view required by each platform. All we need to do is start at the VNode layer. So, we implement a listener function based on the Vue source code and mix it with the Vue instance:

Vue.mixin({ // ... created() { if (this.$options.renderCanvas) { // ... $watch(this.updatecanvas, this.noop); } }, methods: {updateCanvas() {// Simulate the Vue render function // find the renderCanvas method defined in the instance, And passed the createElement method method let vnode = this. $options. RenderCanvas. Call (enclosing _renderProxy, enclosing $createElement method)}})Copy the code

This way we can happily use it inside the component:

renderCanvas (h) {
  return h(...)
}
Copy the code

Canvas element handling

Render vNode we need to make some additional constraints, that is, what render tag we need to render the corresponding canvas element (for example 🌰) :

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

Each of these element classes inherits from a Super class, and since they have different presentation styles, they implement their own DRAW methods to do custom presentation.

Drawing object layout mechanism implementation

The most basic way to draw a Canvas layout is to pass in a series of coordinate points and the relevant base width and height for the Canvas element, which may be written in the actual project like this:

renderCanvas(h) {
  return h('view', {
     style: {
       left: 10,
       top: 10,
       width: 100,
       height: 100
     }
  })
}
Copy the code

There are several solutions, one of which is to use CSS-Layout to do management. Css-layout supports the following conversion attributes:


This is just one layer of transformation, which helps us to write canvas with CSS thinking, but if we are not happy with CSS in JS, we can also write a Webpack loader to load external 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)}`
  }
}
Copy the code

To put it simply: it is mainly to convert CSS files into AST syntax tree, and then convert the syntax tree to the definition form required by Canvas. And injected into the component as a variable.

Implement list scrolling

If we have a lot of elements and need to scroll, we have to solve the problem of scrolling elements inside the canvas. Here I chose to use Zynga Scroller to simulate the user’s scrolling method and redraw the canvas based on the scrolling coordinate points it returned. Those interested can refer to my implementation here:

scroller

Event simulation

For the simulation of CLICK, touch and other DOM events, the scheme we adopt is to detect the click region and find out the lowest element, recursively find the parent element and trigger the corresponding event handler, so as to simulate the bubbling of events. A detailed implementation can be found here:

event

The last

Canvas drawing page is also an innovative attempt, I hope the research here can inspire you, and welcome your PR. There are also a number of performance optimizations, which I don’t have space to describe, but can also be discussed together if you are interested.

Finally: It’s not meant to completely replace DOM based rendering, which still requires text input, copy/paste, accessibility, and SEO. For these reasons, we can use a combination of canvas and DOM-based rendering.