概要
VueやReactにインスパイアされ、Go言語で書かれたWebAssemblyのライブラリ、Vuguが登場しました。Vuguでは、Vueのような単一コンポーネントファイルを利用して、簡単にWebAssemblyのアプリケーションを作ることができます。
ちなみに、WebAssemblyについて知りたい方は、こちらの記事などをご参照ください。
本記事では、Vuguを使ってWebAssemblyを利用したサンプルアプリケーションを作っていきます。
具体的には、GitHubのAPIを利用して、ユーザのリポジトリの一覧を表示するアプリケーションを作ります。
本記事で作成するアプリケーションの挙動を確認したい方は、以下から閲覧することができます。
また、ソースコードを確認したい方は、以下から閲覧することができます。
想定読者・解説内容
本記事では、VueおよびGoの文法について基本的な知識のある方を対象としています。
また、Vueを知っていればある程度予測できる内容部分に関する解説はスキップし、Vueとは異なる部分にフォーカスして解説をします。
開発
はじめに
筆者の開発環境は以下の通りです。
VuguがGoのバージョン1.12以上を求めているので、それ以上のバージョンとしてください。
go version go1.13.3 darwin/amd64
まずはじめに、作業用のディレクトリを作成します。
名前はなんでも良いですが、VuguGitHubClientという名前で進めていきます。
mkdir VuguGitHubClient
cd VuguGitHubClient
Goのパッケージ管理をする仕組みとして、今回はGo Modulesを利用します。
以下のコマンドで初期化します。
ユーザ名などは適宜置き換えてください。
go mod init github.com/solt9029/VuguGitHubClient
コンポーネント実装
さてここからが本題です。
Vueの単一コンポーネントのようなファイルとして、root.vuguを作成します。
内容は以下の通りです。
<div class="root">
<div class="container">
<div class="row">
<h1>VuguGitHubClient</h1>
<div class="input-group block">
<input type="text" placeholder="user" id="user" class="form-control" @change="data.HandleChange(event)">
<div class="input-group-append">
<button class="btn btn-primary" @click="data.HandleClick(event)">find repos</button>
</div>
</div>
<div class="block">
<div vg-if="data.IsLoading">isLoading...</div>
<div vg-if='data.Error != ""' vg-html="data.Error"></div>
<div vg-if="len(data.Repos) > 0">
<ul class="list-group">
<repo-item vg-for="_, repo := range data.Repos" :name="repo.Name" :html-url="repo.HtmlUrl"></repo-item>
</ul>
</div>
</div>
</div>
</div>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</div>
<style>
.block {
margin-bottom: 30px;
}
</style>
<script type="application/x-go">
import (
"encoding/json"
"log"
"net/http"
)
type RootData struct {
Repos []Repo
IsLoading bool
User string
Error string
}
type Repo struct {
Name string `json:"name"`
HtmlUrl string `json:"html_url"`
}
func (data *RootData) HandleChange(event *vugu.DOMEvent) {
data.User = event.JSEvent().Get("target").Get("value").String()
log.Printf("user: %v", data.User)
}
func (data *RootData) HandleClick(event *vugu.DOMEvent) {
data.Repos = []Repo{}
eventEnv := event.EventEnv()
data.IsLoading = true
data.Error = ""
go func() {
eventEnv.Lock()
defer func() {
eventEnv.UnlockRender()
data.IsLoading = false
}()
res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
if err != nil {
log.Printf("Error fetching: %v", err)
data.Error = "Error fetching."
return
}
defer res.Body.Close()
var newRepos []Repo
err = json.NewDecoder(res.Body).Decode(&newRepos)
if err != nil {
log.Printf("Error JSON decoding: %v", err)
data.Error = "Error JSON decoding."
return
}
data.Repos = newRepos
}()
}
</script>
デフォルトでは、root.vugu
という名前のコンポーネントが一番親の要素ということになります。
さて、Vuguファイルの仕様についてもう少し詳しく解説していきます。
Vuguファイルは以下のように構成します。
基本的には、Vueと同じだと考えて問題ないでしょう。
// html
<div></div>
// style
<style></style>
// script
<script type="application/x-go"></script>
大きくVueと異なる点は、当たり前ではありますが、script内にGoのコードを書くというところです。
まずはscript内のコードから見ていきましょう。
type RootData struct {
Repos []Repo
IsLoading bool
User string
Error string
}
RootDataで定義されているものは、HTML内でdata.User
のように参照できたりします。
コンポーネント内で状態を持つデータを表しています。
RootDataという名前になっていますが、こちらは命名規則に従って名前がつけられています。
コンポーネントの名前に対してアッパーキャメルケースで名付けます。
// 例
root.vugu -> RootData
example-component.vugu -> ExampleComponentData
APIから値を取得し、データを更新する処理については、非同期処理になります。
WebAssembly内で非同期処理を行う場合、デッドロックを避けるために、新しいgoroutineを作成します。
細かい解説はコード内部に記します。
go func() {
eventEnv.Lock() // 排他ロック取得
defer func() {
eventEnv.UnlockRender() // この関数終了時にロック解除
data.IsLoading = false // この関数終了時にdata.isLoadingをfalseにする
}()
res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
if err != nil {
log.Printf("Error fetching: %v", err)
data.Error = "Error fetching."
return
}
defer res.Body.Close()
var newRepos []Repo
err = json.NewDecoder(res.Body).Decode(&newRepos) // 取得したjsonをパースする
if err != nil {
log.Printf("Error JSON decoding: %v", err)
data.Error = "Error JSON decoding."
return
}
data.Repos = newRepos // data.Reposを更新する
}()
お気づきかもしれませんが、root.vugu
のHTML内で<repo-item>
といったHTMLタグが登場しています。
こちらはカスタムコンポーネントなので、今から作成していきましょう。
repo-item.vugu
を作成します。
内容は以下の通りです。
<li class="list-group-item">
<a :href="data.HtmlUrl" vg-html="data.Name"></a>
</li>
<script type="application/x-go">
type RepoItemData struct {
Name string
HtmlUrl string
}
func (component *RepoItem) NewData(props vugu.Props) (interface{}, error) {
ret := &RepoItemData{}
ret.Name, _ = props["name"].(string)
ret.HtmlUrl, _ = props["html-url"].(string)
return ret, nil
}
</script>
propsとして値を受け取るためには、NewData
という関数を利用して、dataに受け渡してあげる必要があります。
propsをそのまま使うといったことは基本的にはできません。(できないはず。違ったら教えてください。)
さて、コアとなる必要なコンポーネントを作ることができました!
サーバ実装
次に、サーバのコードをちょろっと書きます。
server.go
を作成しましょう。
こちらはVuguが提供しているsimplehttpを利用しており、hotreloadとはいきませんが、vuguファイルを書きなおすたびにその変更が反映されます。(ブラウザのリロードは手動)
// +build !wasm
package main
//go:generate vugugen .
import (
"flag"
"log"
"net/http"
"os"
"path/filepath"
"github.com/vugu/vugu/simplehttp"
)
func main() {
dev := flag.Bool("dev", false, "Enable development features")
dir := flag.String("dir", ".", "Project directory")
httpl := flag.String("http", "127.0.0.1:8877", "Listen for HTTP on this host:port")
flag.Parse()
wd, _ := filepath.Abs(*dir)
os.Chdir(wd)
log.Printf("Starting HTTP Server at %q", *httpl)
h := simplehttp.New(wd, *dev)
log.Fatal(http.ListenAndServe(*httpl, h))
}
上記のコードは、Vugu公式のドキュメント内のソースコードを利用しています。
さて、準備完了です!
以下のコマンドを打ってみてください。
go run server.go -dev
http://localhost:8877 にてVuguがWebAssemblyを利用して動いていることを確認できるでしょう。
実際にserver.go
を利用して、Vuguのアプリケーションが動いていることを確認できました。
できれば、staticなファイルとしてビルドできると嬉しいですよね?
以下のようなdist.go
を作成し、distディレクトリにビルドしましょう!
// +build ignore
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"text/template"
"time"
"github.com/vugu/vugu/distutil"
"github.com/vugu/vugu/simplehttp"
)
func main() {
clean := flag.Bool("clean", false, "Remove dist dir before starting")
dist := flag.String("dist", "dist", "Directory to put distribution files in")
flag.Parse()
start := time.Now()
if *clean {
os.RemoveAll(*dist)
}
os.MkdirAll(*dist, 0755) // create dist dir if not there
// copy static files
distutil.MustCopyDirFiltered(".", *dist, nil)
// find and copy wasm_exec.js
distutil.MustCopyFile(distutil.MustWasmExecJsPath(), filepath.Join(*dist, "wasm_exec.js"))
// check for vugugen and go get if not there
if _, err := exec.LookPath("vugugen"); err != nil {
fmt.Print(distutil.MustExec("go", "get", "github.com/vugu/vugu/cmd/vugugen"))
}
// run go generate
fmt.Print(distutil.MustExec("go", "generate", "."))
// run go build for wasm binary
fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(*dist, "main.wasm"), "."))
// STATIC INDEX FILE:
// if you are hosting with a static file server or CDN, you can write out the default index.html from simplehttp
req, _ := http.NewRequest("GET", "/index.html", nil)
outf, err := os.OpenFile(filepath.Join(*dist, "index.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
distutil.Must(err)
defer outf.Close()
template.Must(template.New("_page_").Parse(simplehttp.DefaultPageTemplateSource)).Execute(outf, map[string]interface{}{"Request": req})
// BUILD GO SERVER:
// or if you are deploying a Go server (yay!) you can build that binary here
// fmt.Print(distutil.MustExec("go", "build", "-o", filepath.Join(*dist, "server"), "."))
log.Printf("dist.go complete in %v", time.Since(start))
}
上記のコードもVugu公式のドキュメント内のソースコードを利用しています。
以下のコマンドを打ってみてください。
go run dist.go
新しくdistディレクトリが作成され、index.html
・main.wasm
・wasm_exec.js
が作られていることが確認できます。
以上で開発は終了です!
お疲れ様でした。
Vueと比較してVuguでやりにくいこと
筆者がVuguを使ってみて、やりにくいと感じたことを書きます。
なお、調べきれていないと思うので、何か間違いなどあればご指摘ください。
- propsを一旦dataにいれなければならない
- エディタのSyntaxHighlightが存在しない(はず)
- ライフサイクルに対応した関数がほとんど存在しない(こちらのPRによれば、
BeforeBuild()
というライフサイクルは現在新しいバージョンで実装されているらしい。例えばコンポーネントがunmountするときのようなライフサイクルイベントは現状存在しなそう。)
最後に
Vuguを利用して、簡単にWebAssemblyのアプリケーションを書くことができました。
WebAssemblyは敷居の高いものだと思われていますが、基本的なVueとGoの知識さえあれば何も抵抗なく開発を行えるということがわかりました。
最後まで本記事を読んでいただきありがとうございました。