AssemblyScript 入门指南

3 年前 · 来自专栏 前端先锋

作者:Danny Guo
翻译:疯狂的技术宅
原文: blog.logrocket.com/the-
未经允许严禁转载



WebAssembly (Wasm)是 Web 浏览器中相对较新的功能,但它地扩展了把 Web 作为服务应用平台的功能潜力。

对于 Web 开发人员来说,学习使用 WebAssembly 可能会有一个艰难的过程,但是 AssemblyScript 提供了一种解决方法。首先让我们看一下为什么 WebAssembly 是一项很有前途的技术,然后再看怎样 AssemblyScript 挖掘潜力。

WebAssembly

WebAssembly 是浏览器的低级语言,为开发人员提供了除 JavaScript 之外的 Web 编译目标。它使网站代码可以在安全的沙盒环境中以接近本机的速度运行。

它是根据所有主流浏览器(Chrome,Firefox,Safari 和 Edge)所代表的意见开发的,他们 达成了设计共识 ,这些浏览器现在都支持 WebAssembly。

WebAssembly 以二进制格式交付,这意味着与 JavaScript 相比,WebAssembly 在大小和加载时间上都具有优势。但是它也有易于理解的 文本表示形式

当 WebAssembly 首次发布时,一些开发人员认为它有可能最终取代 JavaScript 作为 Web 的主要语言。但是最好把 WebAssembly 看作是与现有 Web 平台良好集成的新工具,这是它的 高级目标

WebAssembly 并没有取代 JavaScript 现有的用例,而是吸引了更多人,因为它引入了新的用例。 目前 WebAssembly 还不能直接访问 DOM,大多数网站都希望使用 JavaScript,经过多年的优化,JavaScript 已经相当快了。以下 WebAssembly 可能的使用案例列表 的示例:

  • 游戏
  • 科学的可视化和模拟
  • CAD应用
  • 图像/视频编辑

这些应用共同特点是,它们通常会被看作是桌面应用。通过为 CPU 密集型任务提供接近本机的性能,WebAssembly 使得将这些程序迁移至 Web 成为可行。

现有网站也可以从 WebAssembly 中受益。 Figma( figma.com/ ) 提供了一个真实的例子,它通过使用 WebAssembly 大大缩短了其加载时间。如果网站使用进行大量计算的代码,则可以将其替换为 WebAssembly 以提高性能。

也许现在你对怎样使用 WebAssembly 感兴趣。你可以学习语言本身并 直接编写 ,但实际上它打算成为 其他语言的编译目标 。它被 设计 为对 C 和 C++ 具有良好的支持,Go语言 在 version 1.11 中增加了实验性支持 的版本中,Rust 也对其 进行了大量投入

但是也许你并不想为了使用 WebAssembly 而学习或使用其中某种语言。这就是 AssemblyScript 存在的意义。

AssemblyScript

AssemblyScript 是一个把 TypeScript 转换到 WebAssembly 的编译器。由微软开发的 TypeScript 将类型添加到了 JavaScript 中。它已经变得 相当受欢迎 ,即使对于不熟悉它的人,AssemblyScript 只允许 TypeScript 的有限功能子集,因此不需要花太多时间就可以上手。。

因为它与 JavaScript 非常相似,所以 AssemblyScript 使 Web 开发人员可以轻松地将 WebAssembly 整合到他们的网站中,而不必使用完全不同的语言。

试用

让我们编写第一个 AssemblyScript 模块(以下所有代码均可在 GitHub 上 找到)。我们需要 Node.js 的最低版本为 8 才能得到 WebAssembly 的支持

转到一个空目录,创建一个 package.json 文件,然后安装 AssemblyScript。请注意,我们需要直接从 它的 GitHub 存储库 安装。它尚未在 npm 上发布,因为 AssemblyScript 开发人员 还没有考虑编译器是否已经准备好 能够支持广泛使用。

mkdir assemblyscript-demo
cd assemblyscript-demo
npm init
npm install --save-dev github:AssemblyScript/assemblyscript

使用 asinit 命令生成脚手架文件:

npx asinit .

我们的 package.json 现在应该包含以下脚本:

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"
}

顶层的 index.js 看起来像这样:

const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
  env: {
    abort(_msg, _file, line, column) {
       console.error("abort called at index.ts:" + line + ":" + column);
Object.defineProperty(module, "exports", {
  get: () => new WebAssembly.Instance(compiled, imports).exports
});

它使我们能够像使用普通的 JavaScript 模块一样轻松地 require WebAssembly 模块。

assembly 目录中包含我们的 AssemblyScript 源代码。生成的示例是一个简单的加法函数。

export function add(a: i32, b: i32): i32 {
  return a + b;

函数签名就像在 TypeScript 中那样,它之所以使用 i32 的原因是 AssemblyScript 使用了 WebAssembly 的 特定整数和浮点类型 ,而不是 TypeScript 的href=" typescriptlang.org/docs ">通用 number 类型。

让我们来构建示例。

npm run asbuild

build 目录现在应包含以下文件:

optimized.wasm
optimized.wasm.map
optimized.wat
untouched.wasm
untouched.wasm.map
untouched.wat

我们得到了构建的普通版本和优化版本。对于每个构建版本,都有一个 .wasm 二进制文件,一个 .wasm.map 源码映射 ,以及二进制文件的 .wat 文本表示形式。文本表示形式是为了供人阅读,但现在我们无需阅读或理解它——使用 AssemblyScript 的目的之一就是我们不需要使用原始 WebAssembly。

启动 Node 并像其他模块一样使用编译模块。

$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> const add = require('./index').add;
undefined
> add(3, 5)
8

这就是从 Node 调用 WebAssembly 所需要的全部!

添加监视脚本

为了便于开发,我建议你在每次更改源代码时都用 onchange 自动重建模块,因为 AssemblyScript 尚不包括监视模式

npm install --save-dev onchange

package.json 中添加一个 asbuild:watch 脚本。包含 -i flag ,即可在运行命令后立即运行初始构建。

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
    "asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild"
}

现在你可以运行 asbuild:watch ,而不必不断地重新运行 asbuild

性能

让我们写一个基本的基准测试,用来了解究竟可以获得什么样的性能提升。 WebAssembly 的专长是处理诸如数字计算之类的 CPU 密集型任务,所以我们用一个函数来确定整数是否为质数。

我们的参考实现如下所示。这是一种幼稚的暴力解决方案,因为我们的目标是执行大量计算。

function isPrime(x) {
    if (x < 2) {
        return false;
    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
    return true;

等效的 AssemblyScript 版本仅需要一些类型注释:

function isPrime(x: u32): bool {
    if (x < 2) {
        return false;
    for (let i: u32 = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
    return true;
}

我们将使用 Benchmark.js

npm install --save-dev benchmark

创建 benchmark.js

const Benchmark = require('benchmark');
const assemblyScriptIsPrime = require('./index').isPrime;
function isPrime(x) {
    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
    return true;
const suite = new Benchmark.Suite;
const startNumber = 2;
const stopNumber = 10000;
suite.add('AssemblyScript isPrime', function () {
    for (let i = startNumber; i < stopNumber; i++) {
        assemblyScriptIsPrime(i);
}).add('JavaScript isPrime', function () {
    for (let i = startNumber; i < stopNumber; i++) {
        isPrime(i);
}).on('cycle', function (event) {
    console.log(String(event.target));
}).on('complete', function () {
    const fastest = this.filter('fastest');
    const slowest = this.filter('slowest');
    const difference = (fastest.map('hz') - slowest.map('hz')) / slowest.map('hz') * 100;
    console.log(`${fastest.map('name')} is ~${difference.toFixed(1)}% faster.`);
}).run();

在我的机器上,运行 node benchmark 时得到了以下结果:

AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled)
JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled)
AssemblyScript isPrime is ~20.2% faster.

请注意,这个测试是一个 microbenchmark ,我们应该谨慎阅读。

对于一些更多的 AssemblyScript 基准测试,我建议你查看 WasmBoy 基准测试 波动方程式基准测试

加载模块

接下来,在网站中使用我们的模块。

先创建 index.html

<!DOCTYPE html>
        <meta charset="utf-8" />
        <title>AssemblyScript isPrime demo</title>
    </head>
        <form id="prime-checker">
            <label for="number">Enter a number to check if it is prime:</label>
            <input name="number" type="number" />
            <button type="submit">Submit</button>
        </form>
        <p id="result"></p>
        <script src="demo.js"></script>
    </body>
</html>

再创建 demo.js 。加载 WebAssembly 模块有 多种方式 ,但是最有效的方法是通过使用 WebAssembly.instantiateStreaming 函数以流的方式编译和实例化。请注意,如果 assertion 失败的话,我们需要 提供 abort 函数

(async () => {
    const importObject = {
        env: {
            abort(_msg, _file, line, column) {
                console.error("abort called at index.ts:" + line + ":" + column);
    const module = await WebAssembly.instantiateStreaming(
        fetch("build/optimized.wasm"),
        importObject
    const isPrime = module.instance.exports.isPrime;
    const result = document.querySelector("#result");
    document.querySelector("#prime-checker").addEventListener("submit", event => {
        event.preventDefault();
        result.innerText = "";
        const number = event.target.elements.number.value;
        result.innerText = `${number} is ${isPrime(number) ? '' : 'not '}prime.`;