PocketJS:没有 DOM 的组件运行时

把 Solid、Vue Vapor 与 Octane 组件编译成同一棵原生树,在没有 DOM、CSS 引擎和 WebView 的设备上直接绘制像素。

本文目录

PocketJS 是我最近看到的最“反常规”的前端项目之一:它让现代组件代码运行在没有 DOM、没有 CSS 引擎、没有 WebView 的环境里。

不是把浏览器塞进设备,而是把组件编译成一棵原生树,由 Rust 核心做 flexbox 布局并绘制像素。

三种框架,一个运行时

PocketJS 支持 Solid、Vue Vapor 和 Octane。三者最终编译到同一棵原生树,运行在同一个 QuickJS guest 上。

框架状态与生命周期源码形式
Solidsolid-jsJSX
Vue VaporvueJSX 与 <script setup> SFC
Octaneoctane编译期 hooks 与 JSX

框架只影响应用代码,不影响下面的运行时、输入、动画和原生边界。

从组件到像素

一个 Solid 计数器在 PocketJS 里大概是这样:

import { createSignal, Show } from "solid-js";
import { mount } from "@pocketjs/framework/solid";
import { Text, View } from "@pocketjs/framework/solid/components";

function Counter() {
  const [count, setCount] = createSignal(0);

  return (
    <View class="w-full h-full flex-col items-center gap-4 p-4 bg-slate-50">
      <Text class="text-xl text-slate-950 font-bold">Count: {count()}</Text>
      <View
        class="px-4 py-2 rounded-xl bg-blue-600"
        focusable
        onPress={() => setCount(count() + 1)}
      >
        <Text class="text-base text-white font-bold">Press Circle</Text>
      </View>
      <Show when={count() > 3}>
        <Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
      </Show>
    </View>
  );
}

mount(() => <Counter />);

这段代码没有 DOM 节点,也没有 CSS 解析。class 字面量在构建期被编译成样式表,运行时只做查表。

样式与动画

PocketJS 接受一个固定的 Tailwind 子集。构建时把 class 编译成 baked style table,运行时没有 CSS parser、cascade、specificity 或 reflow。

动画也是同一套思路:关键帧时间线和弹簧曲线被烘焙进样式表,由 Rust 核心按自己的时钟推进。也就是说,一屏动画可以在没有逐帧 JavaScript 的情况下运行。

目标准入

一个 bundle 可以服务不同密度的设备。应用在 pocket.json 里声明视口和所需 API,目标 profile 必须满足声明才能编译和打包。

pocket check --target psp
pocket check --target vita

同一个 bundle,不需要为不同设备改组件。

为什么值得关注

PocketJS 把前端开发者熟悉的组件模型,带到了资源受限、没有浏览器的手持设备上。它没有试图模拟 Web,而是重新定义了“组件到像素”的路径。

这条路当然有边界:固定样式子集、目标设备能力、QuickJS/Rust 的调试成本。但它至少证明了一件事——组件化不一定等于 DOM。

讨论

评论由 GitHub Discussions 提供,点击后加载。未加载前不会产生第三方请求。