默認的目標渲染平臺
- 在vue3中容許用戶自定義目標渲染平臺,以往的版本中目標渲染被侷限於瀏覽器dom平臺,而如今能夠把 vue 的開發模型擴展到其餘平臺。點擊進入官網
- Tips:以往解決把 vue 的開發模型擴展到其餘平臺(
Canvas、iOS、Android等等
)的方式之一是藉助第三方工具例如WEEX(點擊進入官網)
- 咱們先來弄懂vue是如何定義默認的目標渲染平臺的,也就是說如何將目標渲染到瀏覽器dom平臺上。能夠先參考官方圖:
- 咱們先構建起一個初始化的vue3新項目,來一步步分析vue是怎麼默認的將目標渲染到瀏覽器dom平臺上,下面是項目中入口文件main.js的代碼
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
複製代碼
<template>
<div>我是根組件實例</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
複製代碼
- 寫這兩個文件後咱們一運行命令
npm run serve
,會發現咱們寫在'./APP.vue'
的template已經被渲染到瀏覽器dom平臺上成爲了真實的dom元素了,以下圖:
- 咱們應該要發出疑惑,寫在
'./APP.vue'
的template是怎麼被渲染到瀏覽器dom平臺上又被轉換成真實的dom元素呢?若是讓咱們本身來作得怎麼作到呢?咱們能夠在入口文件main.js
中找到關鍵線索:
import App from './App.vue'
//咱們能夠打印出App來查看一下
console.log(App)
複製代碼
- 咱們先打印出App查看一下,這是一個什麼信息?以下圖:
- 在打印出來的App對象裏,咱們並無在該對象上找到template屬性!咱們不由發出更大的問號,沒有了template的信息要怎麼作到被轉換成真實的dom元素???答案是依靠該對象上的render函數,能夠理解爲template通過了vue的特殊加工轉換爲了render函數,而且這個render函數會依照template的有用信息返回一個虛擬DOM
(Vnode)
- 咱們能夠藉助工具來驗證,以下圖:
- 如上圖,咱們能夠明確的看到template通過了vue的特殊加工轉換爲了render函數,而且這個render函數會依照template的有用信息返回一個虛擬DOM
(Vnode)
,關於虛擬DOM的描述能夠參考官網點擊進入官網
- 咱們在發現了這個線索以後,咱們能夠手動調用App對象下的render函數,來查看一下返回的虛擬DOM到底長什麼樣子,代碼和圖片以下:
import App from './App.vue'
console.log(App.render());
複製代碼
- 如上圖,咱們能夠從返回的虛擬DOM中獲得許多有用的信息,這裏我用紅色框出來的有用信息來簡單實現一下如何渲染到瀏覽器dom平臺上而且讓其轉換成真實的dom元素,代碼以下:
//假設這個是虛擬Dom的信息
//僅僅是爲了演示基本思想
const vnode={
type:'div',
children:'123'
}
const element=document.creatElement(vnode.type)
element.innerText=vnode.children
//告訴它的出口在哪裏 要被渲染到哪裏去
//這裏的出口先假設爲#app這個容器
document.querySelector('#app').appendChild(element)
複製代碼
- 到了這一步咱們也作到了如何將寫在
'./APP.vue'
的template渲染到瀏覽器dom平臺上而且轉換成真實的dom元素(雖然寫的代碼很菜
),但是這一套邏輯vue已經幫咱們實現了,咱們如今再來看入口文件main.js的代碼:
/*
//createApp的做用是將傳入的組件轉換爲真實的Dom元素
//核心思想就是剛纔寫的
//const element=document.creatElement(vnode.type)
//element.innerText=vnode.children
*/
import { createApp } from 'vue'
import App from './App.vue'
/*
//mount的做用是告訴它的出口在哪裏、要被渲染到哪裏去
//核心思想就是剛纔寫的
//document.querySelector('#app').appendChild(element)
*/
createApp(App).mount('#app')
複製代碼
自定義的目標渲染平臺
- 咱們在實現自定義的目標渲染平臺以前,還得在溫習一遍默認的目標渲染平臺的流程邏輯圖,以下圖:
- 咱們知道canvas也是一個平臺,這裏就以如何使用vue3渲染到canvas平臺上來舉例說明。咱們先來當作果圖:
- 咱們即將要實現使用vue3的新特性custom renderer來將目標元素渲染到canvas平臺上,咱們如今實現的邏輯圖以下:(
注意分支
)
- 在實現以前,咱們必須得先學會幾個簡單的關於canvas的api。爲了快速上手,在這裏我使用了
pixi.js
第三方工具(點擊進入官網),pixi.js
是基於canvas 的遊戲渲染引擎庫,藉助pixi.js
能夠省去繁瑣的操縱canvas的流程,讓咱們專心於感覺vue3的新特性custom renderer的魅力。
- 下面是使用
pixi.js
建立canvas並往canvas內添加各類東西的流程圖:(最終爲了能夠直觀的看到效果,將canvas呈如今瀏覽器上(**插入到dom**)
)
- 在vue3的項目使用安裝
npm i pixi.js
後,咱們來看一下簡單的關於canvas的使用方式,代碼和簡圖以下:
import {
//初始化
Application,
//建立矩形
Graphics,
//建立圖片
Sprite,
//建立文字
Texture,
Text,
TextStyle,
//建立容器
Container,
} from "pixi.js";
/*
經過 new Application來初始化建立canvas
options規定建立的canvas的寬和高
*/
const game = new Application({
width: 500,
height: 500,
});
/*
爲了能夠直觀的看到效果
將canvas呈如今瀏覽器上(**插入到dom**)
game.view是canvas視圖元素
*/
document.body.append(game.view);
/*
建立一個矩形
rect.x和rect.y是設置矩形的初始位置偏移量
//單獨(獨自)添加矩形到canvas容器上使用下一行命令
game.stage.addChild(rect);
*/
const rect = new Graphics();
rect.beginFill(0xffff00);
rect.drawRect(0, 0, 50, 50);
rect.endFill();
rect.x = 50;
rect.y = 50;
/*
建立圖片
//單獨(獨自)添加矩形到canvas容器上使用下一行命令
game.stage.addChild(img);
*/
import logo from "./assets/logo.png";
const img = new Sprite();
//指定後才容許給圖片添加點擊事件
img.interactive = true;
//指定圖片的src路徑
img.texture = Texture.from(logo);
//添加幀循環 會一直執行handleTicker事件直至刪除該幀循環
game.ticker.add(handleTicker);
//handleTicker事件 令圖片的x偏移量不斷增長
const handleTicker = () => {img.x++};
/*
pixi的點擊事件名
必須配合img.interactive = true才能容許被點擊
*/
img.on("pointertap", () => {
game.ticker.remove(handleTicker);
});
/*
建立文本
//單獨(獨自)添加矩形到canvas容器上使用下一行命令
game.stage.addChild(text);
*/
const text = new Text("heihei");
text.style = new TextStyle({
fill: "red",
});
text.x = 380;
/*
建立容器
//容器中能夠放圖片、文字、矩形等等
//容器是一個大的總體
//將容器添加到canvas上的話
//容器中的內容也會一併被添加到canvas上
//即下一行代碼
game.stage.addChild(box);
*/
const box = new Container();
box.addChild(text);
box.addChild(img);
//統一的移動它們的位置
box.x = 2
/*
若是你想要把你建立的東西渲染到canvas容器內的話
必須把東西經過game.stage.addChild的方式添加進去才能顯示
*/
//單獨添加以添加矩形爲例
game.stage.addChild(rect);
//添加一個容器
//(容器中能夠包含圖片、文字等等也會被一併添加上canvas)
game.stage.addChild(box);
複製代碼
- 咱們如今藉助
pixi.js
學會了對canvas的簡單操縱,接下來咱們就要使用vue3的custom renderer來將元素渲染到canvas平臺上了。
自定義渲染到canvas平臺上
- 咱們在上一講已經學會了藉助
pixi.js
對canvas進行簡單的操縱,並梳理了自定義渲染到canvas平臺上的邏輯,讓咱們在回顧一下邏輯圖,再開始着手使用vue3的新特性custom renderer:
- 咱們接下來如何操做來完成這一套自定義邏輯呢??有請咱們今天的主角登場:custom renderer(點擊進入官網)。
- 咱們先來重寫
App.vue
裏的代碼,參考以下:
<template>
<!-- 這裏的circle和rect是自定義標籤
不是組件不是組件不是組件 -->
<circle x="50" y="50"></circle>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
複製代碼
- 咱們接着重寫入口文件
main.js
中的代碼。參考以下:
/* 默認的渲染到瀏覽器dom平臺上的代碼
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
*/
/*自定義渲染到canvas平臺上
createRenderer就是告訴vue我要自定義渲染平臺了
自定義渲染器能夠傳入特定於平臺的類型
*/
import { createRenderer } from "vue";
//咱們不急着往createRenderer添加相關配置
//咱們先打印render查看這個究竟是個什麼
const render=createRenderer({})
console.log(render,'render');
複製代碼
- 咱們從vue中導出了createRenderer函數,在不配置任何選項的狀況下打印出render來查看這究竟是個什麼東西??以下圖:
- 咱們在打印出的圖中能夠發現兩條熟悉的線索,一個是該render上有createApp方法另外一個是該render上有render方法!!!!
- 咱們還記得
import { createApp } from 'vue'
這一句代碼,這一句代碼配合createApp(App).mount('#app')
就將寫在App.vue
中的template給渲染到瀏覽器Dom平臺上了,因此vue暴露出來的createApp是已經幫咱們封裝好邏輯的了。
- 咱們如今的任務是調用render下的createApp函數來封裝實現咱們的邏輯,渲染到canvas平臺上,咱們先來看下面的代碼:
import {createRenderer } from 'vue'
import App from './App.vue'
//本身要來寫邏輯
const render=createRenderer({})
/*
本身要來寫邏輯 ----> render下有createApp函數
調用createApp()方法後
返回的對象下依舊是有mount()方法的
*/
render.createApp(這裏要填什麼).mount(這裏又要填什麼)
複製代碼
- 咱們在上面的代碼中先不考慮要怎麼書寫
createRenderer()
函數中的配置項封裝邏輯,先來考慮render.createApp(這裏要填什麼).mount(這裏又要填什麼)
這兩個空要怎麼填的問題?
- 咱們參考
createApp(App).mount('#app')
即可以知道第一個空應該要填的是根組件
,在這裏咱們一樣填的是import App from './App.vue'
導出的App,第二個空應該要填的是根容器
,咱們須要的是渲染到canvas平臺上,因此咱們的根容器得是game.stage
(這裏的game.stage是通過pixi.js初始化後的canvas容器
),代碼以下:
import { Application } from "pixi.js";
//經過 new Application來初始化建立canvas
const game = new Application({
width: 750,
height: 750,
});
// 爲了能夠直觀的看到效果
// 將canvas呈如今瀏覽器上(**插入到dom**)
document.body.append(game.view);
/*
導出canvas容器供
render.createApp(這裏要填什麼).mount(getRootContainer())
使用
*/
export function getRootContainer() {
return game.stage;
}
複製代碼
- 緊接着咱們就來書寫
createRenderer()
函數中的配置項,經過配置項來最終實現咱們的邏輯把在App.vue
中重寫的template渲染到canvas平臺上,來看一下createRenderer()
函數都有哪些配置項,如圖:
- 咱們書寫
createRenderer()
函數中的配置項,經過配置項來最終實現咱們的邏輯,代碼以下:
import { createRenderer } from "vue";
import { Graphics } from "pixi.js";
const renderer = createRenderer({
// 建立一個元素 ---> 抽象的接口函數
// vue執行時會調用這個函數中的邏輯
createElement(type) {
//參考vnode中的type
//由於咱們書寫了<circle></circle>
//因此這裏的type會有circle
console.log(type);
let element;
//調用canvas api來建立矩形、圓形、圖片等等
//層級關係是後添加的在上面
switch (type) {
case "rect":
element = new Graphics();
element.beginFill(0xff0000);
element.drawRect(0, 0, 500, 500);
element.endFill();
break;
case "circle":
element = new Graphics();
element.beginFill(0xffff00);
//第三個參數是圓的半徑
element.drawCircle(0, 0, 50);
element.endFill();
break;
}
//最終必定要返回element不然下方的函數接收不到
return element;
},
patchProp(el, key, prevValue, nextValue) {
/*
向<circle x="50" y="50"></circle>中
傳遞的props能在這裏獲取到
利用這點能夠去改變canvas容器中具體東西的行爲
好比改變位置、添加點擊事件、等等
若是傳遞的是響應式數據的話
當響應式數據變動時canvas上的具體東西也會實時響應更新
好比實時響應移動改變位置等等
console.log(el,'能夠獲得該對象');
console.log(key,'能夠獲得x和y');
console.log(nextValue,'能夠獲得50');
*/
switch (key) {
case "x":
el.x = nextValue;
break;
case "y":
el.y = nextValue;
break;
default:
break;
}
},
// 插入到對應的容器內
insert(el, parent) {
console.log(el, parent);
/*
el是上面的createElement函數中返回的element
parent是render.createApp(App).mount(getRootContainer())中
getRootContainer()的返回值即canvas容器game.stage;
在該函數中把建立的東西(矩形、圖形、圓形等等)添加到canvas容器內
即game.stage.addChild(element);
*/
parent.addChild(el);
},
});
/*
由於vue中本身暴露了默承認以渲染到dom平臺上的createApp方法
咱們模仿這個行爲也暴露一個本身封裝好的渲染到canvas平臺上的createApp方法
只須要經過如下四行代碼就能夠開始使用了
import {createApp} from './runtime-canvas/index';
import App from './App.vue';
import {getRootContainer} from './game/index';
createApp(App).mount(getRootContainer());
*/
export function createApp(rootComponent) {
return renderer.createApp(rootComponent);
}
複製代碼
小案例
- 最後溫故一下利用custom renderer渲染到canvas平臺上的邏輯圖:
//封裝自定義渲染到canvas平臺上的邏輯
import { createApp } from "./runtime-canvas";
import App from "./App.vue";
//初始化canvas的容器
import { getRootContainer } from "./game";
createApp(App).mount(getRootContainer());
複製代碼
- 咱們來看
"./game/index.js"
文件的代碼:
import { Application } from "pixi.js";
const game = new Application({
width: 750,
height: 750,
});
document.body.append(game.view);
export function getRootContainer() {
return game.stage;
}
export function getGame() {
return game
}
複製代碼
- 咱們緊接着看
"./runtime-canvas/index.js"
文件的代碼:
import { createRenderer } from "vue";
import { Graphics } from "pixi.js";
const renderer = createRenderer({
createElement(type) {
let element;
switch (type) {
case "rect":
element = new Graphics();
element.beginFill(0xff0000);
element.drawRect(0, 0, 500, 500);
element.endFill();
break;
case "circle":
//建立球形
element = new Graphics();
element.beginFill(0xffff00);
element.drawCircle(0, 0, 50);
element.endFill();
break;
}
return element;
},
patchProp(el, key, prevValue, nextValue) {
switch (key) {
//根據傳遞的props初始化‘具體東西元素’的位置
//若是props是響應式數據那麼在該響應式數據改變時
//會被這裏攔截到並實時響應更新視圖位置
case "x":
el.x = nextValue;
break;
case "y":
el.y = nextValue;
break;
default:
break;
}
},
insert(el, parent) {
console.log(el, parent);
//添加到canvas容器內
parent.addChild(el);
},
});
export function createApp(rootComponent) {
return renderer.createApp(rootComponent);
}
複製代碼
- 咱們再看
'componenets/Circle.vue'
文件的代碼:
<template>
<circle></circle>
</template>
<script>
export default {
};
</script>
<style></style>
複製代碼
<template>
<Circle :x="x" :y="y" ref="circle"></Circle>
</template>
<script>
import Circle from "./components/Circle";
import {getGame} from './game/index';
import {ref,onMounted, onUnmounted} from 'vue';
export default {
name: "App",
components: {
Circle,
},
setup() {
let x=ref('50')
let y=ref('50')
const game=getGame()
onMounted(()=>{
// console.log(circle,'circle');
// console.log(game,'game');
// console.log(circle.value.$el,'xx');
game.ticker.add(handleTicker);
});
const handleTicker = function(){
// console.log(circle.value.$el);
circle.value.$el.x+=10
if(circle.value.$el.x>700){
game.ticker.remove(handleTicker);
game.ticker.add(handleTicker2);
}
}
const handleTicker2 = function(){
// console.log(circle.value.$el);
circle.value.$el.x-=10
if(circle.value.$el.x<50){
game.ticker.remove(handleTicker2)
game.ticker.add(handleTicker);
}
};
// console.log(circle,'circle');
let circle=ref(null)
onUnmounted(() => {
game.ticker.remove(handleTicker)
game.ticker.remove(handleTicker2)
})
return{
circle,
handleTicker,
x,
y
}
}
}
</script>
<style>
</style>
複製代碼