没了虚拟 DOM 也能跨平台?

前言

之前写了篇文章《无虚拟 DOM 版 Vue 进行到哪一步了?》

这篇文章的评论区有很多人都觉得这是在倒退:

在这个人的眼里,无虚拟 DOMVue + SSR 就等于 jsp + jq,这两者怎么可能一样呢?写 Vue 和写 jQuery 一样?NuxtJSP 一样?在他的眼中已经不关心写法了,反正都是 JS 写业务画页面。如果按照他这种说法那后端也别用 Spring 了,都是用 Java 写业务和接口,反正都不关心写法了已经。而且持这种观点的人不在少数:

甚至还在沸点里看到了这样的观点:

首先我们可以总结一下,评论区大概可以分为如下几类:

  • 前端倒退派:无虚拟 DOM + SSR 不就是 jQuery + JSP
  • 无意义派:前端性能已经很快了,再快几十毫秒也没什么太大意义
  • 抄袭派:这不就是在抄 Svelte 么(实际官方承认的是 Solid
  • 灌水派:6、学不动了、尤雨溪不懂 Vue、前端墙头草...等一系列没什么太大意义的评论
  • 认真思考派:

认真思考派其实也分为两种:一种是不理解虚拟 DOM 原本不是为了提升性能的吗?为何现在反而成为了性能瓶颈?大家都在想方设法的绕开虚拟 DOM

另一种则是思考如果没了虚拟 DOM 还怎么跨平台?本文主要解答的是跨平台的这样一个问题:

性能的问题会放到下篇文章去讲,不然的话文章太长会让人看不下去。相信我,这两个问题绝对会成为日后的面试题。有的面试官是自己也有这个疑问,想听听大家都是怎么回答的,如果你能够在众多候选人中解答了他的疑惑,那么相信你一定会给面试官留下一个更加深刻的印象。

还有的面试官是通过自己思考得出了答案,然后拿来作为筛选候选人的一道面试题。甚至更有可能出现的一种情况是面试官自己也觉得想要跨平台就必须依赖虚拟 DOM,如果你能反驳他,那势必会引起他的兴趣,然后你再把这里面的道道给他讲的明明白白的,相信一定会帮他打开新世界的大门。

我简单的搜索了一下这两个问题,发现市面上目前还没有系统讲解有关这两个问题的资料,所以我希望能够通过自己的见解来帮助大家答疑解惑。

无虚拟 DOM 与虚拟 DOM 共存

尤雨溪在美国的演讲中已经明确的说明了这两个模式是可以共存的,不仅可以在虚拟 DOM 组件中使用无虚拟 DOM 组件,反过来也照样没问题。这是一个非常棒的兼容,所以当我们想要跨平台的时候就用虚拟 DOM 呗!又不是说有了无虚拟 DOM 就会把虚拟 DOM 删掉。

但我个人认为,在尤雨溪维护了一段时间的共存模式后,他会想方设法的在下一个大版本中只保留无虚拟 DOM 模式。为什么这样说呢?相信大家都听过这样一句话:

做的越多错的就越多

现在的 Vue 仓库已经很庞大了,并且随着不断的推陈出新而变得越来越庞大,现在光组件就有两种写法:

  • Options API
  • Composition API

其实尤雨溪一直想把 Options API 给干掉,这样可以大大减少他的维护成本,不然为何无虚拟 DOM 模式只支持 Composition API?虽然他在演讲中的措辞是为了更好的性能,但他不想要 Options API 也是真的,当初要不是反对的人实在太多,他怕掉粉才不得不用 Composition API 去实现了一版 Options API

同理,他现在又要同时维护虚拟和无虚拟 DOM 这两套模式,关键是这俩模式还可以混着用,开发复杂度就不说了,毕竟他就是专门干这个的。那维护起来是不是也更加的困难?当这个功能正式上线之际可想而知 Vue 仓库会新增多少 issue

其实尤雨溪现在已经开始烦了腻了,最近看到他在国外的一个采访视频,他说自己现在特别希望能够出现一个类似于 Vite 那样的团队,因为现在的 Vite 基本上都是别人在帮他管,而他本人则更专注于 Vue。当然这个烦了腻了指的并不是他在 Vue 这边也想当个甩手掌柜坐享其成,他本人的表述是自己还是热衷于写代码和开发新功能的,只是每天有很多 PR,同时他这人又有点强迫症,忍不住去仔细审核每一行代码,以确保合并到 Vue 里的代码都是最棒的代码。这当然没毛病,但问题就在于每天产生的 PRissue 实在是太多了,有点太过于消耗他的精力了,每天处理这些都要花费大量的时间。

而且还有 Vue 周边一些其它的工具或库也在消耗着他的时间和精力,比方说 Vue 的开发工具(如 volarvue-tsc 等,当然这些很多也已经交给别人负责了)还有 vue-router 等一系列跟 Vue 相关的生态也让他感到很疲惫,他迫切的希望上天能够赐给他一位大牛来帮他处理上述的那堆脏活累活,而自己只需要专注 Vue Core 即可。

视频地址(需翻墙):www.youtube.com/watch?v=wea...

然后主持人就提到了 AntFu

他这个新头像看着有点雌雄难辨,而且居然还挂了个日本国旗,也不知道他是想表达在日本度假还是想表达什么?尤雨溪说他是很不错,但他的开源项目也太多了点,他自己每天处理那些的时间都不够用呢!而且他也很有创造力,他的那些项目很多都是自己的突发奇想,这么一名富有创造力的程序员用来帮他处理 Vue 的脏活累活有点可惜了,他值得更好的。(这话说的相当高情商,当然这话是经过了我的适度添油加醋,因为尤雨溪在话还没说完的情况下就被主持人打断问了下一个问题,尤雨溪也紧接着回答了下一个问题,所以后面的几句高情商是我自己加进去的,但我觉得他想表达的想法大致是这个意思)

根据尤总的说法,自己目前很想减轻工作量,把精力更加集中到一起。所以只要无虚拟 DOM 能够实现出虚拟 DOM 的跨平台,那么相信在下一个大版本中尤雨溪一定会想方设法的去掉这俩累赘(Options API 和虚拟 DOM)而且不仅仅只是去掉了两个累赘,毕竟要是没了虚拟 DOM 的话,Diff 算法也可以得到大幅度的精简。这无疑会大大降低尤雨溪的压力,如果我是尤雨溪的话我也会这么做,除非反对的意见太大。

不知大家看没看过俄罗斯拍的 AK47 电影,AK47 是一款非常皮实耐用的枪,即便是进了沙子也照样能够开枪。电影里有一个这样的桥段:AK47 的设计者拿着他的枪给上届枪械设计冠军看,冠军说一款枪的结构越简单就越不容易出故障,反而说那些设计的非常精密的枪械更容易坏,因为精密就意味着有大量的零部件,只要有一个部件损坏那就会影响整体。这个建议给了 AK47 的缔造者带来了很大的灵感,他开始精简零部件,并采取尽可能简洁的结构来完成这把枪,最终造就了这把世界名枪:

我认为这个设计理念同样也可以放到框架上,一款框架设计的越精简,就越不容易出 bug。当然也不是说那些设计的越精密的框架 bug 就越多,你看人家 AngularReact 照样稳得一批。但是越是小而美的框架就越好维护这个肯定是没错的,这也意味着更多的人能够读懂源码参与建设,也就更容易从中挑选出尤雨溪所迫切期待的"大牛"。所以只要 Vue 的无虚拟 DOM 模式取得了成功,那么相信尤雨溪也会适度的精简 Vue

我知道肯定会有人说其实也没精简多少啊,虽说在去掉虚拟 DOM 后的运行时能够得到大幅精简,源码也更加易读更好懂了,但编译做的也更复杂了呀!只不过是把复杂度从运行时转移到了编译期罢了,运行时源码易读的同时也会导致编译部分的源码比以往更难读懂。这么说确实是有一定的道理,但仔细思考一下尤雨溪口中的愿景,他虽然没在视频中直接说那些都是脏活累活,但毫无疑问的是他说的那些都是技术含量相对不高的没错吧?技术不高但却占据着他大量的时间,目前 Vue 的编译器和运行时都属于技术含量相对较高的情况,所以能够参与其中的开发者比例也并没有太高。

那假如说他能够把运行时的复杂度降下来,参与其中的开发者比例是不是就上来了,哪怕说由于编译器的复杂度提高导致没几个人能给 Vue 的编译器提代码,但给运行时提代码的会越来越多,精通运行时源码的人也不会像以前那么少了。那么此时他是不是只需要精心挑选一名代码写的好、时间充裕、有责任心又经常给 Vue 提代码的程序员来帮他维护运行时这边的事情,自己则只需专注编译器的开发就行了?

而且随着编译器复杂度的提升,能给编译器提 PR 的人也会越来越少,这无疑能够节省尤雨溪大量的时间(毕竟他有强迫症)同时如果在更复杂的情况下还能给编译器提代码的人,那绝对是大牛无疑了,能够筛掉一大批技术一般但还没事老想混 PR 的老 6

Solid.js

Solid 就是这样一款重编译轻运行的框架,而且它还是无虚拟 DOMVue 官方指定的"灵感源泉",所以我们先来看看它是如何跨平台的。

简介

为了防止有同学不了解 Solid,这里先做个简单的介绍,Solid 是近些年来异军突起的一个框架,能够把 JSX 编译为真实 DOM。正是由于没有虚拟 DOM,它的性能高居榜首,超越了其他所有框架:

看完图有人可能会说:你少在这忽悠我!这不排第二么?榜首是 Vanilla。实际上 Vanilla 是原生,用来作为评判标准的,Solid 是原生的 1.05 倍慢,已经十分接近原生了,遥遥领先于其他框架:

虽然它的语法和 React 有些类似,但实际上底层和 React 相去甚远,反而是它的响应式原理和 Vue 的响应式比较相似。这也是为何它能够获得尤雨溪的青睐的重要原因之一,它编译出来的那套代码稍微改改就能够套用在 Vue 的身上。而同为无虚拟 DOM 框架的 Svelte 原理则更类似于脏检查,编译出来的代码与 ReactVue 都不相似,所以别再说 Vue 抄袭 Svelte 了,人家抄袭 借鉴的是 Solid.js

DOM Expressions

Solid.js 的源码就属于编译器和运行时分离的那种,这样你去读它的源码时就不会受到编译代码的干扰了。它的编译器叫 DOM Expressions,在 DOM Expressions 中的 packages 文件夹下有个 babel-plugin-jsx-dom-expressions,点进去后进入 src 文件夹,可以看到有四个文件夹:

  • dom
  • shared
  • ssr
  • universal

dom 顾名思义就是编译为 DOM 操作、shared 是公共代码,类似于 vue 的那个 sharedssr 就是编译为 ssr 的那种形式、最后这个 universal 是重点,它能编译为跨平台。

接下来我们退回到 src 文件夹,在它下面有个 test 文件夹,顾名思义放的是测试代码。它有一个 dom 测试和跨平台测试:

我们展开文件夹就能发现,它们之间有很多相同的测试,比方说测试 attributefragments 等:

那么我们就同时点开 attribute 文件夹,看看它们两个分别会被编译成什么样:

code.js 是编译前的代码,output.js 是编译后的代码。我们先来看编译前代码,两种不同编译策略的前几个用例几乎一模一样,我们找一个比较好阅读的用例:

它分别会被编译为:

js 复制代码
// dom 版编译策略:
const _tmpl$5 = /*#__PURE__*/ _$template(`<div class="a b">`)
const template5 = _tmpl$5();
​
// universal 版编译策略:
const template5 = (() => {
  const _el$11 = _$createElement("div");
  _$setProp(_el$11, "class", "a");
  _$setProp(_el$11, "className", "b");
  return _el$11;
})();

因为 DOM 平台有 innerHTML 这个神器,所以会用字符串的形式去直接生成 DOM,而由于别的平台没有 innerHTML 这种神器,所以也不能够采用字符串,取而代之的是 createElementsetPropAPI,这些 API 的实现可以取决于你想要跨的平台。这不就和 Vue 现在的跨平台 APIcrateRenderer 差不多么:

Vue 编译

这是尤雨溪 PPT 里的代码:

在他的设想中上面的代码将会编译为:

既然 Solid 能够实现跨平台,那么 Vue 最终肯定也是没问题的,其实我蛮佩服尤雨溪的一点是:他吹过的牛 B 基本都能实现。

熟悉他的小伙伴们可能都知道,尤雨溪不仅仅只是一名优秀的开源框架作者,他同时还是一位出色的产品经理和营销大师。他经常会在某个新功能没开发完、甚至还没开始开发的时候就已经满世界宣传了,比如说这个无虚拟 DOM 模式,鸽了快一年了,一年前就开始宣传了,结果最近几个月才刚刚开始开发。

但他能这么做的底气就是他的提前宣传从未翻过车,从来没有说宣传的挺好的,结果到了最后却实现不出来的事情发生。尤其是那种竞品已经实现出来的功能,那就更不可能翻车了,所以我个人认为 Vue 的无虚拟 DOM 模式一定能够实现跨平台功能。

那么接下来我们就把 Vue PPT 中编译后的代码转为 Solid.js 跨平台风格的编译代码:

js 复制代码
// 改造后的跨平台代码:
import { ref, effect } from 'vue'
import { createElement, on, setProp, setText } from 'vue/vapor'
​
export default () => {
  const count = ref(0)
 
  const div = createElement('div')
  const button = createElement('button')
  let _button_class, _button_id, _button_text
  effect(() => {
    setProp(button, 'class', _button_class, (_button_class = { red: count.value % 2 }))
    setProp(button, 'id', _button_id, (_button_id = `foo-${count.value}`))
    setText(button, _button_text, count.value) // 个人认为这里有 bug 但 Vue PPT 中就是这么写的
  })
  on(button, 'click', () => count.value++)
  return div
}

比方说我们想渲染到 Three.js,那么 createElement 就可以写成:

js 复制代码
import {
  WebGLRenderer,
  BoxGeometry,
  MeshBasicMaterial,
  Mesh,
  Scene,
  PerspectiveCamera,
  AmbientLight,
  Group,
} from "three"

export function createElement(type, isSVG, isCustomizedBuiltIn, props) {
    props ||= {}
    const result = {
      scene () {
        if (scene) throw new Error('已经有 <scene> 了,一个 <scene> 就够了')
        return scene = new Scene()
      },
      camera () {
        if (camera) throw new Error('已经有 <camera> 了,一个 <camera> 就够了')
        camera = new PerspectiveCamera(
          props.fov || 75,
          props.aspect || innerWidth / innerHeight,
          props.near || 0.1,
          props.far || 1000
        )
        camera.position.set(props.position?.x || 50, props.position?.y || 50, props.position?.z || 100)
        camera.lookAt(props.lookAt?.x || 0, props.lookAt?.y || 0, props.lookAt?.z || 0)
        return camera
      },
      light: () => new AmbientLight(props.color, props.intensity),
      group: () => new Group(),
      box: () => new Mesh(
        new BoxGeometry(props.width, props.height, props.depth),
        new MeshBasicMaterial({ color: props.color })
      ),
      yingtianmen: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/yingtianmen.FBX' },
      guozijian: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/guozijian.FBX' },
      duanmen: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/duanmen.FBX' },
      liuyuxi: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/liuyuxi.FBX' },
      yunhe: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/yunhe.FBX' },
      siyuan: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/siyuan.FBX' },
      tianjinqiao: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/tianjinqiao.FBX' }
    }[type]

    if (result) return typeof result === 'object' ? result : result()
    else throw new Error(`不支持 <${type}> 元素`)
}

缺少的 API

之前的虚拟 DOM 编译是在最终的 createRenderer 中统一调用 API,而现在则分散到组件中调用 API 了:

所以现在缺少一种 API,比方说能直接获取到当前 AppgetCurrentApp

js 复制代码
// 改造后的跨平台代码:
import { ref, effect, getCurrentApp } from 'vue'
​
export default () => {
  const { renderer: r } = getCurrentApp()
  
  const count = ref(0)
 
  const div = r.createElement('div')
  const button = r.createElement('button')
  let _button_class, _button_id, _button_text
  effect(() => {
    r.setProp(button, 'class', _button_class, (_button_class = { red: count.value % 2 }))
    r.setProp(button, 'id', _button_id, (_button_id = `foo-${count.value}`))
    r.setText(button, _button_text, count.value) // 个人认为这里有 bug 但 Vue PPT 中就是这么写的
  })
  r.on(button, 'click', () => count.value++)
  return div
}

这样我们就可以跟以前写跨平台代码一样了:

js 复制代码
// 该代码来自 Vue 官网:
import { createRenderer } from '@vue/runtime-core'

const { createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement
  // ...
})

