#はじめに
技術書典7 に行った際、Try WebAssembly!! という本に出会い、
前々から WebAssembly に興味持っていたのもあり購入して勉強していました
一般的に Go で WebAssembly アプリを作る際は syscall/js を利用するようなのですが、
Vugu を利用すると Vue.js の手触りで WebAssembly アプリの開発進めていけるので、
UI を組む手間などが大幅に省けて楽に開発を進めていけるようです
今回は勉強のため Vugu で Todo アプリを作る際は、
なるべく Vugu に用意されている便利な機能を取り入れて、
理解を深めながら開発を進めることを意識しました
実際のソースコードは GitHub にアップしておきました
https://github.com/nikaera/vugu-todo
#動作環境
- Go 1.12.5 (Modules が使えるバージョン)
- Vugu v0.1.0
#環境構築
公式サイトの Getting Started に沿って構築していきます
- 適当な場所に作業用フォルダ
testapp
を作成します -
testapp
フォルダ内でGO111MODULE=on go mod init nikaera.com/vugu/testapp
を実行してgo.mod
ファイルを作成します -
testapp
フォルダ内に Vugu のテンプレートであるroot.vugu
ファイルを作成する (中身及び詳細は )
<!-- Vugu のテンプレート構文 (Vue.js とほぼ一緒) -->
<!-- HTML テンプレートを定義する -->
<div class="my-first-vugu-comp">
<!-- @<イベント名> で js のイベントを Go でハンドリング出来る -->
<button @click="data.Toggle()">Test</button>
<!-- vg-if で条件付きレンダリング出来、その条件は Go で定義することが出来る -->
<div vg-if="data.Show">I am here!</div>
</div>
<!-- HTML の style を定義する -->
<style>
.my-first-vugu-comp { background: #eee; }
</style>
<!-- Vugu では Go で script タグ内にビジネスロジックを書く -->
<script type="application/x-go">
// 1. Vugu の命名規則に従い `${ファイル名のキャメルケース}Data` で struct を作成する
// 今回はファイル名が root.vugu だから RootData という名前で struct を宣言
// 2. Vugu の命名規則に従っている struct は自動でインスタンス生成され、
// HTML テンプレート内から data 変数でアクセス出来るようになる
// 3. HTML テンプレート内から RootData の変数の参照や関数の呼び出しが data 変数で可能になる
type RootData struct { Show bool }
func (data *RootData) Toggle() { data.Show = !data.Show }
</script>
-
testapp
フォルダ内に Vugu の開発用サーバーを起動するためdevserver.go
を作成する
// +build ignore
package main
import (
"log"
"net/http"
"os"
// Vugu アプリケーションを動かすための HTTP サーバー
"github.com/vugu/vugu/simplehttp"
)
func main() {
wd, _ := os.Getwd()
l := "127.0.0.1:8844"
log.Printf("Starting HTTP Server at %q", l)
h := simplehttp.New(wd, true)
// include a CSS file
// simplehttp.DefaultStaticData["CSSFiles"] = []string{ "/my/file.css" }
log.Fatal(http.ListenAndServe(l, h))
}
-
go run devserver.go
で開発用サーバーを起動する
すると、ターミナルに下記が出力されているはずです
$ go run devserver.go
go: finding github.com/vugu/vugu/simplehttp latest
2019/09/28 17:16:23 Starting HTTP Server at "127.0.0.1:8844"
ブラウザから http://127.0.0.1:8844 にアクセスすると、
root.vugu
内で定義した内容が反映されたページを確認できるはずです
これで Vugu の開発環境構築が出来ました
試しに root.vugu
ファイルで定義している I am here!
という文字列を
I am here NOW!!
とかに変更してからブラウザをリロードすると
表示されている文字列が変わっていることが確認出来るはずです
Todo アプリの作成手順
Todo の文字列入力フィールドとその登録ボタンを用意する
root.vugu
を書き換えていきます
<div class="my-first-vugu-comp">
<!-- input タグでテキストフィールドを用意する -->
<!-- @change でテキストの変更イベントを Go 側の InputTodo 関数でハンドリングする -->
<!-- 引数に event を指定することで JavaScript の event 変数を Go 側に渡すことが可能 -->
<input type="text" id="todotext" @change='data.InputTodo(event)'>
<!-- 上記テキストフィールド内の文字列から Todo を登録するためのボタン -->
<!-- @click でボタンのクリックイベントを Go 側の AddTodo 関数でハンドリングする -->
<button @click="data.AddTodo()">Add Todo</button>
</div>
<script type="application/x-go">
type RootData struct {
// テキストフィールドの内容を保持しておく変数
TodoText string
}
// テキストフィールドの入力内容を検知して RootData の TodoText に代入するための関数
func (data *RootData) InputTodo(event *vugu.DOMEvent) {
// event.JSEvent() で JavaScript の event を取得出来る
// Get 関数を利用してテキストフィールド内の文字列を取得して TodoText に代入する
data.TodoText = event.JSEvent().Get("target").Get("value").String()
}
// "Add Todo" ボタンのクリックを検知して Todo リストに登録するための関数
func (data *RootData) AddTodo() {
// テキストフィールド内の文字列をコンソールに出力する
// (Todo リストへの登録処理は後の項目で実装する)
fmt.Println("TodoText: " + data.TodoText);
}
</script>
root.vugu
を書き換えたら go run devserver.go
で HTTP サーバーを起動して、
ブラウザから http://127.0.0.1:8844 にアクセスします
(既に HTTP サーバーを起動済みであればブラウザをリロードすれば大丈夫です)
テキスト入力フィールドに文字列を入力し、Add Todo
ボタンをクリックすることで、
JavaScript コンソールに入力フィールドの文字列が出力されていれば OK です
Todo の内容が画面に表示されるようにする
Vugu では Vue.js のようにコンポーネントを作成することが可能です。
root.vugu
もコンポーネントであり、トップレベルのコンポーネントとして利用しています。
今回は Todo を表示するためのコンポーネントを作成します
testapp
フォルダ内に todo.vugu
ファイルを作成します
<div class="todo">
<!-- Todo の完了ステータスを変更するために利用するためのチェックボックス -->
<input type="checkbox" name="done" @click="data.Done(event)">
<!-- Todo の内容を表示するための span タグ -->
<!-- :style のように : を接頭辞につけることで style 属性を Go 側で変更出来るようにする -->
<!-- vg-html で Go 側で HTML 要素のテキストを Go 側で出力することが出来る -->
<span :style='"text-decoration:" + data.TextStyle' vg-html='data.Date + " -> " + data.Text'></span>
</div>
<style>
.todo {
display: inline;
}
</style>
<script type="application/x-go">
// コンポーネントファイルを todo.vugu で作成したので TodoData で struct を宣言
type TodoData struct {
// 登録した Todo 内容を表す文字列
Text string
// Todo を登録した日付の unixtime 文字列
Date string
// Todo テキストを表示する際の span タグの style の
// text-decoration の値を保持する文字列
// (Todo が完了したら、文字に取り消し線を適用したかったため利用)
TextStyle string
}
// コンポーネント生成時に呼ばれるコンストラクタ関数
// props は Vue.js の props と同義で HTML タグの属性値が取得出来るもの
func (data *Todo) NewData(props vugu.Props) (interface{}, error) {
ret := &TodoData{}
// <todo text='test' date='1569666420'/> と宣言していた場合
// props["text"] には "test" 文字列が入り、
// props["date"] には "1569666420" が入る
ret.Text, _ = props["text"].(string)
ret.Date, _ = props["date"].(string)
ret.TextStyle = "none"
return ret, nil
}
// チェックボックスの On/Off を切り替えるたびに呼び出される関数
func (data *TodoData) Done(event *vugu.DOMEvent) {
// Todo に紐付いたチェックボックスが On か Off かを bool で取得
done := event.JSEvent().Get("target").Get("checked").Bool()
// チェックボックスが on なら Todo が完了済みとみなし、取り消し線を表示する
// チェックボックスが off なら Todo が未完了とみなし、普通に文字列を表示する
if done {
data.TextStyle = "line-through"
} else {
data.TextStyle = "none"
}
}
</script>
その後、todo.vugu
で定義したコンポーネントが正しく使えるようになったか、
root.vugu
を改修して動作検証してみます
<div class="my-first-vugu-comp">
<input type="text" id="todotext" @change='data.InputTodo(event)'>
<button @click="data.AddTodo()">Add Todo</button>
<ul>
<!-- todo.vugu で定義した Todo コンポーネントの表示を確かめる -->
<li><todo text="Test" date="1569666420"/></li>
</ul>
</div>
<script type="application/x-go">
type RootData struct {
TodoText string
}
func (data *RootData) InputTodo(event *vugu.DOMEvent) {
data.TodoText = event.JSEvent().Get("target").Get("value").String()
}
func (data *RootData) AddTodo() {
fmt.Println("TodoText: " + data.TodoText);
}
</script>
http://127.0.0.1:8844 にアクセスすると、
todo.vugu
で定義した内容の表示が確認出来るはずです
Todo の登録 & 登録した Todo をリスト表示する
こちら保留にしていた root.vugu
内の AddTodo
関数の実装を行っていきます
<div class="my-first-vugu-comp">
<input type="text" id="todotext" @change='data.InputTodo(event)'>
<button @click="data.AddTodo(event)">Add Todo</button>
<ul>
<!-- vg-for で Go の配列サイズ数だけタグを繰り返し生成する -->
<!-- TodoData の配列 data.Todos の配列サイズ数分 li タグを繰り返し生成する -->
<li vg-for='data.Todos'>
<!-- vg-for 内では value で配列内の要素にアクセスすることが出来る -->
<!-- data.Todos は TodoData の配列なので value には TodoData インスタンスが設定されている -->
<todo :date='value.Date' :text='value.Text'></todo>
</li>
</ul>
</div>
<script type="application/x-go">
// Vugu 内では Go のライブラリを利用することが出来る
import (
"time"
"strconv"
)
type RootData struct {
TodoText string
// Todo リストを TodoData の配列をして保持する
Todos []TodoData
}
func (data *RootData) InputTodo(event *vugu.DOMEvent) {
data.TodoText = event.JSEvent().Get("target").Get("value").String()
}
func (data *RootData) AddTodo(event *vugu.DOMEvent) {
ee := event.EventEnv()
// メインスレッドに影響を出さずにレンダリング内容をアップデートするため、
// Goroutine 内で Todo リストデータの更新を行う
go func() {
// レンダリング内容をロックする
ee.Lock()
// スコープ内の処理が完了次第、
// レンダリング内容をアンロックし、
// 再びレンダリングを開始する
defer ee.UnlockRender()
// テキスト入力フィールドの文字列と
// データ生成時の unixtime スタンプで
// TodoData を新たに生成する
todo := TodoData{
Text: data.TodoText,
Date: strconv.FormatInt(time.Now().Unix(), 10),
}
// data.Todos 配列に新たに生成した TodoData を追加する
// (ee.Lock() でロック処理を呼んでいないと ↓ の代入処理でエラーが発生する)
data.Todos = append(data.Todos, todo)
}()
}
</script>
これで Todo アプリの完成です 再度 http://127.0.0.1:8844 にアクセスしてみます
無事 Todo を複数登録することに成功しました
#おわりに
今回は Vugu 入門のための Todo アプリの作り方をご紹介しました。
今後は WebAssembly の利点をキチンと活かせるアプリとして、
JavaScript では実行するのに重い処理を go get したライブラリで実行したり、
高 FPS で計算処理走らせて WebGL で描画するアプリとか作ってみたいなと考えてます。
技術書典7 で購入した本はまだまだあるので、
引き続きインプットもアウトプットも頑張るぞい
#参考リンク