如何在Flutter Web中开始使用WebAssembly(详细教程)

1,669 阅读6分钟

Flutter是用Skia构建的,这是一个用C++编写的2D图形库。Flutter的Skia引擎使其能够在各种平台上创建可移植的、性能良好的应用程序,包括网络。

大多数网络浏览器理解JavaScript语言;然而,Skia C++ API可以通过WebAssembly在网络浏览器上运行。WebAssembly通过将本地代码编译成浏览器能够理解的语言,使其能够在浏览器中运行。

在本教程中,我们将介绍WebAssembly并了解其优点。我们还将学习Flutter代码如何与WebAssembly绑定进行交互。

什么是WebAssembly?

WebAssembly(Wasm)是一种低级语言,以接近原生的性能运行。它是一种虚拟堆栈机,具有紧凑的二进制格式,旨在成为高级语言的编译目标。

WebAssembly使工程师能够用C、C++、Rust和其他高级语言编写代码,在浏览器中运行。Wasm把用高级语言编写的代码编译成WebAssembly模块。然后,它们被加载到一个网络应用程序中,并用JavaScript调用。

浏览器可以迅速将WebAssembly模块变成任何机器的指令:

Webassembly Compiling Chart

我们为什么要使用WebAssembly?

JavaScript一直是浏览器理解的主要语言。然而,当运行资源密集型的应用程序,如3D游戏时,JavaScript往往会很迟钝。对于这样的应用程序,需要一个接近原生的体验。这就是Wasm出现的地方。

WebAssembly与JavaScript一起工作,为我们的应用程序提供一个接近原生的速度。由于模块的小尺寸,Wasm的加载和执行速度更快,在网络上实现高性能的应用程序。

Wasm使我们能够建立快速、高性能、可移植和内存安全的应用程序。它是一个开放的标准,旨在运行在其他平台上,而不仅仅是网络。许多流行的语言至少对WebAssembly有一些支持。

Dart和WebAssembly的互操作性

Dart网络平台使Dart代码可以在由JavaScript驱动的平台上编译和运行。我们也可以在我们的Dart代码中调用现有的JavaScript代码,这是由包提供的JavaScript绑定所实现的。 js包提供的JavaScript绑定。

从Dart代码中调用JavaScript代码和从JavaScript代码中调用Dart代码的能力被称为 "Dart-JavaScript互操作性":

Dart Js Interoperability

js 包提供注解和函数,让我们指定我们的Dart代码如何与JavaScript代码连接。JavaScript API有WebAssembly 对象,这是所有与WebAssembly有关的函数的命名空间,它允许加载WebAssembly模块,创建新的内存和表实例,并处理WebAssembly错误。

WebAssembly有两种文件格式:

  • .wasm:包含二进制的汇编代码,是可执行文件。
  • .wat: 包含人类可读的文本格式的.wasm 文件,并编译为.wasm 。它只用于编辑或调试。

编写WebAssembly代码可能是很痛苦的。大多数语言支持从我们的源代码中生成Wasm模块,然后我们可以使用提供的绑定来加载和调用。

我们可以通过Dart-web代码中的JavaScript WebAssembly对象,使用js 绑定,与WebAssembly一起工作。

Dart Web Code Chart

为了在我们的Dart代码中利用js 绑定,用@JS 注释方法,并在其中添加external 关键字:

@JS('WebAssembly.instantiate')
external Object instantiate(Object bytesOrBuffer, Object import);

在Flutter网络应用中使用WebAssembly

我们可以使用各种语言来创建Wasm模块,并加载到我们的Flutter应用程序中。在这篇文章中,我们将使用AssemblyScript,一种类似TypeScript的WebAssembly语言,来生成Wasm模块。

使用AssemblyScript生成WebAssembly模块

为了开始,我们需要安装Node.js。你可以从Node的官方网站下载Node。

接下来,使用下面的命令安装npx ,一个npm包运行器:

npm i -g npx

创建一个新的目录和一个package.json 文件。然后使用下面的命令安装assemblyscriptassemblyscript/loader

mkdir wasm && cd wasm
npm init
npm i --save-dev assemblyscript
npm i --save @assemblyscript/loader

接下来,运行下面的命令来构建一个新的项目:

npx asinit .

该命令将生成assemblybuild 文件夹。我们将在index.ts 文件中编写我们的AssemblyScript模块,并将生成的Wasm代码放在build 文件夹中:

Folders Screenshot

接下来,将下面的方法添加到index.ts 文件中。plusOne 函数在计数器上加一,而minusOne 函数从计数器上减一:

// The entry file of your WebAssembly module.
export function plusOne(n: i32): i32 {
  return n+1;
}
export function minusOne(n:i32):i32{
  return n - 1;
}

通过在根目录下运行npm run asbuild ,生成WebAssembly模块。这个命令会在build 文件夹中生成.wasm.wat 文件。我们将在我们的Flutter应用程序中利用release.wasm 文件:

Generated Wasm Files

在Flutter应用程序中使用WebAssembly模块

为了使用生成的Wasm模块,我们将把release.wasm 文件作为一个资产添加到我们的Flutter应用程序。我们还将使用wasm_interop包,它为我们处理JavaScript WebAssembly绑定,使我们能够通过调用暴露的方法与WebAssembly进行交互。

首先,使用flutter create . 命令在wasm 文件夹中创建一个新的Flutter应用程序。然后,创建一个新的assets/wasm 文件夹并添加生成的release.wasm 文件。更新pubspec.yaml 文件以包括assets文件夹和wasm_interop 包:

dependencies:
  wasm_interop: ^2.0.1
flutter:
  assets:
    - assets/wasm/

运行flutter pub get ,添加依赖项。

更新main.dart 文件中的MyHomePage widget,如下所示:

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
  }
  void _decrementCounter() {
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You current count is:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            const SizedBox(
              height: 100,
            ),
            Wrap(
              spacing: 100,
              children: [
                ElevatedButton(
                    onPressed: _incrementCounter,
                    child: const Text('Increment')),
                ElevatedButton(
                    onPressed: _decrementCounter,
                    child: const Text('Decrement'))
              ],
            )
          ],
        ),
      ),
    );
  }
}

在根目录下运行flutter run -d chrome ,在Chrome上提供应用程序。我们的应用程序有增量减量按钮,将与我们的Wasm函数挂钩。

Initial App State Ui

创建一个新的wasm_loader.dart 文件并添加WasmLoader 类。WasmLoader 类包含我们的Dart到Wasm的互操作逻辑:

import 'package:flutter/services.dart' show rootBundle;
import 'package:wasm_interop/wasm_interop.dart';

class WasmLoader {
  WasmLoader({required this.path});
  late Instance? _wasmInstance;
  final String path;
  Future<bool> initialized() async {
    try {
      final bytes = await rootBundle.load(path);
      _wasmInstance = await Instance.fromBufferAsync(bytes.buffer);
      return isLoaded;
    } catch (exc) {
      // ignore: avoid_print
      print('Error on wasm init ${exc.toString()}');
    }
    return false;
  }
  bool get isLoaded => _wasmInstance != null;
  Object callfunction(String name, int input) {
    final func = _wasmInstance?.functions[name];
    return func?.call(input);
  }
}

上面的代码片段做了以下事情:

  • 在类的构造函数中期待一个Wasm模块路径
  • initialized 方法中加载Wasm模块文件
  • 使用异步的Instance.fromBufferAsync 方法编译和实例化Wasm代码。这个方法利用了 [WebAssembly.instantiate()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate)JavaScript API
  • 如果Wasm代码被成功初始化,在initialized 方法中返回isLoaded 状态
  • 添加一个callfunction 方法,期望一个函数名称和参数,然后对该函数进行调用

最后,更新main.dart 文件中的MyHomePage widget,以利用WasmLoader

  late WasmLoader loader;
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    loader = WasmLoader(path: 'assets/wasm/release.wasm');
    final isLoaded = await loader.initialized();
    if (isLoaded) {
      setState(() {});
    }
  }

  void _incrementCounter() {
    _counter = loader.callfunction('plusOne', _counter) as int;
    setState(() {});
  }

  void _decrementCounter() {
    _counter = loader.callfunction('minusOne', _counter) as int;
    setState(() {});
  }

上面的代码片断做了以下工作:

  • 用我们的.wasm 文件的路径创建一个WasmLoader 的实例
  • 初始化WasmLoader ,并在初始化后更新应用程序的状态
  • 用我们Wasm模块中调用plusOneminusOne 函数的结果更新_counter 属性

重新运行应用程序,点击增量减量按钮--计数器相应地更新。你成功地在你的Flutter应用程序中使用了WebAssembly代码!

总结

在本教程中,我们讨论了WebAssembly,并研究了它在提高应用程序性能方面的一些好处。我们还研究了Flutter如何与JavaScript绑定进行交互。最后,我们使用AssemblyScript生成WebAssembly模块,并将其挂在我们的Flutter网络应用中。

有了这些,你现在可以在你的Flutter Web应用程序中使用WebAssembly,并提高其性能。本文中的所有代码都可以在GitHub上找到。

我希望你喜欢这个教程