LoginSignup
4
4

More than 3 years have passed since last update.

Vugu で Todo アプリ作って Try WebAssembly!!

Last updated at Posted at 2019-09-28

はじめに

技術書典7 に行った際、Try WebAssembly!! という本に出会い、
前々から WebAssembly に興味持っていたのもあり購入して勉強していました :book:

一般的に Go で WebAssembly アプリを作る際は syscall/js を利用するようなのですが、
Vugu を利用すると Vue.js の手触りで WebAssembly アプリの開発進めていけるので、
UI を組む手間などが大幅に省けて楽に開発を進めていけるようです :star2:

今回は勉強のため Vugu で Todo アプリを作る際は、
なるべく Vugu に用意されている便利な機能を取り入れて、
理解を深めながら開発を進めることを意識しました :arrow_heading_up:

demo.gif

実際のソースコードは GitHub にアップしておきました :arrow_down:
https://github.com/nikaera/vugu-todo

動作環境

  • Go 1.12.5 (Modules が使えるバージョン)
  • Vugu v0.1.0

環境構築

公式サイトの Getting Started に沿って構築していきます :construction_site:

  • 適当な場所に作業用フォルダ testapp を作成します
  • testapp フォルダ内で GO111MODULE=on go mod init nikaera.com/vugu/testapp を実行して go.mod ファイルを作成します
  • testapp フォルダ内に Vugu のテンプレートである root.vugu ファイルを作成する (中身及び詳細は :arrow_down: )
testapp/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 を作成する
testapp/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 で開発用サーバーを起動する

すると、ターミナルに下記が出力されているはずです :arrow_down:

$ 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 内で定義した内容が反映されたページを確認できるはずです :tada:

demo2.gif

これで Vugu の開発環境構築が出来ました :clap:

試しに root.vugu ファイルで定義している I am here! という文字列を
I am here NOW!! とかに変更してからブラウザをリロードすると
表示されている文字列が変わっていることが確認出来るはずです :white_check_mark:

Todo アプリの作成手順

Todo の文字列入力フィールドとその登録ボタンを用意する

root.vugu を書き換えていきます :pencil:

testapp/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 にアクセスします :earth_africa:
(既に HTTP サーバーを起動済みであればブラウザをリロードすれば大丈夫です)

テキスト入力フィールドに文字列を入力し、Add Todo ボタンをクリックすることで、
JavaScript コンソールに入力フィールドの文字列が出力されていれば OK です :thumbsup: :arrow_down:

demo3.gif

Todo の内容が画面に表示されるようにする

Vugu では Vue.js のようにコンポーネントを作成することが可能です。
root.vugu もコンポーネントであり、トップレベルのコンポーネントとして利用しています。

今回は Todo を表示するためのコンポーネントを作成します :notepad_spiral:
testapp フォルダ内に todo.vugu ファイルを作成します :arrow_down:

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 を改修して動作検証してみます :arrow_down:

testapp/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 で定義した内容の表示が確認出来るはずです :arrow_down:

スクリーンショット 2019-09-28 20.51.50.png

Todo の登録 & 登録した Todo をリスト表示する

こちら保留にしていた root.vugu 内の AddTodo 関数の実装を行っていきます :fire:

testapp/root.vugu
<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 アプリの完成です:bangbang: 再度 http://127.0.0.1:8844 にアクセスしてみます :arrow_down:

demo4.gif

無事 Todo を複数登録することに成功しました :clap: :clap:

おわりに

今回は Vugu 入門のための Todo アプリの作り方をご紹介しました。

今後は WebAssembly の利点をキチンと活かせるアプリとして、
JavaScript では実行するのに重い処理を go get したライブラリで実行したり、
高 FPS で計算処理走らせて WebGL で描画するアプリとか作ってみたいなと考えてます。

技術書典7 で購入した本はまだまだあるので、
引き続きインプットもアウトプットも頑張るぞい :bangbang: :fire:

参考リンク

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4