跳到主要内容

3 篇博文 含有标签「JavaScript」

查看所有标签

如何在你的网站中使用 Moonpad

· 阅读需 16 分钟

cover.png

MoonBit官网语言导览中都有一个组件可以在浏览器中直接编写 MoonBit 代码并实时编译运行。它就是我们开发的 Moonpad 组件,目前已经发布到 npm 上,这篇博客将介绍如何在你的网站中使用 Moonpad。

这篇博客中出现的所有代码已都上传到 github,你可以在 https://github.com/moonbit-community/moonpad-blog-examples 中看到。

什么是 Moonpad

MoonBit 插件目前已具备多种为开发者带来极大便利的功能。除了针对 MoonBit 语法的高亮显示、自动补全,以及错误提示等功能的支持外,还实现了开箱即用的调试器、实时值追踪、测试以及内置的MoonBit AI助手,能够非常有效地减少除核心开发以外的工作量,提供一个高效流畅的开发环境。

在本篇教程中介绍的 Moonpad 是一个基于 monaco editor 的在线 MoonBit 编辑器,支持 MoonBit 语法高亮、自动补全、错误提示等功能。除此之外还支持在浏览器中实时编译 MoonBit 代码,可以说是一个在浏览器上的简单版 MoonBit 插件。它在 MoonBit 的官网和语言导览已经有所使用。如果你想体验完整版的 MoonBit 开发功能可以安装我们的 VS Code 插件

如何使用 Moonpad

准备

新建一个 JS 项目:

mkdir moonpad
cd moonpad
npm init -y

安装依赖:

npm i @moonbit/moonpad-monaco esbuild monaco-editor-core

每个依赖的作用如下:

  • @moonbit/moonpad-monaco 是一个 monaco editor 的插件,提供 MoonBit 语法高亮、自动补全、错误提示、编译源代码等功能。
  • esbuild 是一个快速的 JavaScript 打包工具,用于打包源码。
  • monaco-editor-core 是 monaco editor 的核心库。选择这个库而不是monaco-editor的原因是,这个库没有 monaco editor 自带的 MoonBit 用不上的各种其他语言的语法高亮和 html, css, js 等语言的语义支持,打包出来的体积更小。

编写代码

接下来我们需要编写使用 moomnpad 和 monaco editor 的代码以及构建脚本。

以下展示的代码都是在 moonpad 目录下编写的。并且带有大量注释,以便你更好的理解。

创建 index.js 文件并输入以下代码:

import * as moonbitMode from "@moonbit/moonpad-monaco";
import * as monaco from "monaco-editor-core";

// monaco editor 要求的全局变量,具体可以查看其文档:https://github.com/microsoft/monaco-editor
self.MonacoEnvironment = {
getWorkerUrl: function () {
return "/moonpad/editor.worker.js";
},
};

// moonbitMode 是一个 monaco-editor 的扩展,也就是我们提到的 Moonpad。
// moonbitMode.init 会初始化 MoonBit 的各种功能,并且会返回一个简单的 MoonBit 构建系统,用于编译/运行 MoonBit 代码。
const moon = moonbitMode.init({
// 一个可以请求到 onig.wasm 的 URL 字符串
// onig.wasm 是 oniguruma 的 wasm 版本。oniguruma 是一个正则表达式引擎,在这里用于支持 MoonBit 的 textmate 语法高亮。
onigWasmUrl: new URL("./onig.wasm", import.meta.url).toString(),
// 一个运行 MoonBit LSP 服务器的 Worker,用于给 MoonBit 语言提供LSP服务。
lspWorker: new Worker("/moonpad/lsp-server.js"),
// 一个工厂函数,返回一个运行 MoonBit 编译器的 Worker,用于在浏览器中直接编译 MoonBit 代码。
mooncWorkerFactory: () => new Worker("/moonpad/moonc-worker.js"),
// 一个配置函数,用于配置哪些codeLens需要被显示。这里我们出于简单的考虑,直接返回false,不显示任何codeLens。
codeLensFilter() {
return false;
},
});
// 值得一提的是,这里所有的路径都是硬编码的,这意味着后面在编写构建脚本时,我们需要确保这些路径都是正确的。

// 挂载 Moonpad

// 创建一个 editor model,并且指定其 languageId 为 "moonbit",在这里我们可以初始化代码内容,
// moonpad 只对 languageId 为 "moonbit" 的 model 提供 LSP 服务。
const model = monaco.editor.createModel(
`fn main {
println("hello")
}
`,
"moonbit",
);