也就是说我们几乎完全无感知,跨平台的时候顶多就是在 config 里明确指定一下编译策略,剩下的我们写的代码和以前几乎一模一样,但编译策略却发生了翻天覆地的变化。

现场实现无虚拟 DOM 跨平台到 Three.js

在学生时代,同学们之间非常流行的一句话是:

语言的力量是苍白的

等到了工作的时候也有类似的流行语:

Talk is cheap, show me your code.

我光在这用嘴说 Vue 一定能实现无虚拟 DOM 的跨平台,即便我引用了各种案例也依然还是没有很强的说服力。那咱们就现场实现一款简易版的,如果说像我这种小卡了米都能实现出来,那就更别提尤大神了。

虚拟 DOM 对应的是什么?大家可能会说那当然对应的是无虚拟 DOM 咯:

  • 虚拟 DOMvirtual-dom
  • 无虚拟 DOMno-virtual-dom

其实换种方式来讲,虚拟 DOM 对应的不就是真实 DOM 么:

  • 虚拟 DOMvirtual-dom
  • 真实 DOMactual-dom

那咱们就来写一个叫 actual-dom 的库,库的实现就不放在这篇文章里讲了,太长!咱们直接用就行,先来到 GitHub

github.com/riozjs/actu...

把它克隆到本地:

