← 返回知识库
前端工程WebAssemblyAssemblyScriptWASM性能内存

WebAssembly 入门

WebAssembly 核心价值、AssemblyScript 手写第一个 WASM 模块、JS 与 WASM 内存交互(指针)、as-bind 高级数据类型与性能边界。

WebAssembly 入门教程:从零到一,手写第一个 WASM 模块

你好!很高兴你想亲手尝试 WebAssembly。作为前端开发者,我们将用最友好的方式——AssemblyScript(它长得像 TypeScript)来编写第一个 WASM 模块,并在浏览器中运行它。整个流程只需几分钟,但能让你彻底理解 WASM 是如何工作的。


为什么选择 AssemblyScript?

  • 语法近似 TypeScript:如果你是前端,几乎零学习成本。
  • 工具链简单:基于 Node.js/npm,无需安装庞大的 C/C++ 编译器。
  • 专注业务逻辑:可以快速验证想法,不用纠结内存管理等底层细节。

第一步:环境准备

确保你已经安装了 Node.js(建议 v16+)和 npm

打开终端,创建一个新目录并初始化项目:

mkdir my-first-wasm
cd my-first-wasm
npm init -y

第二步:安装 AssemblyScript

AssemblyScript 提供了将 TS 编译为 WASM 的编译器,以及必要的类型定义。

npm install --save-dev assemblyscript

安装完成后,执行初始化命令,它会自动创建推荐的目录结构:

npx asinit .

这个命令会做几件事:

  • 创建 assembly/ 目录,里面有个 index.ts(你的源码文件)。
  • 创建 build/ 目录,用于存放编译后的 .wasm 文件。
  • package.json 中添加几个常用的脚本命令。

现在你的项目结构应该像这样:

my-first-wasm/
├── assembly/
│   └── index.ts
├── build/
├── node_modules/
├── package.json
└── asconfig.json      # AssemblyScript 配置文件

第三步:编写 AssemblyScript 代码

打开 assembly/index.ts,删除默认内容,写入一个简单的加法函数:

// assembly/index.ts
export function add(a: i32, b: i32): i32 {
  return a + b;
}
  • i32 是 AssemblyScript 中的 32 位整数类型(对应 WASM 的 i32)。
  • export 关键字会将此函数导出,供 JavaScript 调用。

是不是和 TypeScript 一模一样?非常直观。


第四步:编译为 WebAssembly

先看一眼 asinit . 生成的默认 npm 脚本(打开 package.json):

{
  "scripts": {
    "asbuild:debug": "asc assembly/index.ts --target debug",
    "asbuild:release": "asc assembly/index.ts --target release",
    "asbuild": "npm run asbuild:debug && npm run asbuild:release",
    "test": "node tests"
  }
}

🔴 重要坑点(照抄会 404):默认的 npm run asbuild 产出的是 build/debug.wasmbuild/release.wasm(外加 untouched.wasm), 并不存在 optimized.wasm。而本文后续代码统一 fetch('./build/optimized.wasm'), 若不做下面任一步处理,会直接 404 / instantiate 失败

两种做法,二选一:

方案 A —— 改文档里的文件名:把本文所有 optimized.wasm / optimized.wat 替换成 release.wasm / release.wat

方案 B —— 改构建脚本(本文后续沿用 optimized.wasm,采用这个):在 package.json 里自定义输出名:

{
  "scripts": {
    "asbuild": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --optimize",
    "asbuild:debug": "asc assembly/index.ts -b build/debug.wasm -t build/debug.wat --debug",
    "test": "node tests"
  }
}

然后执行编译:

npm run asbuild

会生成两个文件:

  • build/optimized.wasm:优化后的生产版本(体积小,运行快)。
  • build/optimized.wat:对应的文本格式(方便人类阅读,可以打开看看)。

