添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • WebAssembly 是一种在Web上运行字节码的编译型语言。
  • 相对于 Javascript,WebAssembly 提供了可预测的性能。 它并不是永远比 JavaScript 快,但是在一些正确的用例中,它的运行效率可以比JavaScript 快。 例如计算密集型任务,例如嵌套循环或处理大量数据。 因此,WebAssembly 是对 JavaScript 的补充,而不是替代品。
  • WebAssembly 具有极高的可移植性。 WebAssembly 可以运行在:所有主流的 Web浏览器,V8 的运行时环境(如Node.js)和独立的 Wasm 运行时环境(如 Wasmtime Lucet Wasmer )。
  • WebAssembly 具有线性内存,换句话说,就是一个很大的可扩展数组。 并且在 Javascript 上下文中,可以通过 Javascript和 Wasm 同步访问。
  • WebAssembly 可以导出函数和常量,并且可以在Javascript上下文中由 Javascript 和 Wasm 同步访问。
  • WebAssembly 在其当前的 MVP 中仅处理整数和浮点数。 但是,存在工具和库可以使高级数据类型的传递变得方便。
  • 什么是 AssemblyScript ?

    AssemblyScript 本质上是 WebAssembly 编译器的 TypeScript。可以说是专门为想要编写 WebAssembly 而又不想学习新语言的前端开发人员准备的。 但是,由于 WebAssembly 与 TypeScript / JavaScript 的区别,不能直接将我们的 TypeScript 应用程序(比如 React + TypeScript 的技术栈)编译为 WebAssembly,这是语言本身上的区别和限制造成的,一些 JavaScript 的特性和 Typescript 的类型我们是不能够使用的。 但是如果从零开始搭建 WebAssembly 应用的话 AssemblyScript 仍然是一个好的选择。

    浏览器中的 WebAssembly

    WebAssembly JavaScript 对象是所有 WebAssembly 相关功能的命名空间。

    和大多数全局对象不一样, WebAssembly 不是一个构造函数(它不是一个函数对象)。它类似于 Math 对象或者 Intl 对象,Math 对象也是一个命名空间对象,用于保存数学常量和函数,Intl 则是用于国际化和其他语言相关函数的命名空间对象。

    为了理解 WebAssembly 如何在浏览器中运行,需要了解几个概念。

    下面的概念都被抽象为了一个对象

    模块(Module)

    包含已经由浏览器编译的无状态 WebAssembly 代码,可以高效地与 Workers 共享(通过 postMessage() 函数)、缓存在 IndexedDB 中,和多次实例化。一个模块可以被当做一个 JavaScript 模块导入导出。

    创建方法:

  • WebAssembly.Module() 构造函数可以用来同步编译给定的 WebAssembly 二进制代码。
  • 通过异步编译函数,如 WebAssembly.compile() 方法,或者是通过 IndexedDB 读取 Module 对象。
  • 注: 由于大型模块的编译可能很消耗资源,开发人员只有在绝对需要同步编译时,才使用 Module() 构造函数;其他情况下,应该使用异步 WebAssembly.compile() 方法。

    内存(Memory)

    一个可调整大小的 ArrayBuffer ,其内存储的是 WebAssembly 实例 所访问内存的原始字节码。从 JavaScript 或 WebAssembly 中所创建的内存,可以由 JavaScript 或 WebAssembly 来访问及更改。

    创建方法:

  • WebAssembly.Memory() 构造函数创建一个新的 Memory 对象。该对象的 buffer 属性即可获取到需要的 ArrayBuffer
  • 表格(Table)

    具有类数组结构,存储了多个函数引用。在 Javascript 或者 WebAssemble 中创建 Table 对象可以同时被 Javascript 或 WebAssemble 访问和更改。

    创建方法:

  • 一般会通过 WebAssembly.Table() 构造函数根据给定的大小和元素类型创建一个 Table 对象。
  • 实例(Instance)

    WebAssembly.Module 的一个可执行实例,本身就具有状态,实例中有一个 exports 对象包含了所有 WebAssembly 的导出,可以使用 JavaScript 调用 WebAssembly 代码。

    创建方法:

  • 使用 WebAssembly.Instance() 构造函数以同步方式实例化一 WebAssembly.Module 对象。
  • 通过异步函数 WebAssembly.instantiate()
  • 注: 由于大型模块的实例化代价极高, 开发人员应只在必须同步实例化的时候,才使用 Instance() ,绝大多数情况应该使用异步方法 WebAssembly.instantiate()

    全局(Global)

    在前面我们知道了 WebAssembly 的一个特性模块性,但 WebAssembly 同样允许我们跨越多个模块共享内存信息,跨越一个或多个 WebAssembly.Module 实例,允许被多个modules动态连接。

    创建方法:

  • 使用 WebAssembly.Global 对象表示一个全局变量实例,该对象可以被 JavaScript 和 importable/exportable 访问。
  • 下面的方法除了 WebAssembly.validate() 都是返回的 Promise ,主要看一下 WebAssembly.instantiate() WebAssembly.instantiateStreaming() 两个方法,一般都是使用这两个方法加载 wbsm 模块

    WebAssembly.compile()

    WebAssembly.compile() 方法编译 WebAssembly 二进制代码到一个 WebAssembly.Module 对象。如果在实例化之前有必要去编译一个模块,那么这个方法是有用的(否则,将会使用 WebAssembly.instantiate() 方法)。

    WebAssembly.compileStreaming()

    WebAssembly.compileStreaming() 方法用来从一个流式源中直接编译一个 WebAssembly.Module 。当模块需要在被实例化前被编译时,这个方法会很有用。如果要从流式源实例化一个模块应采用 WebAssembly.instantiateStreaming() 方法。

    WebAssembly.validate()

    WebAssembly.validate() 方法用于验证包含 WebAssembly 二进制码的一个 typed array 是否合法,如果这些字节能构成一个合法的 wasm 模块则返回 true ,否则返回 false

    WebAssembly.instantiate()

    用于编译和实例化 WebAssembly 代码的主 API,返回一个 Module 和它的第一个 Instance 实例。这个方法有两个重载方式:

  • 第一种主要重载方式使用 WebAssembly 二进制代码的 typed array ArrayBuffer ,一并进行编译和实例化。返回的 Promise 会携带已编译的 WebAssembly.Module 和它的第一个实例化对象 WebAssembly.Instance
  • 第二种重载使用已编译的 WebAssembly.Module , 返回的 Promise 携带一个 Module 的实例化对象 Instance . 如果这个 Module 已经被编译了或者是从缓存中获取的, 那么这种重载方式是非常有用的。
  • 注: 此方法不是获取(fetch)和实例化wasm模块的最具效率方法。 如果可能的话,应该改用较新的 WebAssembly.instantiateStreaming() 方法,该方法直接从原始字节码中直接获取,编译和实例化模块,因此不需要转换为 ArrayBuffer

    WebAssembly.instantiateStreaming()

    直接从流式底层源编译和实例化 WebAssembly 模块,同时返回 Module 及其第一个 Instance 实例。

    下面是对上面两个方法做的封装:

    // 封装 WebAssembly 模块的读取函数
    export const wasmBrowserInstantiate = async (
      wasmModuleUrl,
      importObject
    ) => {
      let response
      // 传入的 importObject 需要提供 env.abort() 方法
      if (typeof importObject?.env?.abort !== 'function') {
        importObject = Object.assign({}, importObject, {
          env: {
            abort: () => console.log('Abort!')
      // 判断是否支持 streaming instantiation
      if (WebAssembly.instantiateStreaming) {
        // 请求模块然后初始化
        response = await WebAssembly.instantiateStreaming(
          fetch(wasmModuleUrl),
          importObject
      } else {
        // 不支持要 fallback,手动转成 Buffer
        const fetchAndInstantiateTask = async () => {
          const wasmArrayBuffer = await fetch(wasmModuleUrl).then((response) =>
            response.arrayBuffer()
          return WebAssembly.instantiate(wasmArrayBuffer, importObject)
        response = await fetchAndInstantiateTask()
      return response
    

    如何使用:

    import { wasmBrowserInstantiate } from './wbsm-util'
    // 解析后的 wasm 模块
    const wasmModule = await wasmBrowserInstantiate('www.xxx.com/index.wasm') 
    

    至于怎么具体使用wasm模块,下面会有详细介绍。

    AssemblyScript 的使用

    构建一个 AssemblyScript 的应用程序

    如果你不想要在本地构建项目,可以试试使用 WebAssembly Studio 在线构建

    对于前端开发者而言十分简单,使用npm就能下载相关依赖项:

    首先创建一个空项目,然后下载下面两个依赖项:

    npm install --save @assemblyscript/loader
    npm install --save-dev assemblyscript
    
    npx asinit .
    

    它会自动在我们项目中创建初始的推荐目录结构和配置文件用于开发:

    目录结构如下:

    asconfigasc命令的配置文件,用于生成对应的.wasm文件

    assembly目录就是我们实际的开发目录

    build目录为我们将 TypeScript 转换为 WebAssembly 之后的默认构建目录

    index.js文件用于引入我们编译后的.wasm文件然后使用相应的loader解析并给其余文件使用:

    // 可以看到,当前的环境为 node 环境,我们可以使用浏览器响应的 api 来读取 wasm
    const fs = require("fs");
    const loader = require("@assemblyscript/loader");
    // 用于在 WebAssembly 中使用 JavaScript 模块,后面会提到
    const imports = { /* imports go here */ };
    // 在这解析 Module
    const wasmModule = loader.instantiateSync(fs.readFileSync(__dirname + "/build/optimized.wasm"), imports);
    module.exports = wasmModule.exports;
    

    tests目录即测试目录,下面的文件会引入上面的index.js,用于测试编译后的模块,但是这种测试方式过于繁琐,社区内已经专门为 AssemblyScript 开发了对应基于jest的单元测试库,后续会进行说明,所以这个目录可以删掉。

    在浏览器中引入 wasm 模块

    现在打开assembly/index.ts,可以看到下面的代码:

    // The entry file of your WebAssembly module.
    export function add(a: i32, b: i32): i32 {
      return a + b;
    

    我们会看到一个不同于之前写TypeScript的类型i32,但实际上它的定义是这样的:

    /** A 32-bit signed integer. */
    declare type i32 = number;
    

    可以看到,他本身只是一个number的类型别名,在WebAseembly中代表的是一个 32 位的有符号整型,除此之外还有i64f32f64等这样单独代表 64 位整型和浮点型的类型,可以看出 webAseembly 同 Java 等一样将整形与浮点型的数值分开进行了声明。因为需要符合 WebAssembly 的相关特性,所以我们在类型的指定也不能同普通的 TypeScript 语法一样了。

    OK,我们先把它复制到assembly/Hello-World/index.ts文件夹下,再把它编译一下(先使用命令编译):

    npx asc assembly/Hello-World/index.ts -b build/Hello-World/index.wasm
    

    可以看到,在build目录下面生成了对应的hello-world目录和编译后的wasm文件,我们来把它们引入到浏览器中:

    // examples/01.hello-world.js
    // @assemblyscript/loader 可以在多个环境中使用
    import { instantiate } from '@assemblyscript/loader'
    const runWasmAdd = async () => {
      // 实例化 wasm module
      // 这里其实类似之前对于 wbsm api 的封装
      const wasmModule = await instantiate(fetch('../build/Hello-World/index.wasm')) // 需要传入 Promise 包装的文件或者直接传 Buffer
      // 调用 add 我们刚才写的函数
      const addResult = wasmModule.exports.add(24, 24)
      // 把值放入 body 中
      document.body.textContent = `Hello World! addResult: ${addResult}`
    runWasmAdd()
    
    <!DOCTYPE html>
        <meta charset="UTF-8" />
        <title>Hello World - AssemblyScript</title>
        <!-- 导入 -->
        <script type="module" src="./01.hello-world.js"></script>
      </head>
      <body></body>
    </html>
    

    wasm 模块的导出

    // assembly/Exports/index.ts
    // 按照正常情况导出,可以在导出函数内部使用未导出的函数
    export function callMeFromJavascript(a: i32, b: i32): i32 {
      return addIntegerWithConstant(a, b)
    // 导出一个常量
    export const GET_THIS_CONSTANT_FROM_JAVASCRIPT: i32 = 2424
    // 没有导出
    function addIntegerWithConstant(a: i32, b: i32): i32 {
      return a + b + ADD_CONSTANT
    // 没有导出
    const ADD_CONSTANT: i32 = 1
    
    npx asc assembly/Exports/index.ts -b build/Exports/index.wasm
    
    // examples/02.exports.js
    import { instantiate } from '@assemblyscript/loader'
    const runWasm = async () => {
      // Instantiate our wasm module
      const wasmModule = await instantiate(fetch('../build/Exports/index.wasm'))
      // 拿到模块实例对象上的 exports 属性
      const exports = wasmModule.instance.exports
      console.log(exports.callMeFromJavascript(24, 24)) // 49
      // 如果只打印变量不会出现值,而是一个 Global 对象,我们需要获取该对象的 value 属性才能拿到值
      console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT) // GLobal { value: 2424 }
      console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT.valueOf()) //  2424
      console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT.value) //  2424
      // 打印一个没有导出的函数
      console.log(exports.addIntegerWithConstant) // undefined
    runWasm()
    

    我们可以在控制台看到打印效果:

    与 JavaScript 共享内存

    线性内存是无符号字节的连续缓冲区,可以由 Wasm 和 JavaScript 读取和存储。 换句话说,Wasm 内存是 JavaScript 和 Wasm 可以同步读取和修改的字节的可扩展数组。 线性内存可以用于很多事情,其中之一是在 Wasm 和 JavaScript 之间来回传递值。

    // assembly/WebAssembly-Linear-Memory/index.ts
    // 创建 memory
    // 通过增加一页(64KB)的 Wasm Memory
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory#Examples
    memory.grow(1)
    // 通过 store 函数存储值,存储形式类似数组
    const index = 0
    const value = 24
    store<u8>(index, value)
    export function readWasmMemoryAndReturnIndexOne(): i32 {
      // 读取内存里的数据
      let valueAtIndexOne = load<u8>(1)
      // 返回索引为 1 的值(上面默认值设置里索引为 0 的值)
      return valueAtIndexOne
    
    npx asc assembly/WebAssembly-Linear-Memory/index.ts -b build/WebAssembly-Linear-Memory/index.wasm
    
    // examples/03.webassembly-linear-memory.js
    import { instantiate } from '@assemblyscript/loader'
    const runWasm = async () => {
      const wasmModule = await instantiate(
        fetch('../build/WebAssembly-Linear-Memory/index.wasm')
      const exports = wasmModule.instance.exports
      // 获取我们的 memory 对象,在 Wasm 中的内存都在这里面
      const memory = exports.memory
      // 创建 Uint8Array 来允许我们访问 Wasm Memory
      const wasmByteMemoryArray = new Uint8Array(memory.buffer)
      // 在 js 中访问
      console.log(wasmByteMemoryArray[0]) // 24
      // 在 js 中修改内存中的值
      wasmByteMemoryArray[1] = 25
      // 能够正确打印出索引为 1 的值,证明能够修改成功
      console.log(exports.readWasmMemoryAndReturnIndexOne()) // 25
    runWasm()
    

    在 WebAssembly 中导入 JavaScript 的函数

    这里我们说一下在上面构建一个 AssemblyScript 的应用程序中初始摸版提到的imports对象,该对象可以让我们在 Wasm 模块中注入 JavaScript 的变量或函数并在其中调用。

    注意: 编译的模块中,对于每一个导入的值都要有一个与其匹配的属性与之相对应,否则会抛出错误。

    // assembly/Importing-Javascript-Functions-Into-WebAssembly/index.ts
    // 定义一个 imports 对象的函数
    declare function consoleLog(arg0: i32): void
    // 直接调用该函数
    consoleLog(24)
    
    npx asc assembly/Importing-Javascript-Functions-Into-WebAssembly/index.ts -b build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm
    
    // examples/04-importing-javascript-functions-into-webAssembly.js
    import { instantiate } from '@assemblyscript/loader'
    const runWasm = async () => {
      const wasmModule = await instantiate(
        fetch(
          '../build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
        // 实例化的时候传入第二个参数 importObject
          // 模块名,AssemblyScript 编译时默认都是把所有导入值挂载到以文件名命名的模块中
          index: {
            consoleLog: (value) => console.log(value)
    runWasm()
    
    自定义导入名称

    我们可以使用@external装饰器修改我们要从外界导入进来的函数名称,@external装饰器可以传入一个或两个参数,如果传入一个参数则代表修改函数或变量名,如果传入两个参数则代表修改模块名和函数或变量名。

    // 定义一个 imports 对象的函数
    // @ts-ignore
    // 只修改函数名
    @external("consoleLog3")
    declare function consoleLog(arg0: i32): void
    // import consoleLog3 from index
    // @ts-ignore
    // 修改模块名和变量名
    @external("foo", "consoleLog4")
    declare function consoleLog2(arg0: i32): void
    // import consoleLog4 from foo
    consoleLog(24)
    consoleLog2(25)
    

    可以看出,即使是在同一个文件中,AssemblyScript 在编译时也可以将其单独分配在不同的模块中使用。

    import { instantiate } from '@assemblyscript/loader'
    const runWasm = async () => {
      const wasmModule = await instantiate(
        fetch(
          '../build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
          // 默认文件名
          index: {
            consoleLog3: (value) => console.log(value)
          // 我们自定义的模块名
          foo: {
            consoleLog4: (value) => console.log(value)
    runWasm()
    

    WebAssembly 非常适合计算密集型任务,甚至官方的 AssemblyScript 文档都涵盖了这一点。例如,涉及大数据,带有条件的繁重逻辑或嵌套循环之类的任务。 因此,通过将上述任务移至 WebAssembly 中,可以生成/渲染图形,从而显着提高速度。

    在下面的示例中,我们将每秒生成 20x20 个彩色棋盘图像,并使用ImageData对象上的Pixel Manipulation将它们显示在canvas上。 用图形术语来说,这是一个光栅图。

    // assembly/Reading-and-Writing-Graphics/index.ts	
    // 创建 memory
    memory.grow(1)
    // 定义光栅数量
    const CHECKERBOARD_SIZE: i32 = 20
    //创建一个缓冲区/指针(数组索引和大小),指向我们在内存中存储像素。
    // memoryBase,向外抛出内存的起始偏移量
    export const CHECKERBOARD_BUFFER_POINTER: i32 = 0
    // 需要占用内存的总值,最后乘以 4 是因为每个格子需要存 (r,g,b,a) 四个值
    export const CHECKERBOARD_BUFFER_SIZE: i32 =
      CHECKERBOARD_SIZE * CHECKERBOARD_SIZE * 4
    // 按像素逐步生成光栅,传入 rgb 的亮暗值
    export function generateCheckerBoard(
      darkValueRed: i32,
      darkValueGreen: i32,
      darkValueBlue: i32,
      lightValueRed: i32,
      lightValueGreen: i32,
      lightValueBlue: i32
    ): void {
      // 因为 WebAssembly 的线性内存是一个一维数组,所以我们需要做二维到一维的映射
      for (let x: i32 = 0; x < CHECKERBOARD_SIZE; x++) {
        for (let y: i32 = 0; y < CHECKERBOARD_SIZE; y++) {
          // 当前格子有亮色和暗色两种选择
          let isDarkSquare: boolean = true
          // 判断当前格子的亮按情况
          // 如果 y 是偶数则是亮色
          if (y % 2 === 0) {
            isDarkSquare = false
          // 如果 x 是偶数则亮度取反
          if (x % 2 === 0) {
            isDarkSquare = !isDarkSquare
          // 对当前格子的 rgb 赋值
          let squareValueRed = darkValueRed
          let squareValueGreen = darkValueGreen
          let squareValueBlue = darkValueBlue
          if (!isDarkSquare) {
            squareValueRed = lightValueRed
            squareValueGreen = lightValueGreen
            squareValueBlue = lightValueBlue
          // 通过二维到一维的映射计算索引
          let squareNumber = y * CHECKERBOARD_SIZE + x
          // 注意还要乘以 4,因为每个格子需要保存 4 个值(r,g.b.a)
          let squareRgbaIndex = squareNumber * 4
          // 通过 store 保存所有值在内存中
          store<u8>(
            CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 0,
            squareValueRed
          ) // Red
          store<u8>(
            CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 1,
            squareValueGreen
          ) // Green
          store<u8>(
            CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 2,
            squareValueBlue
          ) // Blue
          store<u8>(CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 3, 255) // Alpha (始终不透明)
    
    npx asc assembly/Reading-and-Writing-Graphics/index.ts -b build/Reading-and-Writing-Graphics/index.wasm
    
    // examples/05-reading-and-writing-graphics.js
    import { instantiate } from '@assemblyscript/loader'
    const runWasm = async () => {
      const wasmModule = await instantiate(
        fetch('../build/Reading-and-Writing-Graphics/index.wasm')
      const exports = wasmModule.instance.exports
      // 获取我们的 memory 对象,在 Wasm 中的内存都在这里面
      const memory = exports.memory
      // 创建 Uint8Array 来允许我们访问 Wasm Memory
      const wasmByteMemoryArray = new Uint8Array(memory.buffer)
      const canvasElement = document.querySelector('canvas')
      const canvasContext = canvasElement.getContext('2d')
      // 绘制宽高
      const canvasImageData = canvasContext.createImageData(
        canvasElement.width,
        canvasElement.height
      // 随机获取值
      const getDarkValue = () => {
        return Math.floor(Math.random() * 100)
      const getLightValue = () => {
        return Math.floor(Math.random() * 127) + 127
      const drawCheckerBoard = () => {
        exports.generateCheckerBoard(
          getDarkValue(),
          getDarkValue(),
          getDarkValue(),
          getLightValue(),
          getLightValue(),
          getLightValue()
        // 从内存中取出我们保存的光栅值,只取光栅开始到结束所占的内存空间
        const imageDataArray = wasmByteMemoryArray.slice(
          // 我们在 Wasm 中导出的值
          exports.CHECKERBOARD_BUFFER_POINTER.value,
          exports.CHECKERBOARD_BUFFER_SIZE.value
        // 设置 values
        canvasImageData.data.set(imageDataArray)
        // 清空 canvas
        canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height)
        // 绘制新的图形
        canvasContext.putImageData(canvasImageData, 0, 0)
      drawCheckerBoard()
      setInterval(() => {
        drawCheckerBoard()
      }, 1000)
    runWasm()
    
    <!DOCTYPE html>
        <meta charset="UTF-8" />
        <title>Reading-and-Writing-Graphics</title>
        <script type="module" src="./05-reading-and-writing-graphics.js"></script>
      </head>
        <canvas
          width="20"
          height="20"
          style="
            image-rendering: pixelated;
            image-rendering: crisp-edges;
            width: 100%;
        </canvas>
      </body>
    </html>
    

    这个例子只是为了演示如何在 AssemblyScript 中读写音频,但是生产环境其实并不会这样做,我们这样写也只是出于学习的目的。

    该示例会发出噪声,注意音量!!!

    同样的,可以在 WebAssembly 中生成/渲染音频样本,从而显着提高速度。

    在下面的示例中,我们将使用Web Audio API放大AudioBuffer中的音频样本。

    注意: 可以并且应该通过GainNode来完成此功能,但这主要是为了演示的目的。或者我们可以想象为实现不受支持的Web Audio API效果,例如bitcrusher或针对不受支持的浏览器的ogg解码器。

    // assembly/Reading-and-Writing-Audio/index.ts
    memory.grow(1)
    // 在 JavaScript 中写入到 INPUT_BUFFER
    export const INPUT_BUFFER_POINTER: i32 = 0
    export const INPUT_BUFFER_SIZE: i32 = 1024
    // 在 Wasm 中写入结果到 OUTPUT_BUFFER
    export const OUTPUT_BUFFER_POINTER: i32 =
      INPUT_BUFFER_POINTER + INPUT_BUFFER_SIZE
    export const OUTPUT_BUFFER_SIZE: i32 = INPUT_BUFFER_SIZE
    // 放大音频
    export function amplifyAudioInBuffer(): void {
      for (let i = 0; i < INPUT_BUFFER_SIZE; i++) {
        // 加载指定所以的 audioSample
        let audioSample: u8 = load<u8>(INPUT_BUFFER_POINTER + i)
        // 放大 audioSample
        // 以 127 为界限(127 以下为负,127 以上为正),0 为负的最大,256 为正最大
        if (audioSample > 127) {
          let audioSampleDiff = audioSample - 127
          audioSample = audioSample + audioSampleDiff
        } else if (audioSample < 127) {
          audioSample = audioSample / 2
        // 保存转换后的 audioSample 到 OUTPUT_BUFFER
        store<u8>(OUTPUT_BUFFER_POINTER + i, audioSample)
    
    npx asc assembly/Reading-and-Writing-Audio/index.ts -b build/Reading-and-Writing-Audio/index.wasm
    
    // examples/06-reading-and-writing-audio.js
    import { instantiate } from '@assemblyscript/loader'
    // 创建音频上下文
    const audioContext = new (window.AudioContext || window.webkitAudioContext)()
    // buffer 中的样品帧数
    const numberOfSamples = 1024
    // 用 audioContext 的采样率创建一个空的音频片段
    const audioBuffer = audioContext.createBuffer(
      // 2 个声频通道,代表立体声,1 代表是单声道的
      // 在频率为 audioContext.sampleRate 的音频环境中播放会持续 numberOfSamples / audioContext.sampleRate 秒
      numberOfSamples,
      audioContext.sampleRate
    // 创建原始样本 buffer 和放大样本 buffer 区域
    const originalAudioSamples = new Float32Array(numberOfSamples)
    const amplifiedAudioSamples = new Float32Array(numberOfSamples)
    // 将浮点样本转换为字节样本
    const floatSamplesToByteSamples = (floatSamples) => {
      const byteSamples = new Uint8Array(floatSamples.length)
      // 以 127 为界限(127 以下为负,127 以上为正),0 为负的最大,256 为正最大
      for (let i = 0; i < floatSamples.length; i++) {
        const diff = floatSamples[i] * 127
        byteSamples[i] = 127 + diff
      return byteSamples
    // 将字节样本转换为浮点样本
    const byteSamplesToFloatSamples = (byteSamples) => {
      const floatSamples = new Float32Array(byteSamples.length)
      // 以 127 为界限(127 以下为负,127 以上为正),0 为负的最大,256 为正最大
      for (let i = 0; i < byteSamples.length; i++) {
        const byteSample = byteSamples[i]
        const floatSample = (byteSample - 127) / 127
        floatSamples[i] = floatSample
      return floatSamples
    const runWasm = async () => {
      const wasmModule = await instantiate(
        fetch('../build/Reading-and-Writing-Audio/index.wasm')
      const exports = wasmModule.instance.exports
      const memory = exports.memory
      const wasmByteMemoryArray = new Uint8Array(memory.buffer)
      // 生成 1024 个浮点音频样本
      // 我们的浮点样本值为 0.3
      const sampleValue = 0.3
      for (let i = 0; i < numberOfSamples; i++) {
        if (i < numberOfSamples / 2) {
          originalAudioSamples[i] = sampleValue
        } else {
          originalAudioSamples[i] = sampleValue * -1
      // 转换后的原始字节样本
      const originalByteAudioSamples = floatSamplesToByteSamples(
        originalAudioSamples
      // 用转换后的音频样本填充 wasm 内存,保存在 INPUT_BUFFER_POINTER 索引中
      wasmByteMemoryArray.set(
        originalByteAudioSamples,
        exports.INPUT_BUFFER_POINTER.value
      // 执行放大音频的函数
      exports.amplifyAudioInBuffer()
      // 获取放大后的音频 buffer
      const outputBuffer = wasmByteMemoryArray.slice(
        exports.OUTPUT_BUFFER_POINTER.value,
        exports.OUTPUT_BUFFER_POINTER.value + exports.OUTPUT_BUFFER_SIZE.value
      // 将放大后的字节样本重新转化为浮点样本
      const outputFloatAudioSamples = byteSamplesToFloatSamples(outputBuffer)
      // 将 outputFloatAudioSamples 设置到 amplifiedAudioSamples 中
      amplifiedAudioSamples.set(outputFloatAudioSamples)
    runWasm()
    function beforePlay() {
      // 检查上下文是否处于挂起状态(自动播放)
      if (audioContext.state === 'suspended') {
        audioContext.resume()
    // 全局 audioBufferSource 对象
    let audioBufferSource
    function stopAudioBufferSource() {
      // 如果 audioBufferSource 有值就暂停并赋值为空
      if (audioBufferSource) {
        audioBufferSource.stop()
        audioBufferSource = undefined
    function createAndStartAudioBufferSource() {
      // 先停止以前的
      stopAudioBufferSource()
      audioBufferSource = audioContext.createBufferSource()
      // audioBuffer 是我们之前创建的空的音频片段
      audioBufferSource.buffer = audioBuffer
      // 循环播放
      audioBufferSource.loop = true
      // 连接源和输出并启动
      audioBufferSource.connect(audioContext.destination)
      audioBufferSource.start()
    // UI
    const playBtn = document.getElementById('play-btn')
    const amplifiedBtn = document.getElementById('amplified-btn')
    const pauseBtn = document.getElementById('pause-btn')
    playBtn.addEventListener('click', playAmplified)
    amplifiedBtn.addEventListener('click', playOriginal)
    pauseBtn.addEventListener('click', pause)
    function playOriginal() {
      beforePlay()
      // 原始音频
      // 将浮动音频样本设置为左声道和右声道
      audioBuffer.getChannelData(0).set(originalAudioSamples)
      audioBuffer.getChannelData(1).set(originalAudioSamples)
      createAndStartAudioBufferSource()
    function playAmplified() {
      beforePlay()
      // 放大音频
      // 将浮动音频样本设置为左声道和右声道
      audioBuffer.getChannelData(0).set(amplifiedAudioSamples)
      audioBuffer.getChannelData(1).set(amplifiedAudioSamples)
      createAndStartAudioBufferSource()
    function pause() {
      beforePlay()
      stopAudioBufferSource()
    
    <!DOCTYPE html>
        <meta charset="UTF-8" />
        <title>Reading-and-Writing-Audio</title>
        <script type="module" src="./06-reading-and-writing-audio.js"></script>
      </head>
        <h1>NOTE: Be careful if using headphones</h1>
        <h1>Original Sine Wave</h1>
        <div><button id="play-btn">Play</button></div>
        <h1>Amplified Sine Wave</h1>
          <button id="amplified-btn">Play</button>
        </div>
        <h1>Pause</h1>
        <div><button id="pause-btn">Pause</button></div>
      </body>
    </html>
    

    与 TypeScript 的不同

    因为之前并没有太多用到下面这些特性,所以我决定先从例子入手了解一下 AssemblyScript 的使用,再说一下和 TypeScript 的不同点。

    之前我们有提到过,AssemblyScript 是编译为 WebAssembly 的,而 TypeScript 是编译为 JavaScript,在这两点上就有着本质区别,AssemblyScript 会更像 C、C++ 或 Rust 这些语言,所以 WebAssembly 有着更加严格的静态类型约束。

    更加约束的静态类型

    编写 AssemblyScript 时首先要注意的一点是,它的基本类型与 TypeScript 的基本类型有所不同,因为它使用 WebAssembly 的更特定的整数和浮点类型,而 JavaScript 的数字只是 WebAssembly 的f64的别名。

    JavaScript JIT 会尝试在代码执行时找出数字值的最佳表示形式,并假设数值会改变类型,并可能多次重新编译代码,而 AssemblyScript 则允许开发人员提前及其所有优点和优点指定理想的类型。

    没有 any 和 undefined

    // bad
    function foo(a?) {
      const b = a + 1
      return b
    // good
    function foo(a: i32 = 0): i32 {
      const b = a + 1
      return b
    

    没有联合类型

    // bad
    function foo(a: i32 | string): void {
    // good
    function foo<T>(a: T): void {
    

    严格类型的对象

    相比起 TypeScript,AssemblyScript 更加的偏向静态,在对于对象创建来说,我们不能直接使用对象字面量声明,而是需要使用new Map()这样的形式创建对象。

    // bad
    const obj1 = {};
    obj.a = 1;
    // good
    const obj2 = new Map();
    obj2.set('a', 1);
    

    不同的全等于(===)

    ===运算符的特殊语义(当值和类型都匹配时才为true)在 AssemblyScript 中没有多大意义,因为比较两个不兼容类型的值无论如何都是非法的。 因此,===运算符已被重新用于执行身份比较,如果两个操作数都是相同的对象,则评估为true

    const a = "hello"
    const b = a
    const c = "h" + a.substring(1)
    if (a === b) { /* true */ }
    if (a === c) { /* false */ }
    if (a == c) { /* true */ }
    

    对于 Null 值的检查

    像 TypeScript 一样,AssemblyScript 编译器可以检查传递的值是否可为空:

    function doSomething(something: string | null): void {
      if (something) {
        something.length // works
    

    但是,这不适用于某个对象的属性,因为它们的值可以在检查和使用值之间改变:

    class Foo {
      something: string | null = null;
    function doSomething(foo: Foo): void {
      if (foo.something) {
        // ... some code ...
        foo.something.length // fails
    

    如果我们在运行时将foo.something设置为了null ,再次访问时 TypeScript 会报运行时错误。

    class Foo {
      something: string | null = null;
    function doSomething(foo: Foo) {
      if (foo.something) {
        doSomethingElse(foo);
        foo.something.length; // should error, but doesn't
    function doSomethingElse(foo: Foo) {
      foo.something = null;
    

    但是,AssemblyScript 中不会出现这样的运行时错误,因此,为了安全起见,必须使用本地变量:

    function doSomething(foo: Foo): void {
      let something = foo.something
      if (something) {	
        something.length // works
    

    对于单元测试,我们使用社区提供的 as-pect

    集成在项目中也很简单,我们只需要在之前创建好的项目中安装as-pect即可

    npm install --save-dev @as-pect/cli
    

    然后运行下面的命令:

    npx asp --init
    

    脚手架会自动在我们的项目中创建对应的初始化文件:

    package.json中添加脚本:

    "scripts": { "test": "asp --verbose", "test:ci": "asp --summary"

    默认使用根目录下的as-pect.config.js文件作为配置文件,也可以使用--config主动指定配置文件。具体配置和使用可以查看其官方文档,这里就不多赘述了。

    做一个总结,本文先从浏览器中的 WebAssembly Api 开始介绍,讲了其相关特性和方法,大致讲述了如何在浏览器中引入 WebAssembly 模块。然后使用了几个例子介绍了如何在开发中集成与使用 AssemScript ,并比较了其与传统 TypeScript 的不同点,因为编译后的产物不同,我们需要有不一样的注意点。最后再简单地介绍了一下 AssemblyScript 的单元测试工具的使用。

    总的来说,AssemblyScript 很大程度上帮助前端开发人员深入接触到了 WebAssembly 的世界,我们可以在进行大量运算的时候转入 AssemblyScript 来生成wasm模块,并且能够很轻松地投入到生产中。

    最后,作者技术有限,如果有什么错误或遗漏的地方还请帮忙指出,我会在第一时间修改。

    本文的所有代码已上传到 github

  • The AssemblyScript Book
  • Wasm By Example
  • as-pect
  • MDN - WebAssembly
  • 初识 WebAssembly
  • 分类:
    前端
  •