// 创建一个 monaco editor,展示我们之前创建的 model,并将其挂载到 `app` div 元素上。
monaco.editor.create(document.getElementById("app"), {
model,
// 这个主题是 moonpad 提供的主题,相比于 monaco 自带的主题,语法高亮效果更好。
// 此外 moonpad 还提供了一个暗色主题 "dark-plus",你可以尝试将其替换为 "dark-plus"。
theme: "light-plus",
});

创建 esbuild.js 并输入以下代码,这是我们的构建脚本,用于打包 index.js

const esbuild = require("esbuild");
const fs = require("fs");

// dist 是我们的输出目录,这里把它清空,确保 esbuild 总是从头开始构建
fs.rmSync("./dist", { recursive: true, force: true });

esbuild.buildSync({
entryPoints: [
"./index.js",
// 打包 monaco editor 用于提供编辑服务的 worker,这也是 `index.js` 中 MonacoEnvironment.getWorkerUrl 的返回值。
// 之前提到所有的路径都是硬编码的,所以下面会使用 `entryNames` 确保这个 worker 打包之后的名字是 `editor.worker.js`。
"./node_modules/monaco-editor-core/esm/vs/editor/editor.worker.js",
],
bundle: true,
minify: true,
format: "esm",
// 输出目录对应我们在 `index.js` 中硬编码的路径。
outdir: "./dist/moonpad",
entryNames: "[name]",
loader: {
".ttf": "file",
".woff2": "file",
},
});

fs.copyFileSync("./index.html", "./dist/index.html");

// 复制 `index.js` 中初始化 moonpad 所需要的各种 worker 文件。
// 由于它们已经被打包过了,所以不需要用 esbuild 再次打包。
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/lsp-server.js",
"./dist/moonpad/lsp-server.js",
);
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/moonc-worker.js",
"./dist/moonpad/moonc-worker.js",
);
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/onig.wasm",
"./dist/moonpad/onig.wasm",
);

最后是 index.html 文件,非常简单,没什么值得注意的。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="/moonpad/index.css" />
</head>
<body>
<div id="app" style="height: 500px"></div>
<script type="module" src="/moonpad/index.js"></script>
</body>
</html>

构建并启动服务器

运行构建脚本

node esbuild.js

此时 dist 文件夹应包含以下文件:

dist
├── index.html
└── moonpad
├── codicon-37A3DWZT.ttf
├── editor.worker.js
├── index.css
├── index.js
├── lsp-server.js
├── moonc-worker.js
└── onig.wasm

2 directories, 8 files

此处展示的 codicon-37A3DWZT.ttf 文件后面的哈希值不一定与你实际操作下来的相同,但是无伤大雅。

在 dist 文件夹下启动一个 http 服务器:

python3 -m http.server 8081 -d ./dist

打开 locahost:8081,可以看到 moonpad 已经成功渲染出来了。

moonpad

如何在你的网站中使用 Moonpad

接下来我们以 jekyllmarp 为例,展示如何在你的网站中使用 Moonpad。

原理

在上一节中我们最终得到了一个 moonpad 文件夹,其中包含使用 Moonpad 需要的所有文件。任何网页只要修改一下 index.js 最后硬编码的挂载逻辑然后导入 moonpad/index.jsmoonpad/index.css,就可以直接使用 Moonpad。

所以,想要在网页中使用 Moonpad,我们需要做的是:

  1. 根据不同的网页框架修改 moonpad/index.js 中的挂载逻辑。
  2. 在需要使用 Moonpad 的页面中导入 moonpad/index.jsmoonpad/index.css
  3. moonpad 文件夹放到网站的静态资源目录下。确保它的路径是 /moonpad

在 jekyll 中使用 Moonpad

Jekyll 是一个简单的、博客感知的静态网站生成器。它使用 Markdown 或 Textile 以及 Liquid 模板引擎来生成静态网页。Jekyll 通过将内容与模板结合,生成可以直接部署到任何 Web 服务器上的静态文件。它特别适合用于 GitHub Pages,允许用户轻松地创建和维护博客或网站。

观察 jekyll 渲染代码块的结构

在 jekyll 中对于这样的 markdown 代码块:

```moonbit
fn main {
println("hello, moonbit")
}
```

会生成如下的 html 结构:

<pre><code class="language-moonbit">fn main {
println("hello, moonbit")
}
</code></pre>

如果我们希望 jekyllrb 中所有的 moonbit 代码块都在 Moonpad 中渲染,那么我们需要将生成的 pre 元素替换为 div 并将 Moonpad 挂载在这个 div 上。

以下是这个逻辑的实现:

// 便利所有的 moonbit 代码块
for (const pre of document.querySelectorAll("pre:has(code.language-moonbit)")) {
// 获得代码块的内容
const code = pre.textContent;
// 创建一个 div 元素,这将会是 monaco editor 的挂载点
const div = document.createElement("div");
// 根据代码内容设置 div 的高度
const height = code.split("\n").length * 20;
div.style.height = `${height}px`;
// 将代码块替换为 div
pre.replaceWith(div);
// 使用获得的代码内容创建一个 monaco editor model
const model = monaco.editor.createModel(code, "moonbit");
// 创建 monaco editor 并挂载到 div 上,展示上一行创建的 model
monaco.editor.create(div, {
model,
theme: "light-plus",
});
}

只需将上面的代码替换掉 index.js 中的挂载逻辑即可。

导入 Moonpad

jekyll 支持在 markdown 中直接使用 html,所以我们可以在 markdown 中直接导入 Moonpad。在需要使用 Moonpad 的 markdown 文件中的最后加入以下代码即可。

<link rel="stylesheet" href="/moonpad/index.css" />
<script type="module" src="/moonpad/index.js"></script>

将 moonpad 文件夹放到 jekyll 的静态资源目录下

在执行 jekyll build 之后,jekyll 会将所有的静态资源放到 _site 目录下。我们只需要将 moonpad 文件夹复制到 _site 目录下即可。

复制后 _site 文件夹的目录结构应为

_site/
├── ... # 其他静态资源
└── moonpad
├── codicon-37A3DWZT.ttf
├── editor.worker.js
├── index.css
├── index.js
├── lsp-server.js
├── moonc-worker.js
└── onig.wasm

结果

完成以上步骤后,即可在 jekyll 中使用 Moonpad。效果如下:

在 marp 中使用 Moonpad

Marp 是一个 Markdown 转换工具,可以将 Markdown 文件转换为幻灯片。它基于 Marpit 框架,支持自定义主题,并且可以通过简单的 Markdown 语法来创建和设计幻灯片,非常适合用于制作技术演示文稿。

观察 marp 渲染代码块的结构

对于和上一小节 jekyll 中相同的代码块,marp 渲染出来的 html 结构大致如下:

<marp-pre>
...
<code class="language-moonbit">fn main { println("hello, moonbit") } </code>
</marp-pre>

显然,如果我们希望所有的 moonbit 代码块都使用 moonpad 渲染,我们需要将 marp-pre 替换为 div 并将 Moonpad 挂载在这个 div 上。

以下是这个逻辑的实现,和 jekyll 的例子大同小异:

for (const pre of document.querySelectorAll(
"marp-pre:has(code.language-moonbit)",
)) {
const code = pre.querySelector("code.language-moonbit").textContent;
const div = document.createElement("div");
const height = code.split("\n").length * 20;
div.style.height = `${height}px`;
pre.replaceWith(div);
const model = monaco.editor.createModel(code, "moonbit");
monaco.editor.create(div, {
model,
theme: "light-plus",
});
}

只需将上面的代码替换掉 index.js 中的挂载逻辑即可。

导入 Moonpad

marp 同样支持在 markdown 中使用 html,但需要显示开启这个选项。在需要使用 Moonpad 的 markdown 文件中的最后面加入以下代码:

<link rel="stylesheet" href="/moonpad/index.css" />
<script type="module" src="/moonpad/index.js"></script>

并在构建时开启 html 选项:

marp --html

将 moonpad 文件夹放到 marp 的静态资源目录下

在 marp 中预览幻灯片一般有两种方式,一种是使用 marp 自带的 server 功能,另一种是将 markdown 文件导出为 html,自行配置服务器。

对于 marp 自带的 server 功能,我们需要将 moonpad 文件夹放到 marp --server 命令指定的文件夹内。

对于将 markdown 导出为 html 的情况,我们需要保证 moonpad 文件夹和导出的 html 文件在同一个目录下。

结果

完成以上步骤后,即可在 marp 中使用 Moonpad。效果如下:

可惜的是,在 marp 中 monaco editor 的 hover 提示位置不对,目前我们还不知道如何解决这一问题。

如何使用 Moonpad 编译 MoonBit 代码

上面提到 moonbitMode.init 会返回一个简单的构建系统,我们可以使用这个构建系统来编译并运行 MoonBit 代码。它暴露了两个方法: compilerun:分别用来编译和运行 MoonBit 代码。例如:

