本文主要和大家讨论 asm-dom,即通过 WebAssembly 技术 C++ 率先支持虚拟DOM。在年初,我也确实使用 WebAssembly 将客户端应用成功移植到了 Web,这也是为什么我一直对 WebAssembly 充满好奇的原因。我甚至在头条上开了一个合集《WebAssembly 前沿技术》来专门探讨 WebAssembly ,并将持续关注 WebAssembly 的最新动态。
下面是已发布部分文章传送门(更多文章可以阅读我的头条专题):
正如大家所看到的,当我们还在迟疑是否要在日常开发中引入 WebAssembly 的时候,很多优秀的应用、工具已经开始吃 WebAssembly 的红利了,而且取得了不错的成就,这可能也是为什么各个浏览器厂商、开发者如此热衷 WebAssembly 的原因吧。
asm-dom 是用于构建 C++ SPA 单页应用程序的最小 WebAssembly 虚拟 DOM。 基于 asm-dom,开发者可以使用 C++ 编写整个 SPA,然后使用 Emscripten 将其编译为 WebAssembly(或作为后备的 asmjs),asm-dom 将自动处理 DOM API 调用,从而生成一个旨在通过利用通用硬件功能以本机速度执行的应用程序。
asm-dom项目
此外,开发者可以基于原有 C/C++ 代码而无需任何更改,比如创建绑定层。 这相当于使用 C++ 中创建一个应用程序,在必要的时候会自动调用 JavaScript,而不是通过 Javascript 反向调用 C++。 asm-dom 允许开发者只用 C++ 编写代码一次,并在桌面、移动应用程序和 Web 端实现代码共享。
目前 asm-dom 在 Github 上通过 MIT 协议开源,有超过 2.7k 的 star、0.1k 的 fork、是一个值得关注的开源项目。可以将 asm-dom 的典型特征归纳为以下几个核心要点,包括:
asm-dom 是一个底层虚拟 DOM 库,对于开发者应该如何构建自己的应用程序并无偏好。
只用 C++ 编写代码一次,并与桌面/移动应用程序和 Web 端最大限度的实现代码共享。
asm-dom 允许开发者使用 C++ 编写整个 SPA,并使用 Emscripten 将其编译为 WebAssembly,同时保证应用速度。这对于基于浏览器、WebView的应用程序来说绝对是福音。
借助于 gccx,即一个允许简单语法(CPX)的解析器(和 JSX 有细微差异),asm-dom 会自动将 CPX 语法转换为标准 C++。 通过这种方式,开发者可以编写出与 html 非常相似的文件,从而提供开发人员体验。
VNode* vnode = (
<div
onclick={[](emscripten::val e) -> bool {
emscripten::val::global("console").call<void>("log", emscripten::val("clicked"));
return true;
}}
>
<span style="font-weight: bold">This is bold</span>
and this is just normal text
<a href="/foo">I'll take you places!</a>
</div>
);
asm-dom 支持服务器端渲染,开发者可以用 C++ 编写服务器并使用 WebAssembly 在 Node.js 上运行。只需要两个核心步骤:
下面是服务端渲染的典型示例:
// 返回视图的函数,用于客户端和服务器
VNode* view() {
return (
h("div",
Data(
Attrs {
{"id", "root"}
}
),
Children {
h("h1", std::string("Title")),
h("button",
Data(
Attrs {
{"class", "btn"}
},
Callbacks {
{"onclick", onButtonClick}
}
),
"Click Me!"
)
}
)
);
}
// 服务端渲染
VNode* vnode = view();
std::string AppString = toHTML(vnode);
std::string html = (
"<!DOCTYPE html>"
"<html>"
"<head>"
"<title>My Awesome App</title>"
"<link rel="stylesheet" href="/index.css" />"
"</head>"
"<body>"
+ appString +
"</body>"
"<script src="/bundle.js"></script>"
"</html>"
);
// 客户端渲染
VNode* oldVNode = toVNode(
emscripten::val::global("document").call<emscripten::val>("getElementById", emscripten::val("root"))
);
VNode* vnode = view();
patch(oldVNode, vnode);
// 附加事件处理器
asm-dom 是一个底层虚拟 DOM 库,与如何构建应用程序无关。asm-dom 初始诞生于非游戏、VR、AR 或图像/视频编辑的常见用例中测试 WebAssembly 的强大功能的想法。然而不幸的是,目前 GC/DOM Integration 仍然是一个面向未来的特性 ,所以,asm-dom 并不是完全在 wasm 中开发的。
asm-dom 中所有与 DOM 的交互都是用 Javascript 编写的, 这是一个很大的缺点,因为需要 JS 和 WASM 之间额外的绑定开销。当然,未来 asm-dom 会更强大。
如果想在没有任何配置的情况下开始使用 asm-dom,可以考虑 asm-dom-boilerplate(查看文末资料),这是一个非常简单的项目,包含需要的所有内容,只需克隆并运行它。如果使用的是 webpack,为了使用 asm-dom,必须准备环境。
首先需要安装 arraybuffer-loader:
npm install --save-dev arraybuffer-loader
然后在 Webpack 的 loader 中添加下面的内容:
{
test: /.wasm$/,
loaders: ['arraybuffer-loader'],
}
另外,如果 fs 有一些问题,可以将下面代码添加到 webpack 配置中:
node: {
fs: 'empty',
},
之后,可以使用 cpp 文件夹中的源代码构建应用程序:
并使用 emscripten (emcc cli) 编译它,下面是关于此步骤的一些提示:
编译后就可以在项目中导入应用程序,但是需要注意的是如果使用了 Babel,则需要忽略编译后的文件:
{
test: /.js$/,
loaders: ['babel-loader'],
exclude: [
/node_modules/,
/compiled/, // folder that contAIns the compiled code (wasm and asmjs)
/.asm.js$/ // ignore all .asm.js files
],
}
asm-dom 对类 JSX 语法的支持是通过 gccx 实现的,在使用之前需要导入它,然后就可以像 JSX 那样编写代码。比如下面的例子:
import gccx from 'gccx';
const code = `
#include "../asm-dom/asm-dom.hpp"
#include <emscripten/val.h>
#include <string>
using namespace asmdom;
int main() {
VNode* vnode = <h1>Hello world!</h1>;
// jsx语法
patch(
emscripten::val::global("document").call<emscripten::val>(
"getElementById",
std::string("root")
),
vnode
);
return 0;
};
`;
const compiled = gccx.parse(code);
// 编译代码为字符串
下面是 asm-dom 官方提供的一个示例。
#include "asm-dom.hpp"
using namespace asmdom;
int main() {
Config config = Config();
init(config);
// 借助于 gccx,asm-dom 可以与类似 JSX 的语法一起使用
VNode* vnode = (
<div
onclick={[](emscripten::val e) -> bool {
emscripten::val::global("console").call<void>("log", emscripten::val("clicked"));
return true;
}}
>
<span style="font-weight: bold">This is bold</span>
and this is just normal text
<a href="/foo">I'll take you places!</a>
</div>
);
//插入到空的 DOM 元素,这会修改 DOM 作为副作用
patch(
emscripten::val::global("document").call<emscripten::val>(
"getElementById",
std::string("root")
),
vnode
);
// 不借助于 gccx的语法
VNode* newVnode = h("div",
Data(
Callbacks {
{"onclick", [](emscripten::val e) -> bool {
emscripten::val::global("console").call<void>("log", emscripten::val("another click"));
return true;
}}
}
),
Children {
h("span",
Data(
Attrs {
{"style", "font-weight: normal; font-style: italic"}
}
),
std::string("This is now italic type")
),
h(" and this is just normal text", true),
h("a",
Data(
Attrs {
{"href", "/bar"}
}
),
std::string("I'll take you places!")
)
}
);
// Second `patch` invocation
patch(vnode, newVnode);
// asm-dom 有效地将旧视图更新为新状态
return 0;
};
随着 Web 的不断发展,WebAssembly 会在 Web 应用程序的开发中发挥越来越重要的作用,很多高级语言如:C++/C、Rust、Go、Zig、C#、php、Java等等都已经能将 WebAssembly 作为编译目标 。
同时在基于 WebAssembly 的高级语言 Web 框架层面也是层出不穷,比如 Rust生 态的Yew,Sycamore、percy、seed、MoonZoon 等等,又比如 Go 生态的 Vecty、Vugu、go-app、vue等等。这对于希望扩展技能并保持技术领先地位的开发人员是时候考虑学习 WebAssembly 并解锁其威力了。
因为篇幅有限,文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!
https://mbasso.github.io/asm-dom/
https://github.com/mbasso/asm-dom-boilerplate
https://github.com/mbasso/gccx
https://mbasso.github.io/asm-dom/docs/inline-example.html
https://www.geeksforgeeks.org/explain-dirty-checks-in-react-js/
封面图版权:作者 swapnil074 的《Explain dirty checks in React.js》