28-mini-vue customRender

实现自定义渲染器 customRender

  1. 自定义渲染器,允许我们自定义渲染接口,可以把我们的程序渲染到任意平台,默认vue3渲染到dom平台,如果我们想要渲染到 canvas 平台,就需要借助自定义渲染器
  2. 来看我们原来封装的函数是如何渲染 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); // ✅
}
  1. 可以看到有具体的 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) // ✅
    }
    ...... // 代码省略
}
  1. 搞成这样了,我们在在哪里创建render呢?可以看到我们之前直接创建 render 并导出,现在使用 createRenderer 进行了包裹,并传入接口
  2. 我们可以 创建一个 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,
});
  1. 到这里我们启动项目进行自动打包 pnpm build --watch , 发现 createApp 中的 render 报错,因为我们改了这块的逻辑,目前没有 render 的导出了
  2. 那我们怎么处理呢?
    • 我们把原来 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
}
  1. 我们原来把 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 中的方法
  1. 现在我们把 依赖 dom 实现页面,抽离出来,转换为依赖接口,如果我们不想依赖 dom 实现页面,我们可以依赖 canvas 实现页面,我们引入 createRenderer 方法,传入新的接口,就可以实现页面的渲染了
  2. 下面我们实现一个 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)
  1. 注意如果我们的路径不能够自动找到 index,可以尝试给 tsconfig.json 添加下面配置
json 复制代码
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}
相关推荐
小oo呆2 分钟前
【学习心得】Python的Pydantic(简介)
前端·javascript·python
funnycoffee12311 分钟前
F5 Big IP如何设置web和SSH登录的白名单
前端·tcp/ip·ssh
北辰alk18 分钟前
Vue 数据响应式探秘:如何让数组变化无所遁形?
vue.js
JarvanMo18 分钟前
国产 App,求你放过我的 iPhone 电量吧!
前端
先飞的笨鸟22 分钟前
2026 年 Expo + React Native 项目接入微信分享完整指南
前端·ios·app
angelQ24 分钟前
Vercel部署:前后端分离项目的整体部署流程及问题排查
前端·javascript
AI前端老薛24 分钟前
CSS实现动画的几种方式
前端·css
晨米酱26 分钟前
轻量级 Git Hooks 管理工具 Husky
前端·代码规范
Jing_Rainbow27 分钟前
【 前端三剑客-35 /Lesson58(2025-12-08)】JavaScript 原型继承与对象创建机制详解🧬
前端·javascript·面试
携欢28 分钟前
portswigger靶场之修改序列化数据类型通关秘籍
android·前端·网络·安全