git clone github.com/riozjs/actu...

注意尽量不要选择直接下载 ZIP 到本地,因为里面有 husky 还是啥来着依赖 git,这会导致你安装依赖的时候找不到 .git 而安装失败(Download ZIP 不会连着 .git 一起下载)但如果你非要这么下的话,记得先 git init 初始化一下以防安装失败。

这个项目是用 pnpm 做的 monorepo,所以需要大家提前安装一下 pnpm,已经安装过的就不用再安了,没安装过的话需要 npm i -g pnpmMac 系统别忘了在前面加 sudo

由于是用 pnpm 做的 monorepo,所以要用 pnpm 安装依赖:

css 复制代码
pnpm i

安装完成后运行 pnpm build,然后再 pnpm playground

打开 http://localhost:5173,映入眼帘的便是:

不知尤雨溪为何要用 5173 来作为默认端口号,因为我记得中学时有个游戏交易平台就叫 5173,也许那时候尤雨溪也打游戏,怀念以前的时光,所以 5173。扯远了,不是说好是 Actual DOM 吗?怎么映入眼帘的是 Virtual DOM?注意到下方还有个不太显眼的开关了么:

点击这个开关就会为我们显示 Actual DOM

这个老虎机滚动效果源自这篇:

《产品经理:能不能让这串数字滚动起来?》

