跨端渲染是渲染层并不局限在浏览器 DOM 和移动端的原生 UI 控件,连静态文件乃至虚拟现实等环境,均可以是你的渲染层。这并不仅是个美好的愿景,在今天,除了 React 社区到 .docx
/ .pdf
的渲染层之外,Facebook 甚至还基于 Three.js 实现了到 VR 的渲染层,即 ReactVR。vue
.vue
单文件组件,都能有效地解耦 UI 组件,提升开发效率与代码维护性。从而很天然地,咱们就会但愿使用这样的组件化方式来实现咱们对渲染层的控制了。
react-reconciler
模块将基于 fiber 的 reconciliation 实现封装为了单独的一层。这个模块与咱们定制渲染层的需求有什么关系呢?它的威力在于,
只要咱们为 Reconciler 提供了宿主渲染环境的配置,那么 React 就能无缝地渲染到这个环境
import * as PIXI from 'pixi.js'
import React from 'react'
import { ReactPixi } from 'our-react-pixi'
import { App } from './app'node
// 目标渲染容器
const container = new PIXI.Application()react
// 使用咱们的渲染层替代 react-dom
ReactPixi.render(<App />, container)git
ReactPixi
模块。这个模块是 Renderer 的一层薄封装:
// Renderer 须要依赖 react-reconciler
import { Renderer } from './renderer'github
let containercanvas
export const ReactPixi = {
render (element, pixiApp) {
if (!container) {
container = Renderer.createContainer(pixiApp)
}
// 调用 React Reconciler 更新容器
Renderer.updateContainer(element, container, null)
}
}小程序
createInstance
中实现对 PIXI 对象的 new 操做,在
appendChild
中为传入的 PIXI 子对象实例加入父对象等。只要这些钩子都正确地与渲染层的相应 API 绑定,那么 React 就能将其完整地渲染,并在
setState
时依据自身的 diff 去实现对其的按需更新了。
ReactFiberReconciler
这样专门用于适配渲染层的 API,所以基于 Vue 的渲染层适配在目前有较多不一样的实现方式。咱们首先介绍「非侵入式」的适配,它的特色在于彻底可在业务组件中实现。
<div id="app">
<pixi-renderer>
<container @tick="tickInfo" @pointerdown="scaleObject">
<pixi-text :x="10" :y="10" content="hello world"/>
</container>
</pixi-renderer>
</div>浏览器
pixi-renderer
组件。基于 Vue 中相似 Context 的 Provide / Inject 机制,咱们能够将 PIXI 注入该组件中,并基于 Slot 实现 Renderer 的动态内容:
// renderer.js
import Vue from 'vue'
import * as PIXI from 'pixi.js'数据结构
export default {
template: `
<div class="pixi-renderer">
<canvas ref="renderCanvas"></canvas>
<slot></slot>
</div>`,
data () {
return {
PIXIWrapper: { PIXI, PIXIApp: null },
EventBus: new Vue()
}
},
provide () {
return {
PIXIWrapper: this.PIXIWrapper,
EventBus: this.EventBus
}
},
mounted () {
this.PIXIWrapper.PIXIApp = new PIXI.Application({
view: this.$refs.renderCanvas
})
this.EventBus.$emit('ready')
}
}架构
// container.js
export default {
inject: ['EventBus', 'PIXIWrapper'],
data () {
return {
container: null
}
},
render (h) { return h('template', this.$slots.default) },
created () {
this.container = new this.PIXIWrapper.PIXI.Container()
this.container.interactive = true
this.container.on('pointerdown', () => {
this.$emit('pointerdown', this.container)
})
// 维护 Vue 与 PIXI 组件间同步
this.EventBus.$on('ready', () => {
if (this.$parent.container) {
this.$parent.container.addChild(this.container)
} else {
this.PIXIWrapper.PIXIApp.stage.addChild(this.container)
}
this.PIXIWrapper.PIXIApp.ticker.add(delta => {
this.$emit('tick', this.container, delta)
})
})
}
}
render
是因为其虽然无需模板,但却可能有子组件的特色所决定的。其主要做用便是维护渲染层对象与 Vue 之间的状态一致。最后让咱们看看做为叶子节点的 Text 组件实现:
// text.js
export default {
inject: ['EventBus', 'PIXIWrapper'],
props: ['x', 'y', 'content'],
data () {
return {
text: null
}
},
render (h) { return h() },
created () {
this.text = new this.PIXIWrapper.PIXI.Text(this.content, { fill: 0xFF0000 })
this.text.x = this.x
this.text.y = this.y
this.text.on('pointerdown', () => this.$emit('pointerdown', this.text))
this.EventBus.$on('ready', () => {
if (this.$parent.container) {
this.$parent.container.addChild(this.text)
} else {
this.PIXIWrapper.PIXIApp.stage.addChild(this.text)
}
this.PIXIWrapper.PIXIApp.ticker.add(delta => {
this.$emit('tick', this.text, delta)
})
})
}
}
这样咱们就模拟出了和 React 相似的组件开发体验。但这里存在几个问题:
mounted() {
if (this.$options.renderCanvas) {
this.options = Object.assign({}, this.options, this.getOptions())
constants.IN_BROWSER && (constants.rate = this.options.remUnit ? window.innerWidth / (this.options.remUnit * 10) : 1)
renderInstance = new Canvas(this.options.width, this.options.height, this.options.canvasId)
// 在此 $watch Vnode
this.$watch(this.updateCanvas, this.noop)
constants.IN_BROWSER && document.querySelector(this.options.el || 'body').appendChild(renderInstance._canvas)
}
},
因为这里的 updateCanvas
中返回了 Vnode
(虽然这个行为彷佛有些不合语义的直觉),故而这里实际上会在 Vnode 更新时触发对 Canvas 的渲染。这样咱们就能巧妙地将虚拟节点树的更新与渲染层直接联系在一块儿了。
这个实现确实很新颖,不过多少有些 Hack 的味道:
setData
渲染的支持与优化。// runtime/node-ops.js
const obj = {}
export function createElement (tagName: string, vnode: VNode) {
return obj
}
export function createElementNS (namespace: string, tagName: string) {
return obj
}
export function createTextNode (text: string) {
return obj
}
export function createComment (text: string) {
return obj
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {}
export function removeChild (node: Node, child: Node) {}
export function appendChild (node: Node, child: Node) {}
export function parentNode (node: Node) {
return obj
}
export function nextSibling (node: Node) {
return obj
}
export function tagName (node: Element): string {
return 'div'
}
export function setTextContent (node: Node, text: string) {
return obj
}
export function setAttribute (node: Element, key: string, val: string) {
return obj
}
看起来这不是什么都没有作吗?我的理解里这和小程序的 API 有更多的关系:它须要与 .wxml
模板结合的 API 加大了按照配置 Reconciler 的方法将状态管理由 Vue 接管的难度,于是较难经过这个方式直接适配小程序为渲染层,还不如经过一套代码同时生成 Vue 与小程序的两棵组件树并设法保持其同步来得划算。
到这里咱们已经基本介绍了经过添加 platform 支持 Vue 渲染层的基本方式,这个方案的优点很明显:
而在这个方案的问题上,目前最大的困扰应该是它必须 fork Vue 源码了。除了维护成本之外,若是在基于原生 Vue 的项目中使用了这样的渲染层,那么就将会存在两个具备细微区别的不一样 Vue 环境,这听起来彷佛有些不清真啊…好在这块的对外 API 已经在 Vue 3.0 的规划中了,值得期待 XD
总结;