前端vue适配不同的分辨率_浅析 React / Vue 跨端渲染原理与实现
當(dāng)下的前端同學(xué)對 React 與 Vue 的組件化開發(fā)想必不會陌生,RN 與 Weex 的跨界也常為我們所津津樂道。UI 框架在實現(xiàn)這樣的跨端渲染時需要做哪些工作,其技術(shù)方案能否借鑒乃至應(yīng)用到我們自己的項目中呢?這就是本文所希望分享的主題。
概念簡介
什么是跨端渲染呢?這里的「端」其實并不局限在傳統(tǒng)的 PC 端和移動端,而是抽象的渲染層 (Renderer)。渲染層并不局限在瀏覽器 DOM 和移動端的原生 UI 控件,連靜態(tài)文件乃至虛擬現(xiàn)實等環(huán)境,都可以是你的渲染層。這并不只是個美好的愿景,在 8102 年的今天,除了 React 社區(qū)到 .docx / .pdf 的渲染層以外,Facebook 甚至還基于 Three.js 實現(xiàn)了到 VR 的渲染層,即 ReactVR。現(xiàn)在回顧 React 的 Learn Once, Write Anywhere 口號,實際上強調(diào)的就是它對各種不同渲染層的支持:
為什么不直接使用渲染層的 API 呢?跨端開發(fā)的一個痛點,就在于各種不同渲染層的學(xué)習(xí)、使用與維護成本。而不管是 React 的 JSX 還是 Vue 的 .vue 單文件組件,都能有效地解耦 UI 組件,提高開發(fā)效率與代碼維護性。從而很自然地,我們就會希望使用這樣的組件化方式來實現(xiàn)我們對渲染層的控制了。
在開始介紹如何為 React / Vue 適配不同渲染層之前,我們不妨回顧一下它們在老本行 DOM 中執(zhí)行時的基本層次結(jié)構(gòu)。比如我們都知道,在瀏覽器中使用 React 時,我們一般需要分別導(dǎo)入 react 與 react-dom 兩個不同的 package,這時前端項目的整體結(jié)構(gòu)可以用下圖簡略地表示:
很多前端同學(xué)熟悉的 UI 庫、高階組件、狀態(tài)管理等內(nèi)容,實際上都位于圖中封裝后「基于 React 實現(xiàn)」的最頂層,連接 React 與 DOM 的 React DOM 一層則顯得有些默默無聞。而在 Vue 2.x 中,這種結(jié)構(gòu)是類似的。不過 Vue 目前并未實現(xiàn) React 這樣的拆分,其簡化的基本結(jié)構(gòu)如下圖所示:
如何將它們這個為 DOM 設(shè)計的架構(gòu)遷移到不同的渲染層呢?下文中會依次介紹這些實現(xiàn)方案:
- 基于 React 16 Reconciler 的適配方式
- 基于 Vue EventBus 的非侵入式適配方式
- 基于 Vue Mixin 的適配方式
- 基于 Vue Platform 定制的適配方式
React Reconciler 適配
之所以首先介紹 React,是因為它已經(jīng)提供了成型的接口供適配之用。在 React 16 標(biāo)志性的 Fiber 架構(gòu)中,react-reconciler 模塊將基于 fiber 的 reconciliation 實現(xiàn)封裝為了單獨的一層。這個模塊與我們定制渲染層的需求有什么關(guān)系呢?它的威力在于,只要我們?yōu)?Reconciler 提供了宿主渲染環(huán)境的配置,那么 React 就能無縫地渲染到這個環(huán)境。這時我們的運行時結(jié)構(gòu)如下圖所示:
上圖中我們所需要實現(xiàn)的核心模塊即為 Adapter,這是將 React 能力擴展到新渲染環(huán)境的橋梁。如何實現(xiàn)這樣的適配呢?
我們以適配著名的 WebGL 渲染庫 PIXI.js 為例,簡要介紹這一機制如何工作。首先,我們所實現(xiàn)的適配層,其最終的使用形式應(yīng)當(dāng)如下:
import * as PIXI from 'pixi.js'import React from 'react'import { ReactPixi } from 'our-react-pixi'import { App } from './app'// 目標(biāo)渲染容器const container = new PIXI.Application()// 使用我們的渲染層替代 react-domReactPixi.render(, container)復(fù)制代碼這里我們需要實現(xiàn)的就是 ReactPixi 模塊。這個模塊是 Renderer 的一層薄封裝:
// Renderer 需要依賴 react-reconcilerimport { Renderer } from './renderer'let containerexport const ReactPixi = { render (element, pixiApp) { if (!container) { container = Renderer.createContainer(pixiApp) } // 調(diào)用 React Reconciler 更新容器 Renderer.updateContainer(element, container, null) }}復(fù)制代碼它依賴的 Renderer 是什么形式的呢?大致是這樣的:
import ReactFiberReconciler from 'react-reconciler'export const Renderer = ReactFiberReconciler({ now: Date.now, createInstance () {}, appendInitialChild () {}, appendChild () {}, appendChildToContainer () {}, insertBefore () {}, insertInContainerBefore () {}, removeChild () {}, removeChildFromContainer () {}, getRootHostContext () {}, getChildHostContext () {}, prepareUpdate () {}, // ...})復(fù)制代碼這些配置相當(dāng)于 Fiber 進行渲染的一系列鉤子。我們首先提供一系列的 Stub 空實現(xiàn),而后在相應(yīng)的位置實現(xiàn)按需操作 PIXI 對象的代碼即可。例如,我們需要在 createInstance 中實現(xiàn)對 PIXI 對象的 new 操作,在 appendChild 中為傳入的 PIXI 子對象實例加入父對象等。只要這些鉤子都正確地與渲染層的相應(yīng) API 綁定,那么 React 就能將其完整地渲染,并在 setState 時依據(jù)自身的 diff 去實現(xiàn)對其的按需更新了。
這些連接性的膠水代碼完成后,我們就能夠用 React 組件來控制 PIXI 這樣的第三方渲染庫了:
這就是基于 React 接入渲染層適配的基本實現(xiàn)了。
Vue 非侵入式適配
由于 Vue 暫時未提供類似 ReactFiberReconciler 這樣專門用于適配渲染層的 API,因此基于 Vue 的渲染層適配在目前有較多不同的實現(xiàn)方式。我們首先介紹「非侵入式」的適配,它的特點在于完全可在業(yè)務(wù)組件中實現(xiàn)。其基本結(jié)構(gòu)形如下圖:
這個實現(xiàn)的初衷是讓我們以這種方式編寫渲染層組件:
首先我們實現(xiàn)最外層的 pixi-renderer 組件。基于 Vue 中類似 Context 的 Provide / Inject 機制,我們可以將 PIXI 注入該組件中,并基于 Slot 實現(xiàn) Renderer 的動態(tài)內(nèi)容:
// renderer.jsimport Vue from 'vue'import * as PIXI from 'pixi.js'export default { template: `這樣我們就具備了最外層的渲染層容器了。接下來讓我們看看內(nèi)層的 Container 組件(注意這里的 Container 不代表最外層的容器,只是 PIXI 中代表節(jié)點的概念):
// container.jsexport 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) }) }) }}復(fù)制代碼這個組件里顯得古怪的 render 是由于其雖然無需模板,但卻可能有子組件的特點所決定的。其主要作用即是維護渲染層對象與 Vue 之間的狀態(tài)一致。最后讓我們看看作為葉子節(jié)點的 Text 組件實現(xiàn):
// text.jsexport 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) }) }) }}復(fù)制代碼這樣我們就模擬出了和 React 類似的組件開發(fā)體驗。但這里存在幾個問題:
- 我們無法脫離 DOM 做渲染。
- 我們必須在各個定制的組件中手動維護 PIXI 實例狀態(tài)。
- 使用了 EventBus 和 props 兩套組件間通信機制,存在冗余。
有沒有其它的實現(xiàn)方案呢?
Vue Mixin 適配
將 DOM 節(jié)點繪制到 Canvas 的 vnode2canvas 渲染庫實現(xiàn)了一種特殊的技術(shù),可以通過 Mixin 的方式實現(xiàn)對 Vnode 的監(jiān)聽。這就相當(dāng)于實現(xiàn)了一個直接到 Canvas 的渲染層。這個方案的結(jié)構(gòu)大致形如這樣:
它的源碼并不多,亮點在于這個 Mixin 的 mounted 鉤子:
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) }},復(fù)制代碼由于這里的 updateCanvas 中返回了 Vnode(雖然這個行為似乎有些不合語義的直覺),故而這里實際上會在 Vnode 更新時觸發(fā)對 Canvas 的渲染。這樣我們就能巧妙地將虛擬節(jié)點樹的更新與渲染層直接聯(lián)系在一起了。
這個實現(xiàn)確實很新穎,不過多少有些 Hack 的味道:
- 它需要為 Vue 組件注入一些特殊的方法與屬性。
- 它需要耦合 Vnode 的數(shù)據(jù)結(jié)構(gòu),這在 React Reconciler 中是一種反模式。
- 它需要自己實現(xiàn)對 Vnode 的遍歷與對 Canvas 對象的 getter 代理,實現(xiàn)成本較高。
- 它仍然附帶了 Vue 自身到 DOM 的渲染層。
有沒有一些更加「正統(tǒng)」的方法呢?
Vue Platform 定制適配
可以認為 Vue 2.x 中對 Weex 的支持方式,是最貼合我們對定制渲染層的理解的。大名鼎鼎的 mpvue 也是按照這個方案實現(xiàn)了到小程序的渲染層。類似地,我們可以簡略地畫出它的結(jié)構(gòu)圖:
上圖中的 Platform 是什么呢?我們只要打開 mpvue 的源碼,很容易找到它在 platforms 目錄下新增的目錄結(jié)構(gòu):
platforms├── mp│?? ├── compiler│?? │?? ├── codegen│?? │?? ├── directives│?? │?? └── modules│?? ├── runtime│?? └── util├── web│?? ├── compiler│?? │?? ├── directives│?? │?? └── modules│?? ├── runtime│?? │?? ├── components│?? │?? ├── directives│?? │?? └── modules│?? ├── server│?? │?? ├── directives│?? │?? └── modules│?? └── util└── weex ├── compiler │?? ├── directives │?? └── modules ├── runtime │?? ├── components │?? ├── directives │?? └── modules └── util復(fù)制代碼上面的 mp 實際上就是新增的小程序渲染層入口了。可以看到渲染層是獨立于 Vue 的 core 模塊的。那么這里的適配需要做哪些處理呢?概括而言有以下這些:
- 編譯期的目標(biāo)代碼生成(這個應(yīng)當(dāng)是小程序的平臺特性所決定的)。
- runtime/events 模塊中渲染層事件到 Vue 中事件的轉(zhuǎn)換。
- runtime/lifecycle 模塊中渲染層與 Vue 生命周期的同步。
- runtime/render 模塊中對小程序 setData 渲染的支持與優(yōu)化。
- runtime/node-ops 模塊中對 Vnode 操作的處理。
這里有趣的地方在于 node-ops,和筆者一開始設(shè)想中在此同步渲染層對象的狀態(tài)不同,mpvue 的實現(xiàn)看起來非常容易閱讀……像這樣:
// runtime/node-ops.jsconst 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}復(fù)制代碼看起來這不是什么都沒有做嗎?個人理解里這和小程序的 API 有更多的關(guān)系:它需要與 .wxml 模板結(jié)合的 API 加大了按照配置 Reconciler 的方法將狀態(tài)管理由 Vue 接管的難度,因而較難通過這個方式直接適配小程序為渲染層,還不如通過一套代碼同時生成 Vue 與小程序的兩棵組件樹并設(shè)法保持其同步來得劃算。
到這里我們已經(jīng)基本介紹了通過添加 platform 支持 Vue 渲染層的基本方式,這個方案的優(yōu)勢很明顯:
- 它無需在 Vue 組件中使用渲染層 API。
- 它對 Vue 業(yè)務(wù)組件的侵入相對較少。
- 它不需要耦合 Vnode 的數(shù)據(jù)結(jié)構(gòu)。
- 它可以確實地脫離 DOM 環(huán)境。
而在這個方案的問題上,目前最大的困擾應(yīng)該是它必須 fork Vue 源碼了。除了維護成本以外,如果在基于原生 Vue 的項目中使用了這樣的渲染層,那么就將會存在兩個具有細微區(qū)別的不同 Vue 環(huán)境,這聽起來似乎有些不清真啊…好在這塊的對外 API 已經(jīng)在 Vue 3.0 的規(guī)劃中了,值得期待 XD
總結(jié)
到此為止,我們已經(jīng)總結(jié)了 React 與 Vue 中定制渲染層的主要方式。重復(fù)一遍:
- 基于 React 16 Reconciler 的適配方式,簡單直接。
- 基于 Vue EventBus 的非侵入式適配方式,簡單但對外暴露的細節(jié)較多。
- 基于 Vue Mixin 的適配方式,Hack 意味較強。
- 基于 Vue Platform 定制的適配方式,最為靈活但需要 fork 源碼。
可以看到在目前的時間節(jié)點上,沒有路徑依賴的項目在定制 Canvas / WebGL 渲染層時使用 React 較為簡單。而在 Vue 的方案選擇上,參考尤大在筆者知乎回答里的評論,fork 源碼修改的方式反而是向后兼容性較好的方案。
除了上文中的代碼片段外,筆者編輯本文的過程中也實現(xiàn)了若干渲染適配層的 POC 原型,它們可以在 renderer-adapters-poc 這個倉庫中看到。
倉庫地址:https://github.com/doodlewind/render-adapters-poc
原鏈接:https://juejin.im/post/5bbc99986fb9a05d3c8014b0
總結(jié)
以上是生活随笔為你收集整理的前端vue适配不同的分辨率_浅析 React / Vue 跨端渲染原理与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巴基斯坦飞行员成功试飞中guo歼10!
- 下一篇: 部队文职一年招录几次