但是这个按钮还没写文章讲过:

这个按钮也和老虎机效果一样,都是靠障眼法实现的,特别有趣,有时间会写篇文章详细讲下。

目前的 jsx 会被直接编译为真实 DOM,不信的话我们可以点开 playground 下的 src 文件夹,在 index.jsx 里打印一条 jsx 试试:

jsx 复制代码
console.log(<h1>actual dom 是下一个趋势</h1>)

控制台:

要知道那些虚拟 DOM 框架在打印 jsx 时可不会打印成这样,而是打印成下面这样:

所以它们不能用的写法,咱们能用:

jsx 复制代码
document.body.appendChild(<h1> Actual DOM 将会成为接下来的趋势 </h1>)

接下来我们来到 packages/actual-dom/src/index.ts 看一眼:

可以看到都是些非常好理解的 DOM API 封装,playground 文件夹里的那些 jsx 编译后就会调用 packages/actual-dom 文件夹里导出的方法。那我们只需要把这些方法体里原本是 DOM 操作的代码给改成 Three.js 的代码不就行了?说干咱就干,先把 packages/actual-dom/src/index.ts 里的代码全都删掉,然后再把下面这段代码复制进去:

ts 复制代码
import {
  WebGLRenderer,
  BoxGeometry,
  MeshBasicMaterial,
  Mesh,
  Scene,
  PerspectiveCamera,
  AmbientLight,
  Group,
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader';

let camera: PerspectiveCamera;
const scene = new Scene();
const loader = new FBXLoader();
const renderer = new WebGLRenderer({ antialias: true });
renderer.pixelRatio = devicePixelRatio;
(onresize = () => renderer.setSize(innerWidth, innerHeight))();

export const createText = () => {};

export const createElement = (
  type: string,
  props: Record<string, any>,
  children: (Element | FC)[],
) => {
  props ||= {};
  const result = {
    scene: () => renderer.domElement,
    camera() {
      if (camera) throw new Error('已经有 <camera> 了,一个 <camera> 就够了');
      camera = new PerspectiveCamera(
        props.fov || 75,
        props.aspect || innerWidth / innerHeight,
        props.near || 0.1,
        props.far || 1000,
      );
      camera.position.set(
        props.position?.x || 50,
        props.position?.y || 50,
        props.position?.z || 100,
      );
      camera.lookAt(props.lookAt?.x || 0, props.lookAt?.y || 0, props.lookAt?.z || 0);
      const controls = new OrbitControls(camera, renderer.domElement);
      controls.update();
      (onwheel = onmousemove = () => renderer.render(scene, camera))();
      return camera;
    },
    light: () => new AmbientLight(props.color, props.intensity),
    group: () => new Group(),
    box: () =>
      new Mesh(
        new BoxGeometry(props.width, props.height, props.depth),
        new MeshBasicMaterial({ color: props.color }),
      ),
    yingtianmen: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/yingtianmen.FBX' },
    guozijian: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/guozijian.FBX' },
    duanmen: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/duanmen.FBX' },
    liuyuxi: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/liuyuxi.FBX' },
    yunhe: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/yunhe.FBX' },
    siyuan: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/siyuan.FBX' },
    tianjinqiao: { model: 'https://ytmar.oss-cn-beijing.aliyuncs.com/fbx/tianjinqiao.FBX' },
  }[type];

  if (result) {
    const res = typeof result === 'object' ? result : result();
    children.forEach(child => appendChild(child, res));
    return res;
  } else throw new Error(`不支持 <${type}> 元素`);
};

export const createComment = () => {};

export const createFragment = () => {};

export const on = () => {};

export const off = () => {};

export const setProp = () => {};

export const setProps = () => {};

export const setAttr = () => {};

export const removeAttr = () => {};

export const setStyle = () => {};

export const setText = () => {};

export const getChild = () => {};

export const appendChild = (el: any, target: any) => {
  target instanceof Group
    ? target.add(el)
    : el.model
    ? loader.load(
        el.model,
        model => scene.add(model),
        event => console.log(`${el} 加载 ${(event.loaded / event.total) * 100} %`),
        () => alert('加载失败'),
      )
    : scene.add(el);
};

export type Prop = Record<string, any>;
export type FC = <P = Prop, C = Children>(props?: P, children?: C) => Element;
export type Children = (Element | FC)[];
export const createComponent = <P extends Prop = Prop, C extends Array<Element | FC> = Children>(
  tag: string | FC,
  props = {} as P,
  children = [] as unknown as C,
) => (typeof tag === 'function' ? tag(props, children) : createElement(tag, props, children));

再来到 playground/vite.config.ts,给 jsx 加个 { template: false }

之所以需要这个参数是因为 Actual DOM 会默认把静态的 jsx 编译为 template 方法,因为 web 平台有 innerHTML 这个神器,所以 template 内部利用了这个神器。但别的平台没有 innerHTML 啊,所以就需要告诉编译器不要编译为 template,取而代之的是 createElement 方法。然后来到 playground/src 文件夹,把除了 index.jsxindex.scss 以外的文件全删了:

接下来再把 index.jsx 的代码换成:

jsx 复制代码
import './index.scss';

document.body.appendChild(
  <scene>
    <light color="white" intensity="5" />
    <camera />
    <box width="10" height="10" depth="10" color="red" />
    <yingtianmen />
    <guozijian />
  </scene>,
);

我们先按 Ctrl + C 停掉服务,然后再 pnpm -C packages/actual-dom i three @types/three 安装一下 three.js,接下来 pnpm build 一下,最后再 pnpm playground 重启服务:

相信河南的朋友们看到这个模型一定会倍感亲切,没错,这就是河南洛阳应天门!这几个模型是清华大学美术学院交互媒体研究所送给我的,因为我帮助它们制作了智慧应天门的其中一个 H5 项目,中间过程还蛮有趣的,算是喂到嘴边的一个私活吧!有空的话给可以大家详细的讲一讲,在这里就不过多赘述了,不过代码里的 <yingtianmen><guozijian> 等元素正是我们在 customElement 里自定义的元素,这一看就是拼音。本来一开始想给自定义成 <应天门><国子监> 等,但奈何识别不了中文导致一直报错,咱也不知道应天门的英文翻译是啥,就先写成拼音吧!

虽然我还没去过河南,但从模型中也能感受到应天门的大气磅礴,有机会的话我一定要亲自去一趟,体验一下应天门里的整套流程,反正这个应天门我是越看越喜欢,霸气侧漏:

怎么样?这回相信了吧?其实无虚拟 DOM 想要跨平台并不难,连我这种小卡了米都能实现出来,你们还会觉得 Vue Vapor Mode 无法跨平台么?

往期精彩文章

相关推荐
leoZ23113 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
芭拉拉小魔仙14 小时前
Vue 2 门诊收费系统中的医保结算流程设计与实践
前端·javascript·vue.js
EatFan15 小时前
HTML前端利用flex解决input定位的问题
前端·css·html·css3
JunjunZ18 小时前
Naive UI 虚拟级联选择器适配 Element Plus 风格
前端·javascript·vue.js
光影少年19 小时前
如何实现RN 多环境、多渠道打包
前端·react native·react.js
闲坐含香咀翠19 小时前
百万行数据透视表,我是怎么把 Vue 响应式开销砍到零的
前端·vue.js·性能优化
labixiong19 小时前
告别scroll 监听:CSS 滚动驱动动画,主线程堵死也仍跟手
前端·css·html
许彰午20 小时前
34-安全复盘96个问题
前端·vue.js·安全
志尊宝21 小时前
Vue3 零基础每日笔记(009):watch 侦听器——数据一变就“做事“
vue.js·笔记·缓存
一夜枫林21 小时前
vue实现scroll-view上下滑动右侧左右滑动
前端·javascript·vue.js