编译完成后,build/ 目录下应该出现了 .wasm 文件。(若报错找不到 asc,先 npm i -D assemblyscript;若 Node 版本过新导致 asc 报错,可改用 npx asc 显式调用。


第五步:在 HTML 中加载并调用 WASM

现在我们来创建一个简单的 HTML 页面,加载这个 WASM 模块并调用 add 函数。

在项目根目录下新建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我的第一个 WASM 应用</title>
</head>
<body>
    <h1>WebAssembly 加法示例</h1>
    <p>3 + 5 = <span id="result">计算中...</span></p>

    <script>
        // 使用 WebAssembly.instantiateStreaming 直接从网络流式加载并编译
        WebAssembly.instantiateStreaming(fetch('./build/optimized.wasm'))
            .then(obj => {
                // obj.instance 是模块实例,exports 包含了导出的函数
                const result = obj.instance.exports.add(3, 5);
                document.getElementById('result').innerText = result;
            })
            .catch(console.error);
    </script>
</body>
</html>

关键 API 解释

  • WebAssembly.instantiateStreaming:这是最现代、最高效的加载方式,它会一边下载一边编译。
  • fetch('./build/optimized.wasm'):获取 WASM 二进制文件(注意路径要正确)。
  • obj.instance.exports:包含所有从 WASM 导出的函数(比如我们的 add)。

由于浏览器安全限制,直接双击打开 index.html 可能因为跨域问题无法加载本地文件。我们需要通过一个本地服务器来预览。

如果你安装了 VS Code,可以使用 Live Server 插件(右键 index.html -> Open with Live Server)。或者用 Node.js 的 http-server

npx http-server .

打开浏览器访问 http://localhost:8080(端口可能不同),你应该看到页面上显示 3 + 5 = 8

🎉 恭喜!你已经成功运行了第一个 WebAssembly 模块!


第六步:深入一点——字符串与内存交互

上面的例子只传递了整数,而字符串、数组等复杂数据需要通过 WASM 的线性内存来共享。AssemblyScript 对此做了很好的封装,我们来看一个返回字符串的例子。

修改 assembly/index.ts

// assembly/index.ts
export function greet(name: string): string {
  return "Hello, " + name + "!";
}

关键前提:必须导出运行时(--exportRuntime,否则 JS 侧拿不到读写字符串的辅助函数 __getString / __newString。在 asconfig.json 的对应 target 里加上:

{
  "targets": {
    "release": {
      "outFile": "build/optimized.wasm",
      "textFile": "build/optimized.wat",
      "optimizeLevel": 3,
      "shrinkLevel": 0,
      "exportRuntime": true
    }
  }
}

或直接在命令行加 --exportRuntime(等价于 --runtime incremental)。然后重新编译:

npm run asbuild

现在修改 index.html 中的 JS 部分——这是一段真正能跑通、并且会打印出 Hello, World! 的代码

<script type="module">
  // ⚠️ 没有胶水代码时,需要自己提供 env.abort(AS 运行时的兜底导入)
  const importObject = {
    env: {
      abort: (msg, file, line, col) => {
        throw new Error(`AssemblyScript abort: ${msg} @${file}:${line}:${col}`);
      }
    }
  };

  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('./build/optimized.wasm'),
    importObject
  );

  // 取出:业务函数 greet + 内存对象 memory + 两个字符串辅助函数
  const { greet, memory, __getString, __newString } = instance.exports;

  // ① JS 字符串 → WASM:__newString 把 "World" 写进 WASM 线性内存,返回指针
  const namePtr = __newString("World");

  // ② 调用 greet,返回的是"指向 WASM 内存中结果字符串"的指针(一个数字)
  const resultPtr = greet(namePtr);

  // ③ WASM 字符串 → JS:__getString 依据内存里的长度前缀 + UTF-16 解码回 JS 字符串
  console.log(__getString(resultPtr));   // 输出:Hello, World!
</script>

原理小结(面试可讲):JS 与 WASM 不能直接传字符串——两者内存空间独立。所有复杂类型都要走"写进线性内存 → 传指针 → 按约定解读"这条路:

  • __newString(str):JS 侧分配内存并写入,返回指针(本质是个整数偏移量);
  • __getString(ptr):按 AssemblyScript 的字符串布局(长度前缀 + UTF-16 数据,前面还有运行时对象头)从内存中解出 JS 字符串;
  • memory.buffer 就是那块共享的 ArrayBuffer,JS 可以用 new Uint8Array(memory.buffer, ptr, len) 直接读写。

⚠️ 两个实操提醒

  1. instantiateStreaming 要求服务器返回的 MIME 是 application/wasmfile:// 直接打开 HTML 会失败,起个本地服务器(npx serve / VSCode Live Server)即可;实在不行用 WebAssembly.instantiate(await (await fetch(url)).arrayBuffer(), ...) 兜底。
  2. 指针类型在 JS 侧是 number别当字节偏移直接用——Float32Array 这类视图的下标要先把字节偏移除以元素大小。

不过,如果只是想让 WASM 算、JS 拼,还有个更省事的思路:让 WASM 只返回数字,字符串拼接交给 JS。这样完全避开内存操作,代价是多一次跨界调用。选哪种取决于你的性能与可维护性取舍。

JS ↔ WASM 内存交互图解

flowchart LR
    subgraph JS["🟨 JavaScript 侧"]
        JSHeap["JS 堆<br/>(对象、字符串)"]
        View["TypedArray 视图<br/>new Float32Array<br/>(memory.buffer, ptr, len)"]
    end

    subgraph WASM["🟦 WebAssembly 侧"]
        Linear["线性内存 Linear Memory<br/>(一整块 ArrayBuffer,编号从 0 开始)"]
        Str["字符串布局:<br/>运行时头 + 长度前缀 + UTF-16 数据"]
        Stack["WASM 栈 / 局部变量"]
    end

    JSHeap -->|"__newString(str)<br/>写入内存,返回指针"| Str
    Str -->|"__getString(ptr)<br/>按长度解码,返回 JS 字符串"| JSHeap
    View <-->|"共享同一块 ArrayBuffer<br/>零拷贝读写"| Linear
    Stack -->|"函数返回值(数值/指针)"| JSHeap

    style Linear fill:#dae8fc,stroke:#6c8ebf
    style JSHeap fill:#fff2cc,stroke:#d6b656
    style View fill:#d5e8d4,stroke:#82b366

一句话总结:JS 与 WASM 内存不互通,唯一的桥是 WASM 线性内存 + 指针(整数偏移量)。 传数值直接传;传字符串/数组则要"写进去 → 传指针 → 对方按约定读出来"。 ⚠️ 两个高频坑:① memory.grow() 之后旧的 ArrayBufferdetached,缓存的视图失效,必须重新 new;② 指针是字节偏移,建 Float32Array 视图时下标要 ptr / 4


第七步:使用 C 语言(备选方案)

如果你已经熟悉 C/C++,也可以使用 Emscripten 来编译。这里只给出快速步骤,不展开(因为环境配置稍复杂)。

  1. 安装 Emscripten SDK(参考 官方文档)。
  2. 编写一个 C 文件 add.c
    int add(int a, int b) {
        return a + b;
    }
    
  3. 编译为 WASM:
    emcc add.c -s EXPORTED_FUNCTIONS='["_add"]' -o add.js
    
    这会生成 add.js(胶水代码)和 add.wasm
  4. 在 HTML 中引入 add.js 即可使用 Module._add()

总结与下一步

通过这个教程,你学会了:

  • 使用 AssemblyScript 编写简单的 WASM 模块。
  • 将 TypeScript 风格的代码编译为 .wasm
  • 在浏览器中加载并调用 WASM 函数。

这只是开始,WebAssembly 的世界远不止于此。接下来你可以:

  • 学习更多 AssemblyScript 特性:如内存管理、数组、类等。
  • 尝试 as-bind 实现 JS 与 WASM 的无缝数据交换。
  • 探索真实应用场景:如图像处理、音频编解码、游戏引擎移植。
  • 阅读 MDN 文档WebAssembly 概念使用 WebAssembly 的 JavaScript API

as-bind 使用教程:在 AssemblyScript 和 JavaScript 之间传递高级数据类型

🔴 本节前置警告:as-bind 已停止维护,请谨慎使用

as-bind 最后活跃时间约为 2021 年(本文示例锁定 as-bind@0.8.0),此后基本停止更新。它与当前版本的 AssemblyScript(0.20+)存在兼容问题——其依赖的 --transform as-bind / --exportRuntime 机制在新的 asc 运行时 ABI 下已不适用,照抄本节代码大概率编译失败或运行时出错

因此本节请当作"了解设计思路"来读,不要直接用于生产。 想在 2026 年做字符串 / 数组互传,推荐以下替代方案:

方案说明适用场景
手动管理内存(本文上一节已示范)__newString / __getString / memory.buffer 自己读写简单类型、追求零依赖
AssemblyScript 官方 loader@assemblyscript/loader 提供 instantiate 与字符串辅助函数多数项目
Emscripten Embind / WebIDL Binder若用 C/C++ 编写,生态最成熟C/C++ 项目
wasm-bindgenRust 生态的标准方案Rust 项目

面试时如果聊到"JS 与 WASM 怎么传复杂类型",推荐答"手动内存操作 + 指针约定"或"用官方 loader",并主动指出 as-bind 已不维护——这比背一个过时库更能体现技术判断力。

接续上一个教程,当你尝试在 AssemblyScript 中返回字符串时,会遇到内存管理的难题——这就是 as-bind 要解决的问题。as-bind 是 AssemblyScript 生态中专门用于在 JS 和 WASM 之间传递高级数据类型(如字符串、数组) 的库,曾被称作"AssemblyScript 界的 wasm-bindgen"。

1. 安装 as-bind

在上一节创建的 my-first-wasm 项目中,安装 as-bind:

npm install --save as-bind

as-bind 的核心机制是:编译时通过 transform 嵌入类型信息,运行时利用 AssemblyScript Loader 自动处理内存读写

2. 编写支持字符串的 AssemblyScript 代码

修改 assembly/index.ts,编写一个接收字符串并返回字符串的函数:

// assembly/index.ts
export function greet(name: string): string {
  return "Hello, " + name + "!";
}

// 你也可以返回数组或其他复杂类型
export function doubleArray(arr: Int32Array): Int32Array {
  const result = new Int32Array(arr.length);
  for (let i = 0; i < arr.length; i++) {
    result[i] = arr[i] * 2;
  }
  return result;
}

注意:这里直接使用了 stringInt32Array 类型,完全像在 TypeScript 中一样自然。

3. 编译 AssemblyScript(关键步骤)

编译时必须加上 as-bind 要求的两个参数:

npx asc assembly/index.ts \
  --target release \
  --exportRuntime \
  --transform as-bind \
  -o build/optimized.wasm

参数说明:

  • --exportRuntime必须,导出 AssemblyScript 运行时函数(如 __new__pin),让 as-bind 能在 JS 中分配内存
  • --transform as-bind必须,在编译时嵌入类型信息,使 as-bind 知道如何转换数据结构

4. 在 JavaScript 中使用 as-bind

修改 index.html,使用 as-bind 加载并调用 WASM 模块:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>as-bind 示例</title>
</head>
<body>
    <h1>as-bind 字符串传递示例</h1>
    <p id="greeting">计算中...</p>
    <p id="array-result"></p>

    <!-- 方式1:使用 ES module 导入(推荐) -->
    <script type="module">
        import * as AsBind from 'https://unpkg.com/as-bind@0.8.0/dist/as-bind.esm.js';

        const wasm = fetch('./build/optimized.wasm');

        const run = async () => {
            // 使用 AsBind.instantiate 替代 WebAssembly.instantiate
            const asBindInstance = await AsBind.instantiate(wasm);
            
            // 直接调用导出函数,传递字符串!
            const greeting = asBindInstance.exports.greet("WebAssembly");
            document.getElementById('greeting').innerText = greeting;
            
            // 传递数组
            const originalArray = new Int32Array([1, 2, 3, 4, 5]);
            const doubled = asBindInstance.exports.doubleArray(originalArray);
            document.getElementById('array-result').innerText = 
                `原数组: [${originalArray.join(', ')}] → 翻倍后: [${doubled.join(', ')}]`;
        };

        run().catch(console.error);
    </script>

    <!-- 方式2:如果不使用模块化,可以用 IIFE 版本 -->
    <!-- 
    <script src="https://unpkg.com/as-bind"></script>
    <script>
        const { AsBind } = AsBindIIFE;
        // 同上...
    </script>
    -->
</body>
</html>

关键变化:

  • 使用 AsBind.instantiate(wasm) 替代 WebAssembly.instantiateStreaming
  • 返回的实例中 exports 已经自动包装,可以直接传递和接收字符串/数组

5. 从 JavaScript 导入函数给 AssemblyScript 调用

as-bind 也支持在 AssemblyScript 中调用 JS 函数,并自动处理参数类型。

修改 AssemblyScript

// assembly/index.ts
// 声明一个外部函数,将在 JS 中实现
declare function consoleLog(message: string): void;

export function testLog(): void {
  consoleLog("Hello from AssemblyScript!");
}

重新编译(命令同上)。

修改 JavaScript

import * as AsBind from 'https://unpkg.com/as-bind@0.8.0/dist/as-bind.esm.js';

const wasm = fetch('./build/optimized.wasm');

const run = async () => {
  // 第二个参数是 importObject,与 WebAssembly.instantiate 格式相同
  const asBindInstance = await AsBind.instantiate(wasm, {
    // 模块名对应 AssemblyScript 文件(不加路径)
    index: {  // 这里 'index' 是你的文件名(无扩展名)
      consoleLog: (message) => {
        console.log('[WASM log]:', message);
        document.body.innerHTML += `<p>收到 WASM 消息: ${message}</p>`;
      }
    }
  });
  
  // 调用 WASM 函数,它会回调上面的 consoleLog
  asBindInstance.exports.testLog();
};

run();

as-bind 会自动将 AssemblyScript 的字符串转换为 JS 字符串,传递给 consoleLog

6. 支持的数据类型

根据官方文档,as-bind 支持以下数据类型在 JS 和 WASM 之间自动转换:

类型作为导出函数参数作为导出函数返回值作为导入函数参数作为导入函数返回值
数字 (i32/f32/f64)
字符串❌*
Int8Array/Uint8Array
Int16Array/Uint16Array
Int32Array/Uint32Array
Float32Array/Float64Array
Array(如 Array⚠️ 建议用 TypedArray⚠️ 建议用 TypedArray⚠️ 建议用 TypedArray

*注:导入函数返回值目前只支持数字类型,字符串和数组暂不支持作为导入函数的返回值。

7. 在 Node.js 中使用

如果你需要在 Node.js 环境中使用(比如做后端服务或测试),用法类似:

// 注意:要用 .cjs.js 版本
const AsBind = require("as-bind/dist/as-bind.cjs.js");
const fs = require("fs");

const wasmBuffer = fs.readFileSync("./build/optimized.wasm");

(async () => {
  const asBindInstance = await AsBind.instantiate(wasmBuffer);
  const result = asBindInstance.exports.greet("Node.js");
  console.log(result); // "Hello, Node.js!"
})();

8. 进阶:使用 AssemblyScript 类

as-bind 还支持将 AssemblyScript 中定义的类导出,在 JavaScript 中实例化:

AssemblyScript (assembly/vector.ts):

export class Vector2D {
  x: i32;
  y: i32;
  
  constructor(x: i32, y: i32) {
    this.x = x;
    this.y = y;
  }
  
  magnitude(): f32 {
    return Math.sqrt(<f32>(this.x * this.x + this.y * this.y)) as f32;
  }
}

编译(同上)。

JavaScript

const asBindInstance = await AsBind.instantiate(fetch('./build/optimized.wasm'));

// 可以直接使用导出的类!
const { Vector2D } = asBindInstance.exports;
const vec = new Vector2D(3, 4);
console.log(vec.x, vec.y);              // 3, 4
console.log(vec.magnitude());            // 5

注意:类的属性会直接映射为 JS 属性,方法也可以正常调用。

9. 完整可运行示例

我把上述内容整合成一个完整的 HTML 文件,你可以直接复制使用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>as-bind 完整示例</title>
    <style>
        body { font-family: Arial; padding: 20px; line-height: 1.6; }
        .box { background: #f0f0f0; padding: 15px; border-radius: 8px; margin: 15px 0; }
    </style>
</head>
<body>
    <h1>📦 as-bind 高级数据类型传递</h1>
    
    <div class="box">
        <h3>1. 字符串传递</h3>
        <p id="string-result">加载中...</p>
    </div>
    
    <div class="box">
        <h3>2. 数组传递</h3>
        <p id="array-result">加载中...</p>
    </div>
    
    <div class="box">
        <h3>3. JS 导入函数(WASM 调用 JS)</h3>
        <div id="log-area"></div>
    </div>
    
    <script type="module">
        import * as AsBind from 'https://unpkg.com/as-bind@0.8.0/dist/as-bind.esm.js';
        
        // AssemblyScript 源码(你需要先编译好):
        /*
        export function greet(name: string): string {
            return "Hello, " + name + "!";
        }
        
        export function doubleArray(arr: Int32Array): Int32Array {
            const result = new Int32Array(arr.length);
            for (let i = 0; i < arr.length; i++) result[i] = arr[i] * 2;
            return result;
        }
        
        declare function logFromJS(message: string): void;
        
        export function callJS(): void {
            logFromJS("来自 WASM 的问候!");
        }
        */
        
        const wasm = fetch('./build/optimized.wasm');
        
        const run = async () => {
            // 准备导入对象
            const importObject = {
                index: {
                    logFromJS: (msg) => {
                        console.log('[WASM]', msg);
                        document.getElementById('log-area').innerHTML += 
                            `<p>🔔 ${msg}</p>`;
                    }
                }
            };
            
            // 实例化
            const asBindInstance = await AsBind.instantiate(wasm, importObject);
            
            // 1. 字符串测试
            const greeting = asBindInstance.exports.greet("WebAssembly");
            document.getElementById('string-result').innerText = greeting;
            
            // 2. 数组测试
            const input = new Int32Array([2, 4, 6, 8]);
            const output = asBindInstance.exports.doubleArray(input);
            document.getElementById('array-result').innerHTML = 
                `原始数组: [${input.join(', ')}]<br>` +
                `翻倍后: [${output.join(', ')}]`;
            
            // 3. 调用 JS 函数
            asBindInstance.exports.callJS();
        };
        
        run().catch(err => {
            document.body.innerHTML += `<p style="color:red">错误: ${err}</p>`;
            console.error(err);
        });
    </script>
</body>
</html>

总结

通过 as-bind,你可以在 AssemblyScript 和 JavaScript 之间像写普通 TypeScript 一样传递字符串、数组和对象,完全不用手动操作 WebAssembly 的线性内存。它的核心机制是:

  1. 编译时--transform as-bind 注入类型信息
  2. 运行时AsBind.instantiate 包装导入导出函数,自动处理内存分配和转换

现在你可以专注于业务逻辑,让 as-bind 处理复杂的数据传递了。