日本拥有50K+粉丝开发者_ mizchi 长期活跃于前端与工具链领域,因对 TypeScript、WebAssembly 以及现代 Web 工程实践的深入研究而受到关注,同时也是 MoonBit 日本社区中较为活跃的开发者之一。
在本文中,作者结合自身在_ React、Preact、Qwik、Svelte 等框架中的实践经验,介绍了他为什么选择从零实现一个 UI库 ------Luna UI,并重点讨论了轻量运行时、Signal 响应式系统,以及以 Web Components、SSR 与 Hydration 为前提的整体设计思路。
以下为原文的中文翻译。
作为一名 UI 库爱好者,我从 React 开始尝试过各种 UI 库,但最终决定自己写一个。
多年过去,我对现有库的不满依然无法解决或难以解决,所以现在我要做我真正想要的东西。
-
轻量级运行时带来的便携性
-
基于 Signal 的细粒度响应式
-
足够轻量,无需编译时优化
-
支持 WebComponents SSR + Hydration (大概是世界首创)
这就是我做的 Luna。我还制作了文档网站。

它是用 Moonbit+Luna 本身编写的,甚至 SSG 也是自制的。
https://luna.mizchi.workers.dev/
对现有 UI 库的不满
-
React: 体积大。由于为了兼容现有资产,运行缓慢。不喜欢 RSC 实现的改进方向。
-
Qwik/Solid: 编译时展开很碍事。
-
svelte/vue: SFC 难以集成到生态系统中。
-
preact: 我认为它是思路最正确的,但 signal 是后来补上的。生态系统一般。
-
共同问题: 互操作性低。除 Qwik 外,SSR 速度令人不满。没有以 WebComponents 为核心的项目。
基于这些考虑,我参考了 preact/signal,并以 WebComponents SSR + Hydration 为前提进行了设计。
根据我使用 Qwik + preact + svelte 的经验,Qwik 的优化过于复杂。如果核心足够轻量,preact 风格的轻量核心就足够了。
我也考虑过扩展 preact,但考虑到下文提到的 SSR Hydration 集成以及 Native SSR 的前景,SSR 优化需要垂直整合,因此我决定自研。
产出
🌕 Luna UI - 一个用 MoonBit 编写的基于 Signal 的声明式 UI 库。可用于 Moonbit/JS。
在下文提到的示例代码中,我对比了 luna 和 preact 的相同实现,preact 为 20kb,而 luna 仅为 6.7kb。实际上这取决于 treeshake 使用了哪些功能,但就结果而言可以说是大获成功。
示例代码: tsx
由于我用 jsx-runtime 包装了 Moonbit JS 后端的产物,因此在 js 构建中可以直接使用 JSX。
Bash
$ npm add @luna_ui/luna
安装后即可使用:
javascript
import { createSignal, createMemo, render, For, Show } from '@luna_ui/luna';
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
const isEven = createMemo(() => count() % 2 === 0);
return (
<div>
<h1>Luna Counter Example</h1>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>{() => isEven() ? 'Even' : 'Odd'}</p>
<div class="buttons">
<button onClick={() => setCount(c => c - 1)}>-</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
</div>
);
}
//...
const app = document.getElementById('app');
if (app) {
render(app, <App />);
}
以 tsx 为前提的示例项目
https://github.com/mizchi/luna-tsx-example
示例代码: Moonbit
这是 Moonbit 版本。
rust
fn main {
let doc = @js_dom.document()
guard doc.getElementById("app") is Some(el)
let count = @signal.signal(0)
let app = @dom.div([
p([@dom.text_dyn(fn() { "Count: " + count.get().to_string() })]),
button(
on=@events().click(fn(_) { count.update(fn(n) { n + 1 }) }),
[text("Click me")],
),
])
@dom.render(el |> @dom.DomElement::from_jsdom, app)
}
我感觉连接 vite 和 moonbit 这两个构建过程很复杂,所以自制了 vite-plugin-moonbit 来进行整合。
https://github.com/mizchi/vite-plugin-moonbit
引入后可以实现这样的效果:
JavaScript
import { greet } from 'mbt:username/app';
moon build --target js 会生成 .d.ts 文件。在 vite 端,可以从 Moonbit 的命名空间解析符号,并带有完整的类型支持。很便利吧?
我还集成了错误报告器,将 moon build --watch 的结果作为 vite 错误显示。如果没有这个,类型错误导致不输出时会很麻烦。(一共有选项可以手动设置)
Moonbit 目前还没有 JSX,所以采用函数式 DSL 风格编写。
之所以说"目前还没有",是因为相关的提案正在实现中:
https://github.com/moonbitlang/moonbit-evolution/issues/19
有了这个后会变得非常自然。虽然目前是基于函数的 DSL,但我认为写起来感觉也不错。
实际运行演示
我想通过实际运行的 Demo 来展示它的强大。
Demo: 射击游戏
这是为了基准测试而制作的游戏。

