默认的目标渲染平台
- 在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>
复制代码