实现自定义渲染器 customRender
- 自定义渲染器,允许我们自定义渲染接口,可以把我们的程序渲染到任意平台,默认vue3渲染到dom平台,如果我们想要渲染到 canvas 平台,就需要借助自定义渲染器
- 来看我们原来封装的函数是如何渲染 dom 的
js
复制代码
function mountElement(vnode, container, parentComponent) {
const { type, props, children, shapeFlag } = vnode;
const el = (vnode.el = document.createElement(type)); // ✅
if (shapeFlag & shapeFlags.TEXT_CHILDREN) {
el.textContent = children;
} else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el, parentComponent);
}
for (let key in props) {
const isOn = (key) => /^on[A-Z]/.test(key);
if (isOn(key)) {
const event = key.slice(2).toLocaleLowerCase();
el.addEventListener(event, props[key], false);
} else {
let val = props[key];
el.setAttribute(key, val); // ✅
}
}
container.append(el); // ✅
}
- 可以看到有具体的 api,都已经写死了,我们应该抽离出来,让这块创建 dom 应该依赖稳定的接口,而不是依赖具体的实现,像下面这样
js
复制代码
export function createRenderer(options) { // ✅
const { createElement, patchProps, insert} = options // ✅
...... // 代码省略
function mountElement(vnode, container, parentComponent) {
const { type, props, children, shapeFlag } = vnode
// const el = (vnode.el = document.createElement(type))
const el = vnode.el = createElement(type) // ✅
if (shapeFlag & shapeFlags.TEXT_CHILDREN) {
el.textContent = children
} else if (shapeFlag & shapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el, parentComponent)
}
for (let key in props) {
let value = props[key]
// const isOn = (key) => /^on[A-Z]/.test(key)
// if (isOn(key)) {
// const event = key.slice(2).toLocaleLowerCase()
// el.addEventListener(event, props[key], false)
// } else {
// let val = props[key]
// el.setAttribute(key, val)
// }
patchProps(el, key, value) // ✅
}
// container.append(el)
insert(el, container) // ✅
}
...... // 代码省略
}
- 搞成这样了,我们在在哪里创建render呢?可以看到我们之前直接创建 render 并导出,现在使用 createRenderer 进行了包裹,并传入接口
- 我们可以 创建一个 runtime-dom 文件夹,这里面组织基于 dom 的接口的实现
- 将 runtime-core 的 renderer 文件中的 createRenderer 导出到 index.ts
- 从 runtime-dom 的 index.ts 中引入 runtime-core/index.ts 中导出的 createRenderer 方法进行使用
js
复制代码
// runtime-core/index.ts
export { createRenderer } from "./renderer";
// runtimer-dom/index.ts
import { createRenderer } from "../runtime-core";
export function createElement(type) {
return document.createElement(type);
}
export function patchProps(el, key, value) {
const isOn = (key) => /^on[A-Z]/.test(key);
if (isOn(key)) {
const event = key.slice(2).toLocaleLowerCase();
el.addEventListener(event, value, false);
} else {
el.setAttribute(key, value);
}
}
export function insert(el, parent) {
parent.append(el);
}
const renderer = createRenderer({
createElement,
patchProps,
insert,
});
- 到这里我们启动项目进行自动打包 pnpm build --watch , 发现 createApp 中的 render 报错,因为我们改了这块的逻辑,目前没有 render 的导出了
- 那我们怎么处理呢?
- 我们把原来 createApp 中的逻辑导出,在 renderer.ts 中的 createRenderer 中引入,而这里正好里面放着 render 方法,我们在这里使用 render 方法,并使用 原 createApp 中的逻辑重新生成一个 createApp
js
复制代码
// 原来逻辑
// export function createApp(rootComponent) {
// return {
// mount(rootContainer) {
// const vnode = createVNode(rootComponent)
// render(vnode, rootContainer)
// }
// }
// }
// createApp.ts
export function createAppAPI(render) { // ✅ 将原来的逻辑进行导出
return function createApp(rootComponent) {
return {
mount(rootContainer) {
const vnode = createVNode(rootComponent)
render(vnode, rootContainer)
}
}
}
}
// renderer.ts
import { createAppAPI } from "./createApp"; // ✅ 将原来逻辑导入进行复用
export function createRenderer(options) {
function render(vnode, container, parentComponent?) {
patch(vnode, container, parentComponent)
}
return { // ✅ 重新创建一个 createApp
createApp: createAppAPI(render)
}z
}
- 我们原来把 createApp 直接在runtime-core/index.ts 中直接导出给用户使用,现在我们把 createApp 放到 createRenderer 中,我们就不能这么干了,我们需要把 createApp 导出给用户使用,那我们怎么处理呢?
- 我们可以在 runtime-dom/index.ts 中导出 createApp 方法,就像这样
js
复制代码
// runtime-dom/index.ts
import { createRenderer } from "../runtime-core";
const renderer:any = createRenderer({
createElement,
patchProps,
insert
})
export function createApp(...args) {
return renderer.createApp(...args)
}
// src/index.ts
export * from './runtime-core'
export * from './runtime-dom'
// runtime-dom 在 runtime-core 上面一层,且 runtime-core 更通用,我们可以在 runtime-dom 中导出 runtime-core 中的方法
- 现在我们把 依赖 dom 实现页面,抽离出来,转换为依赖接口,如果我们不想依赖 dom 实现页面,我们可以依赖 canvas 实现页面,我们引入 createRenderer 方法,传入新的接口,就可以实现页面的渲染了
- 下面我们实现一个 canvas 渲染器
js
复制代码
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://pixijs.download/v7.4.2/pixi.js"></script> // ✅ 引入 pixi.js
</head>
<body>
<div id="App"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
js
复制代码
// App.js ✅
import { h } from "../../lib/guide-mini-vue.esm.js";
export const App = {
render() {
return h("rect",{
x: this.x,
y: this.y
}, )
},
setup() {
return {
x: 100,
y: 100
}
}
}
js
复制代码
// main.js
import { App } from './App.js'
import { createRenderer } from '../../lib/guide-mini-vue.esm.js' // ✅ 引入 createRenderer 方法
// ✅ 设置画布
const game = new PIXI.Application({
width: 500,
height: 500
})
document.body.appendChild(game.view)
const renderer = createRenderer({ // ✅ 传入接口
createElement(type) {
if(type === 'rect') {
const rect = new PIXI.Graphics()
rect.beginFill(0xff0000)
rect.drawRect(0,0,100,100)
rect.endFill()
return rect
}
},
patchProps(el, key, value) {
el[key] = value
},
insert(el, parent) {
parent.addChild(el)
}
})
// ✅ game.stage 获取画布
renderer.createApp(App).mount(game.stage)
- 注意如果我们的路径不能够自动找到 index,可以尝试给 tsconfig.json 添加下面配置
json
复制代码
{
"compilerOptions": {
"moduleResolution": "node"
}
}