Vue.js + WebAssembly(Go)を触ってみた

はじめに

こんにちは。20年度新卒の今枝と申します。

気になっていた WebAssembly を業務ツールアプリで使ってみたので、そのときに利用した Vue.js + WebAssembly(Go) のお話をしようと思います。
今回は Go のコードで書かれた関数を、ビルドして WebAssembly として、Vue.js 上で動作させるサンプルアプリを作成します。

今回使う技術について

今回扱う技術について簡単に説明をします。

WebAssembly (WASM)

WebAssembly は、モダンなブラウザーで実行することができるバイナリー形式の言語です。 C言語、Rust や、今回扱う Go のような言語からコンパイルして生成することができます。 WebAssembly は WASM と省略して呼ばれることが多く、本記事でも WASM と記述します。 Webアプリケーションで動作している JavaScript と並行して動作するので、WebアプリケーションからGoで記述された関数を WASM 形式にコンパイルすることで呼び出すことができます。 Vue.js のような単一コンポーネントファイルを利用して、Go + WASM で Webアプリケーションを作ることのできる Vugu というライブラリも存在します。
(今回Vuguは、扱いません。)

Swift(SwiftUI) + WASM で Webアプリケーションを作ることできる Tokamak についてはこちらで解説しています。気になる方は是非ご覧ください。

Go

Goは、Googleによって開発されたオープンソースの静的型付け言語です。
言語の仕様がシンプル、高速、メモリ効率が良い、並行処理を簡単に行うことができるメリットがあり、弊社内の様々なプロジェクトでもGoが採用されています。
Goは、1.11 以上から WASM のビルドに対応していますが、現在は生成する WASM 形式のファイルサイズに問題があるようです。
公式のGoコンパイラ が生成する WASM ファイルは、最小でも約2MB,ライブラリを使用した場合は、10MB 以上になってしまうことが公式のWikiにも記載されています。 しかし、この問題を解消するための改善策が公式のWikiに記載されていたので、改善策の1つである TinyGo という Go のコンパイラを使用します。

Vue

Evan You氏が中心となって開発されているクライアントサイド JavaScript フレームワークです。 Vue.jsでは、コンポーネント指向でアプリケーションなどの開発を行うことが可能です。
最近(2020年9月18日)に、Vue.js の新バージョンである Version 3 が正式リリースされて話題になりました!!
弊社内の様々なプロジェクトでも Vue.js が採用されています。
今回は、Vue.js アプリケーションのための 状態管理パターン + ライブラリである Vuex も使用します。

Vue.js から Go で実装された関数を呼び出そう

早速、Vue.js から Go で実装された関数を呼びだすサンプルのアプリを作っていきます。

環境

  • npm:6.14.4
  • node:v14.1.0
  • yarn:1.22.4
  • Vue-cli:4.5.8
  • docker を動作させる環境

まず、Go の関数を作成していきます。

Go でWASMファイルを出力する

TinyGo の公式ページを参考にして、関数を定義していきます。 今回は、サンプルと同様に足し算をする関数を用意します。

// main.go
package main

func main() {
}

//export add
func add(a, b int) int {
    return a + b
}

//export export-name と記述することで、Go の関数を Export して、JavaScript から Export された関数を呼び出すことができるようになります。

次に、TinyGo の docker Image を使用して、wasm ファイルを作成します。

docker run -v ~/go:/go -e "GOPATH=/go" tinygo/tinygo:0.15.0 tinygo build -o /go/src/TechBlog/wasm.wasm -target wasm TechBlog

ボリュームのマウント先は、自分の環境に合わせて行ってください。

出力されたwasm.wasm ファイルのサイズは約 7.5 KB となりました。
公式のGoコンパイラから出力されるWASMのファイルサイズよりは十分に小さいことが分かります。

f:id:optim-tech:20201027165254p:plain

Vue からGoの関数を呼び出す

構成図は、下記のようになります。 f:id:optim-tech:20201026112947p:plain

VueCLIでプロジェクトを作成します。

vue create wasm-example

Vue CLI v4.5.8
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue vers
ion, Babel, Vuex, Linter
? Choose a version of Vue.js that you want to start the proje
ct with 2.x
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.?
 In dedicated config files
? Save this as a preset for future projects? No

今回は、Vuexの Actions からWASMの関数を呼び出すので、 Vuex のライブラリにチェックを入れます。

次に、WASM ファイルを呼び出すために必要な wasm_exec.js ファイルを用意します。

curl -sO https://raw.githubusercontent.com/tinygo-org/tinygo/master/targets/wasm_exec.js

これらを、main.wasm と、 wasm_exec.js ファイルをpublic配下に移動します。

|--public
| |--favicon.ico
| |--index.html
| |--main.wasm
| |--wasm_exec.js

次に、DevServer に WASM の MIMEtype を登録します。
今回は、DevServer でのみ動作確認をしますが、 本番環境で使用する場合もWASMのMIMEtypeを忘れないように気をつけてください。

//  wasm-example/vue.config.js
const path = require("path");
const contentBase = path.resolve(__dirname);

module.exports = {
  configureWebpack: config => {
    config.devServer = {
      before(app) {
        // wasm file に MINE Type を付与
        app.get("*.wasm", function(req, res, next) {
          var options = {
            root: contentBase + "/public",
            dotfiles: "deny",
            headers: {
              "Content-Type": "application/wasm"
            }
          };
          res.sendFile(req.url, options, function(err) {
            if (err) {
              next(err);
            }
          });
        });
      }
    };
  }
};

index.html に wasm_exec.js で読み込む処理を記述します。

<!-- wasm-example/public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <script src="wasm_exec.js"></script>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

最後に、Vuex で WASM ファイルを読み込み、Go の関数を呼び出してあげれば完成です。

// wasm-example/src/store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const WASM_URL = "/wasm.wasm";

export default new Vuex.Store({
  state: {
    wasm: null
  },
  mutations: {},
  actions: {
    async wasmInit({ state }) {
      const go = new global.Go();
      if ("instantiateStreaming" in WebAssembly) {
        WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(
          obj => {
            state.wasm = obj.instance;
          }
        );
      } else {
        fetch(WASM_URL)
          .then(resp => resp.arrayBuffer())
          .then(bytes =>
            WebAssembly.instantiate(bytes, go.importObject).then(obj => {
              state.wasm = obj.instance;
            })
          );
      }
    },
    add({ state }, payload) {
      return state.wasm.exports.add(payload.a, payload.b);
    }
  },
});

確認

任意のコンポーネントの中で、ActionsのwasmInit()を呼び出した後に、add 関数が実行できることを確認できたら成功です。

yarn serve

上記のコマンドで Dev Server を起動することができます。

おわりに

Go で書かれた既存の実装を JavaScript で書き直さなくても、既存の Webアプリケーションに取り込めることが分かりました。 TinyGo では、サポートされていないパッケージもあるので事前に確認が必要です。使用可能なパッケージ
また、TinyGo で生成したWASM とJavaScript間で文字列を渡し合うのは大変みたいです。 https://alcarney.me/blog/2020/passing-strings-between-tinygo-wasm/alcarney.me

Goの標準のコンパイラでWASMを生成した場合は、文字列の受け渡しも容易にできます。( WASMファイルが大きくなってしまいますが )
いろいろな制限はありますが、既存のGoの実装をJavaScriptで再実装せずにWebフロントで動かすことができるのは嬉しいですね。

OPTiMでは、ものづくりが大好きなエンジニアのみなさんを募集しています!

www.optim.co.jp