这没有使用 HTML Canvas,而是生成了 100x100 的 DOM 节点,并在每一帧进行实时重绘。
在 DevTools 中测试发现,JS 负载极低,且维持在 60FPS。在手机上尝试也非常丝滑。

用 React 尝试制作同等效果时,只能跑出 12FPS 左右。
Bundle 体积也保持在 6.4kb 的轻量水平。
源代码在这里 https://github.com/mizchi/luna.mbt/tree/main/src/examples/game
Demo: TodoMVC
虽然最近见得少了,但我还是制作了作为框架基准测试的 TodoMVC。

https://luna.mizchi.workers.dev/demo/todomvc/
我认为这是展示现实应用场景的一个很好的例子。
源代码
https://github.com/mizchi/luna.mbt/tree/main/src/examples/todomvc
Demo: Sol Framework
我正在开发一个相当于 Next.js 的框架,名为 Sol。Luna 和 Sol,即月亮和太阳。
这目前还是一个概念验证(PoC),但部署在 Cloudflare Worker 上的 Demo 已经可以运行了。
-
首页显示从服务器生成的随机数。也就是说这是动态 SSR。
-
使用 Declarative Shadow DOM 进行 SSR 并完成了 Hydration。
据我所知,目前还没有其他框架能实现这一点。
考虑到最近 React 的安全性问题,我正考虑并实现更安全的设计。等可以使用后,我会另写文章介绍。
Astra SSG: 专门为文档制作的 SSG
我一直觉得,如果框架或 UI 库的文档是用其他工具写的,那就太逊了。
所以,我用 Luna 自制了一个静态站点生成器。
名字叫 astra。和 astro 很像?结合下文提到的 sol 框架,luna, sol, astra 都是拉丁语中与天体相关的术语...
文档: https://luna.mizchi.workers.dev/
这个文档网站就是用基于 luna 的 SSG 构建的。框架参考了 docusaurus,并添加了我想要的功能,比如通过 00_ 这样的前缀自动对文档进行排序。
bash
$ npx @luna_ui/astra new mydocs
$ cd mydocs
$ npx @luna_ui/astra dev
$ npx @luna_ui/astra build # 生成 dist-docs/*
不过目前还是有很多硬编码的实现,可配置项较少。这是未来的课题。
实际上前几天制作的 markdown 编译器就是为了这个而做的。
https://github.com/mizchi/markdown.mbt
为什么选择 MoonBit 实现
这说来话长,自古以来就存在"双重模板问题",即客户端和服务器必须编写相同的模板,并在客户端继承逻辑。
Next.js 是第一个在现实中成功解决这一问题的框架。SSR 不仅仅是在服务器生成 HTML,还必须通过在客户端注入 JS 活跃部分来实现。
根据我使用 Next.js 和 Qwik 的经验,如果要进行 Hydration 和 SSR 时的优化,就必须实现客户端运行时和服务器 SSR 的垂直整合。这是一项非常艰巨的任务,在 Node.js 和浏览器中是通过同一种语言保证幂等性来实现的。除 Node.js 之外几乎没有实现 SSR + Hydration 的原因就在于此。
但是,对于可以跨编译到多种目标的 Moonbit 来说,即使是不同的后端,是否也能实现这一点呢?
MoonBit 可以编译到多个目标:
Plain
MoonBit → JavaScript (浏览器)
→ Native (SSR 服务器)
→ Wasm-GC (WasmEdge)
Moonbit 的优势在于其表达能力足以支撑像 Hydration 这样复杂的设计,且 JS 后端生成的代码非常轻量,几乎等同于原生 JS。而且 wasm-gc 后端也很轻量,还有高速的 native 后端。在我的测试中,Native 构建的速度比 JS 快了约 5 倍。
不过,目前执行 SSR 的 Sol Framework 中,服务器大部分是作为 Hono 的绑定实现的,因此只能在 JS 上运行。目前的目标是先在 Cloudflare Workers 的 JS 后端上运行。
欢迎试用
我想我已经展示了 Moonbit 的可能性。
虽然 Astra 和 Sol 的完成度还不算高,但 Luna 核心部分经过了大量测试,应该是相当好用的。
接下来我想挑战以下主题:
-
完善 WebComponents 和 loader 的示例
-
制作实时预览编辑器
-
制作相当于 radix-ui 的 headless UI 框架
-
在 Cloudflare Worker 上制作 WebComponents Registry
-
实现 Native Server 的 SSR
-
支持 Wasm,使其在 wasmedge 上运行
欢迎试用并提供建议。