moonbridge

    The 'wasm owns state, JS is a thin shell' architecture as a reusable kit: a generic Session with an init/dispatch JSON protocol (state + effects envelope), plus a dependency-free browser runtime. The MoonBit core is the only decision point; the DOM shell only renders and executes effects.

    wasm
    bridge
    frontend
    elm-architecture
    json
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    5

    #moonbridge · wasm-gc 前端桥协议工具包

    CI License

    wasm 拥有全部状态,JS 只做渲染壳」的 MoonBit Web 前端架构,抽成一个可复用的库:一个泛型 Session 承担 init/dispatch JSON 协议的全部机制,一个零依赖的浏览器运行时完成加载与事件循环。你的应用只写业务归约函数。

    #它解决什么问题

    用 MoonBit 写浏览器应用(wasm-gc 后端)时,架构问题比 UI 问题先来:状态放哪、事件怎么进 wasm、副作用(跳转/存储/提示)怎么出来、渲染怎么分区。mooncakes 上已有 vdom 路线的框架(react/respo 等),但**「wasm 业务核心 + 薄 DOM 壳」**这条路线此前没有可复用的实现——每个项目都要手写一遍桥协议。

    moonbridge 把这条路线的全部协议机制收敛为两个件:

    • MoonBit 侧 Session[M, E, F]:持有状态、解码事件、调用你的 update、组装响应信封(含两条错误路径)
    • JS 侧 moonbridge.mjs(~120 行、零依赖):加载 wasm(js-string builtins)、boot、dispatch 循环、副作用处理器注册表

    参考应用:mtab(浏览器起始页,全部业务逻辑在 wasm)。配套测试工具链:moonwebtest

    #架构

    ┌─────────────────────────── 浏览器 ────────────────────────────┐ │ 你的 HTML/CSS moonbridge.mjs(运行时,~120 行) │ │ ▲ onState(state) │ 事件 JSON / 响应 JSON │ │ │ ▼ │ │ 用户交互 ──────▶ wasm.dispatch ──────▶ Session[M,E,F] │ │ (你的 update 归约函数) │ │ localStorage / 跳转 / │ effects │ │ toast ◀── effects 处理器注册表 ◀──┘ (由你的壳执行) │ └──────────────────────────────────────────────────────────────┘

    wasm 是唯一的决策点:Session 把每个事件归约为「新状态 + 副作用列表」;JS 壳只收集事件、重绘、执行副作用。与 vdom 框架的差异:没有虚拟 DOM、没有 diff、没有组件树——渲染分区由你的壳决定(比如输入框永不重绘、只有列表分区重绘),代价是壳层要手写 DOM,换来的是极薄的 JS 层与完全可测试的 JSON 协议边界。

    #快速开始

    moon add 2d5rrr333/moonbridge

    1. MoonBit 侧:定义模型与归约,组装 Session(完整可运行示例见 src/demo/

    // 你的状态 / 事件 / 副作用(应用自己的类型)
    pub(all) struct State { count : Int } derive(ToJson)

    pub enum Event { Inc; Dec } derive(Eq, Debug)

    pub enum Effect {
    Saved
    Notified(message~ : String)
    } derive(Eq, Debug)

    // FromJson / ToJson 手写实现(wire format 完全由你定义)...

    // 归约函数:纯函数,(state, event) -> (next state, effects)
    fn update(state : State, ev : Event) -> (State, Array[Effect]) { ... }

    // 组装 Session,然后导出两个 wasm 函数
    let session : Session[State, Event, Effect] = Session::make(update)

    #export_name("app_init")
    pub fn app_init(stored : String) -> String {
    // ...从 stored 恢复或取默认状态...
    session.init(initial_state, []) // 存入状态并返回信封 JSON
    }

    #export_name("app_dispatch")
    pub fn app_dispatch(event : String) -> String {
    session.dispatch(event) // 协议机制全部在这里
    }

    2. JS 侧:boot 并注册副作用处理器

    <script type="module"> import { bootBridge } from './moonbridge.mjs'; // 从本仓库 web/ 复制 const app = await bootBridge({ wasmUrl: 'wasm/app.wasm', init: 'app_init', // 你的导出名 dispatch: 'app_dispatch', initArgs: () => [localStorage.getItem('app.state') || ''], onState: render, // (state) => void,按分区重绘 effects: { save: (_eff, ctx) => localStorage.setItem('app.state', JSON.stringify(ctx.state)), notify_error: eff => showToast(eff.message), }, }); </script>

    #线协议(wire format)

    每次 init / dispatch 调用返回同一形状的信封:

    { "state": <你的状态 JSON,或 null>, "effects": [ { "type": "...", ... } ] }

    场景stateeffects
    正常 init/dispatch新状态你的 update 产出
    dispatch 先于 init(未初始化)null[{type: "notify_error", message: <文案>}]
    事件解码失败(非 JSON / 形状不对 / 未知 type)当前状态(不前滚)[{type: "notify_error", message: <文案>}]

    错误文案默认中文("未初始化"/"无效的事件"),通过 Session::make(update, texts=...) 自定义。notify_error 是协议内建的唯一 effect 类型;其余 effect 由你的应用定义、由你的壳处理,未注册的类型仅 console.warn(壳对核心侧新增 effect 保持前向兼容)。

    注意:MoonBit 的 try/catch 只捕获 raise 错误——事件解码错误会被转换为错误信封,但 abort/panic 会像任何手写 wasm 桥一样直接 trap 实例,不会变成信封。

    #测试

    • 协议单测:moon test(信封序列化 / Session 全路径 / 错误文案)
    • 运行时冒烟:moon build --target wasm-gc --release && node scripts/smoke.mjs(用 src/demo 计数器应用驱动真实 wasm 实例)
    • Node/浏览器 e2e 与无头断言:使用 moonwebtest

    #License

    Apache-2.0