今回はWebAssemblyについて試してみました。以下目次で進めて行きます。
WebAssembyとは
WebAssemblyは、ブラウザーで実行できるバイナリコードです。WebではJavaScriptが必須ですが、リッチな処理が実現出来るようになるにつれ性能が不足するシーンが増えてきています。この問題解決のために実行が高速になるよう設計され誕生したのがWebAssemblyです。JavaScript以外の言語からコンパイルされていることを前提としており、調査したところ代表的な言語としてはC、C++、Rustです。ただし、WebAssemblyをロードして、JavaScriptから呼出すのでJavaScriptの代替えというわけではないことには留意しておきましょう。
WebアプリケーションにおけるWebAssemblyイメージ
Goでは1.11以降のVersionであれば、コードをWebAssemblyとしてコンパイル出来る機能が試験的ではありますがリリースされているため、今回はGoから試してみたいと思います。
Go 1.11 added an experimental port to WebAssembly. Go 1.12 has improved some parts of it, with further improvements expected in Go 1.13.
今回のコード
こちらに公開しております。
github.com
作成した画面
console.log
HTML書き換え
alertの表示
text-boxの入力値を別のtext-boxへ
前提条件
Goのコードを書いていく
console.log
に出力するためのGo側のコードを書いていきます。
// main.go package main import ( "fmt" "syscall/js" ) // documentオブジェクト取得用 var document = js.Global().Get("document") // bodyのDOM取得 var body = document.Get("body") func main() { // goからbuttonのDOMを作成する cLogBtn := createElement("button") // cLogBtnボタンのテキストを設定 cLogBtn.Set("textContent", "console log!!") // buttonをbodyへ追加 body.Call("appendChild", cLogBtn) // cLogBtnにclickのEventLisnerを設定 cLogBtn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} { fmt.Println("Hello Webassembly!") return nil })) // プログラムが終了しないように待機 select {} } // 使用頻度が高そうなので、対象DOMを作成する関数を用意 func createElement(elementName string) js.Value { return document.Call("createElement", elementName) }
HTMLの作成
HTMLを作成してWebAssembly実行に必要なコードを記述します。console.log
出力用のボタンはGoから作成していますのでHTMLには記述なしです。
<!-- index.html --> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>wasam-demo</title> <script src="./wasm_exec.js"></script> <script> (async () => { const go = new Go(); const { instance } = await WebAssembly.instantiateStreaming( fetch('main.wasm'), go.importObject ); await go.run(instance); })(); async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate() } </script> </head> <body> <div id="message">Hello, World</div> </body> </html>
WebAssemblyへコンパイルする
ここまで来たら、Goのプログラムをコンパイルします。以下のコマンドを打ちます。
❯❯❯ GOOS=js GOARCH=wasm go build -o main.wasm
実行するとmain.wasm
というファイルが生成されます。macOSでWindowsやLinux向けのバイナリをクロスコンパイル出来るのでGOOS
とGOARCH
の環境変数を変更すれば他OSの実行環境へのコンパイルも可能です。ここではJavaScriptの実行環境をOSとし、WebAssemblyとしてコンパイルしています。
GOROOTからwasm_exec.js
をコピーしてくる
ブラウザで実行するためにwasm_exec.js
をコピーしてきます。wasm_exec.js
はGoから JavaScriptへリンクするための実行命令が含まれているスクリプトになります。では以下のコマンドを打ってコピーしてきます。
❯❯❯ cp $GOROOT/misc/wasm/wasm_exec.js .
HTTPサーバーを実行し、確認する
ローカル環境で動作させるために以下をgo get
してから実行していきます。
❯❯❯ go get https://github.com/mattn/serve
❯❯❯ serve -a :8080
これでhttp://localhost:8080/
にアクセスすると画面の確認が出来ます。読み込み時はDOMの生成をGoで行っているので少し遅れてボタン表示がされます。
次は表示されているHTMLのテキストを書き換えていきます。
HTMLのテキストを書き換え
main.go
に以下を追記していきます。func main()
に追記。
// buttonDOMを作成する textChangeBtn := createElement("button") // textChangeBtnのテキストを設定 textChangeBtn.Set("textContent", "text change!!") // buttonをbodyへ追加 body.Call("appendChild", textChangeBtn) // textChangeにclick時のEventLisnerを設定 textChangeBtn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} { message := getElementByID("message") message.Set("innerHTML", "Hello, WebAssembry!!") return nil }))
func main
の外に以下を追記
// 使用頻度が高そうなので、対象のDOMのIDを取得する関数を用意 func getElementByID(targetID string) js.Value { return document.Call("getElementById", targetID) }
今回もDOMをGoから作成しているのでHTML側の修正は行いませんが、Go側を修正したのでコンパイルした後、HTTPサーバーを実行します。
❯❯❯ GOOS=js GOARCH=wasm go build -o main.wasm ❯❯❯ serve -a :8080
※以降Go側の修正があり、再実行時はこの手順を実施します
画面から確認するとテキストの変更が出来ることが確認出来ました!
alertのダイアログを表示させる
main.go
のグローバル変数として以下を追記します。
// windowオブジェクトを取得 var window = js.Global()
main.go
のfunc main
内に以下を追記します。
// buttonのDOMを生成 alertBtn := createElement("button") // alertBtnのテキストを設定 alertBtn.Set("textContent", "alert!!") // buttonをbodyへ追加 body.Call("appendChild", alertBtn) // alertBtnにclick時のEventLisnerを設定 alertBtn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} { window.Call("alert", "Hello!!") return nil }))
コンパイルした後、HTTPサーバーを実行して画面を確認していきます。無事alert
のダイアログが表示されました!
text-boxの入力値を別のtext-boxへ反映させる
main.go
に以下を追記していきます。func main()
に追記。
// textエリアの入力値を取得 getElementByID("in").Call("addEventListener", "keyup", js.FuncOf(func(js.Value, []js.Value) interface{} { getElementByID("out").Set("value", getElementByID("in").Get("value")) return nil }))
index.html
のbody
タグ内に以下を追加します。
<div><input type="text" id="in"></div> <div><input type="text" id="out"></div>
コンパイルした後、HTTPサーバーを実行して画面を確認します。こちらも問題なくDOM操作が出来ました。
躓いた点
ビルドが出来ない問題
main.go
にimportしているsyscall/js
を認識してくれずビルドが不可な状態になりました。調査したところ以下で同じエラーが再現しており、私も同じようにGoLand上で設定を変更したところ動作するようになりました。
Go × WebAssemblyで電卓のWebアプリを作ってみた - Sansan Builders Box
GoLandのBuild設定
js.NewCallback
には変更がある
js.NewCallback
という記述をちらほら見ますが、こちらは現在FuncOf()
に変更されているので注意が必要です。
まとめ
WebAssemblyを使用して、GoからJavaScriptと同じ振る舞いが出来る事が確認できました。WebAssemblyはスピードにフォーカスを当てているので今回のようなケースでは実感しづらいですが、実際のシチュエーションとしてはゲーム、大規模なWebアプリ制作などが考えられます。フロントエンド側の技術が苦手な私でもDOM操作がGoから行えるのは魅力的です。まだ比較的新しい技術ではありますが、今後のWebプログラマーとして関わる方には動向が気になる技術ではないでしょうか。 WebAssemblyでデモ的に作成されたゲームもあるので気になる方はこちらもチェックしてはいかがでしょう。
参考
js - The Go Programming Language WebAssembly: 「なぜ」と「どうやって」 [翻訳記事] - DEV Community 👩💻👨💻 GoでWebAssemblyに触れよう ドキュメントオブジェクトモデル (DOM) - Web API | MDN