// compile 还可以通过参数进行更多的配置,例如编译带测试的文件等。这里我们只编译一个单文件
const result = await moon.compile({ libInputs: [["a.mbt", model.getValue()]] });
switch (result.kind) {
case "success":
// 如果编译成功,则会返回编译器 js 后端编译出来的 js 代码
const js = result.js;
// 可以使用 run 方法运行编译出来的 js,得到标准输出的流。
// 值得注意的是,这里流的最小单位不是字符,而是标准输出每一行的字符串。
const stream = moon.run(js);
// 将流中的内容收集到 buffer 中,并在控制台输出。
let buffer = "";
await stream.pipeTo(
new WritableStream({
write(chunk) {
buffer += `${chunk}\n`;
},
}),
);
console.log(buffer);
break;
case "error":
break;
}

打开控制台,可以看到输出了 hello

对于compile函数的更多用法以及把代码输出展示在页面上的方法,可以参考语言导览中的代码:moonbit-docs/moonbit-tour/src/editor.ts#L28-L61

下一步

使用 Chrome 对 MoonBit 生成的 Wasm 进行性能分析

· 阅读需 10 分钟

cover

我们前一篇博客中,我们介绍了如何在前端 JavaScript 中使用 MoonBit 驱动的 Wasm 库 Cmark。在本文中,我们将探索如何直接从 Chrome 浏览器中对该库进行性能分析。希望这篇教程能对你在使用 MoonBit 在类似的场景中进行开发时提供一些洞察,从而实现更好的整体性能。

具体而言,在本文中,我们将重点介绍如何对我们之前在前端应用中使用 Cmark 的示例做最小程度的修改,以便将 Chrome 内置的 V8 性能分析器应用到 Cmark 的 Wasm 代码中。

对 Cmark 库进行性能分析

我们很容易可以重构原始的前端应用程序,并添加一个新的导航栏以包含 “Demo” 和 “Profiling” 两个链接。点击第一个链接将使浏览器渲染原本的 A Tour of MoonBit for Beginners 示例的 HTML,点击第二个链接将导航到我们的新文档进行性能分析。(如果你对实际实现感兴趣,你可以在本文的末尾找到最终代码的链接。)

现在,我们已经准备为实际实现 Wasm 性能分析着手编写一些代码了。那么,与 JavaScript 性能分析相比,进行 Wasm 性能分析是否有不同之处?

实际上,我们可以使用与 JavaScript 相同的 API 来进行 Wasm 性能分析。Chromium 文档中有一篇文章详细描述了这些 API,简而言之:

  • 当我们调用 console.profile() 时,V8 性能分析器将开始记录 CPU 性能概况;
  • 之后,我们可以调用我们希望分析的性能关键函数;
  • 最后,当我们调用 console.profileEnd() 时,性能分析器将停止记录,并将结果数据可视化显示在 Chrome 的性能标签页中。

考虑到这一点,让我们来看一下实际的性能分析功能实现:

async function profileView() {
const docText = await fetchDocText("../public/spec.md");
console.profile();
const res = cmarkWASM(docText);
console.profileEnd();
return (
`<h2>Note</h2>
<p>
<strong
>Open your browser's
<a
href="https://developer.chrome.com/docs/devtools/performance"
rel="nofollow"
>Performance Tab</a
>
and refresh this page to see the CPU profile.</strong
>
</p>` + res
);
}

如你所见,我们必须最小化性能分析器激活期间执行的代码范围。因此,我们在该范围内仅调用了 cmarkWASM() 函数。

另一方面,我们选择了 《CommonMark 规范》 的 0.31.2 版本(即前文中提到的 spec.md)作为性能分析模式的输入文档。我们选择这一文档的主要原因是该文档使用了许多不同的 Markdown 特性,同时它的长度也足以使许多 Markdown 解析器遇到问题:

> wc -l spec.md  # 行数
9756 spec.md
> wc -w spec.md # 词数
25412 spec.md

我们重新组织了前端应用程序,使得点击导航栏中的 “Profiling” 链接将触发上面定义的 profileView() 函数,从而得到以下结果:

profile-tab-stripped

如果你曾深入研究过性能优化,那么你应该不会对这个火焰图应该感到陌生了...

等等,wasm-function[679]wasm-function[367] 这些名字又是什么?我们该如何知道哪个函数对应哪个编号?

事实证明,我们需要在构建 Wasm 文件时保留一些调试信息。毕竟,我们一直使用以下命令构建我们的 MoonBit 项目:

> moon -C cmarkwrap build --release --target=wasm-gc

...而去除调试信息是 moon 在生成 release 版本时的标准行为。

