はじめに
こんにちは。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のファイルサイズよりは十分に小さいことが分かります。
Vue からGoの関数を呼び出す
構成図は、下記のようになります。
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では、ものづくりが大好きなエンジニアのみなさんを募集しています!