跨端渲染是渲染層並不侷限在瀏覽器 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
總結;