在之前的一篇博客文章中,我们探索了如何在 MoonBit 的 Wasm GC 后端中直接使用 JavaScript 字符串。正如文中所描述的那样,我们可以用 MoonBit 编写一个兼容 JavaScript 的字符串操作 API,并编译生成体积极小的 Wasm 产物。

然而,您可能会好奇这一功能在真实的开发场景中表现如何。因此,今天我们将展示一个更贴近实际的案例:借助 MoonBit 库 [Cmark] 和 Wasm 的 JS String
Builtins 提案,在一个由 JavaScript 驱动的 Web 应用程序中渲染 Markdown 文档。

背景

[Cmark] 是一个用于处理 Markdown 文档的新 MoonBit 库,其可以解析原生 CommonMark 和各种常见的 Markdown 语法扩展(如任务列表、脚注、表格等)。此外,它从早期开始就支持外部渲染器,并附带了一个名为 cmark_html 的官方 HTML 渲染器实现。

由于 Markdown 在线上尤其是 Web 中的广泛使用,将 Markdown 到 HTML 的转换 API 是几乎每个 JavaScript 开发者都会使用到的重要工具。因此,这对于展示 MoonBit Wasm GC API 在前端 JavaScript 中的使用也是一个理想的场景。

封装 Cmark

为了进行这个演示,我们先创建一个新的项目目录:

> mkdir cmark-frontend-example

在该目录中,首先创建一个 MoonBit 库 cmarkwrap 来封装 [Cmark]:

> cd cmark-frontend-example && moon new cmarkwrap

这个额外的项目 cmarkwrap 的主要作用是:

  • [Cmark] 本身不通过 FFI 边界暴露任何 API,这对大多数 MoonBit 库来说是常见情况;
  • 我们需要从 [mooncakes.io] 仓库中获取 [Cmark] 项目,并将其本地编译为 Wasm GC。

cmarkwrap 的结构非常简单:

  • cmark-frontend-example/cmarkwrap/src/lib/moon.pkg.json:

    {
      "import": ["rami3l/cmark/cmark_html"],
      "link": {
        "wasm-gc": {
          "exports": ["render", "result_unwrap", "result_is_ok"],
          "use-js-builtin-string": true
        }
      }
    }

    这个配置基本与我们之前的博客中介绍的设置相同,为 Wasm GC 目标启用了 use-js-builtin-string 功能标志,并导出了相关的封装函数。

  • cmark-frontend-example/cmarkwrap/src/lib/wrap.mbt:

    ///|
    typealias RenderResult = Result[String, Error]
    
    ///|
    pub fn render(md : String) -> RenderResult {
      @cmark_html.render?(md)
    }
    
    ///|
    pub fn result_unwrap(res : RenderResult) -> String {
      match res {
        Ok(s) => s
        Err(_) => ""
      }
    }
    
    ///|
    pub fn result_is_ok(res : RenderResult) -> Bool {
      res.is_ok()
    }

    这里是该演示的关键部分。render() 函数封装了底层的 @cmark_html.render() 函数,但前者不像后者那样直接抛出异常,而是返回一个 RenderResult 类型。

    然而,由于 RenderResult 是一个 Wasm 对象(而不是数字或字符串),其对 JavaScript 来说是不透明的,因此无法直接被 JavaScript 调用者使用。于是,我们还需要在 MoonBit 中提供拆解该 RenderResult 类型的方法:正是出于这一目的,我们提供了 result_unwrap()result_is_ok(),它们接受这一类型的输入。

与 JavaScript 集成

现在是编写项目 Web 部分的时候了。在此阶段,您可以选择您喜欢的任何框架或打包工具。就本次演示而言,我们选择了在 cmark-frontend-example 目录下建立一个最小的项目结构,无需额外的运行时依赖。以下是项目的 HTML 和 JS 部分:

  • cmark-frontend-example/index.html:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Cmark.mbt + JS</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="module" src="/src/main.js"></script>
        <link rel="stylesheet" href="/src/style.css" />
      </body>
    </html>

    这个简单的 HTML 文件包含一个 id="app"div,稍后会用作渲染 Markdown 文档的目标。

  • cmark-frontend-example/src/main.js:

    const cmarkwrapWASM = await WebAssembly.instantiateStreaming(
      fetch("../cmarkwrap/target/wasm-gc/release/build/lib/lib.wasm"),
      {},
      {
        builtins: ["js-string"],
        importedStringConstants: "_",
      },
    );
    const { render, result_is_ok, result_unwrap } =
      cmarkwrapWASM.instance.exports;
    
    function cmarkWASM(md) {
      const res = render(md);
      if (!result_is_ok(res)) {
        throw new Error("cmarkWASM failed to render");
      }
      return result_unwrap(res);
    }
    
    async function docHTML() {
      const doc = await fetch("../public/tour.md");
      const docText = await doc.text();
      return cmarkWASM(docText);
    }
    
    document.getElementById("app").innerHTML = await docHTML();

    cmarkwrap 集成到 JavaScript 中相对简单。在 fetch 并加载 Wasm 产物后,可以直接调用封装函数。result_is_ok() 帮助我们判断是否在正常路径上:如果是,我们通过 result_unwrap() 解包跨 FFI 边界的 HTML 结果,否则抛出一个 JavaScript 错误。如果一切顺利,我们最终将渲染结果填充到 <div id="app"></div> 中。

现在我们可以编译 MoonBit 的 Wasm GC 产物并启动开发服务器:

> moon -C cmarkwrap build --release --target=wasm-gc
> python3 -m http.server

大功告成!我们现在可以用浏览器打开 http://localhost:8000 来访问我们的 JavaScript 前端应用,并查看使用 [Cmark] MoonBit 库渲染出的 A Tour of MoonBit for Beginners 文档了。

您可以在 GitHub 上找到该演示的代码。


Moonbit
1 声望3 粉丝

IDEA基础软件中心打造的下一代智能开发平台