幸运的是,我们可以使用一个额外的标志 --no-strip 来保留符号信息,而不必依赖于慢速的 debug 版本。让我们使用这个标志重新构建项目:

> moon -C cmarkwrap build --release --no-strip --target=wasm-gc

注意

类似地,如果我们想对生成的 Wasm 文件使用 wasm-opt,可以使用 wasm-opt--debuginfo(或 -g)标志来保留优化后输出中的函数名。

保留函数名后,我们终于可以在性能标签页中看到实际情况了!

profile-tab

分析火焰图

如上图所示的火焰图可以很好地总结函数调用及其各自的执行时间。如果你不太熟悉它,火焰图的主要思路如下:

  • Y 轴 表示调用栈,最顶部的函数最先被调用;
  • X 轴 表示执行时间,每个矩形节点的宽度对应于某个函数及其子函数的调用所花费的总时间。

既然我们正在研究 Cmark 库的性能,我们应该向下检索,并特别注意那里的 @rami3l/cmark/cmark_html.render() 节点。例如,在该节点处,我们可以清楚地看到 render() 的执行被分成了两个主要部分,分别被图中的两个子节点代表:

  • @rami3l/cmark/cmark.Doc::from_string(),表示将输入的 Markdown 文档转换为语法树;
  • @rami3l/cmark/cmark_html.from_doc(),表示将语法树渲染为最终的 HTML 文档。

为了更好地查看性能情况,我们可以单击火焰图中的 render() 节点。这会让 Chrome 更新“自底向上”视图,并仅显示由 render() 递归调用的函数。这样,我们将会得到如下所示的结果:

profile-tab-focused

在“自底向上”视图中按自时间(即排除子函数所用时间的总时间)对条目进行排序,我们可以轻松找出那些自己消耗最多时间的函数,进而对这些函数的实现进行进一步的细致检查。同时,我们还需要尽量消除深层次的调用栈,这可以通过查找火焰图中较长的垂直条形找到。

实现高性能

在开发过程中,Cmark 已经使用我们上面展示的性能分析方法进行了数百次性能分析,以追求令人满意的性能。那么,它在与流行的 JavaScript Markdown 库的比较中表现如何呢?

在这个测试中,我们选择了 MicromarkRemark——这两个在 JavaScript 生态系统中广泛使用的 Markdown 库——作为我们的参考。我们在这个测试中使用了最新版本的 Chrome 133 作为我们的 JS 和 Wasm 运行时,并使用 Tinybench 来测量每个库的平均吞吐量。

以下是这些库在 MacBook M1 Pro 上将《CommonMark 规范》转换为 HTML 的平均吞吐量:

测试(从最快到最慢)样本数平均值/Hz±/%
cmark.mbt (WASM-GC + wasm-opt)21203.663.60
cmark.mbt (WASM-GC)19188.463.84
micromark1015.482.07
remark1014.283.16

结果非常明确:得益于持续的性能分析和优化过程,Cmark 现在比 JavaScript 基于的库快了大约 12 倍,相比于 Micromark 快了 13 倍。并且,wasm-opt 的额外优化步骤可以为 Cmark 提供额外的性能提升,将这些因子提高到大约 13 倍和 14 倍。

总之,Cmark 的性能证明了 MoonBit 在前端开发场景中提供可见效率提升的强大能力。

如果你对这个演示的细节感兴趣,可以在 GitHub 上查看最终代码。用于基准测试的代码也可以在 这里 获得。

New to MoonBit?

在 JavaScript 中使用 MoonBit 的高性能 Wasm 库

· 阅读需 6 分钟

封面

在我们之前的一篇博客文章中,我们已经开始探索如何在 MoonBit 的 Wasm GC 后端中直接使用 JavaScript 字符串。正如我们所见,不仅可以用 MoonBit 编写一个兼容 JavaScript 的字符串操作 API,而且在编译为 Wasm 后,生成的产物体积非常小。

然而,您可能会好奇,在更现实的使用场景中,这将会是什么样子。因此,今天我们展示一个更贴近实际的场景:在一个由 JavaScript 驱动的 Web 应用程序中渲染 Markdown 文档,并借助 MoonBit 库 Cmark 和 Wasm 的 JS 字符串内置提案。

动机

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

鉴于 Markdown 在当今网络世界中的广泛存在,将 Markdown 转换为 HTML 的流程仍然是几乎每个 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 中提供拆解该类型的方法:result_unwrap()result_is_ok() 函数正是为此目的设计的,它们接受一个 RenderResult 输入。

与 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,并查看由 Cmark MoonBit 库渲染的 A Tour of MoonBit for Beginners

演示

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

了解更多