注意
本記事はとりあえず動く形にしたくらいの理解度で書いた記事です。内容に誤りが含まれる可能性があります。
概要
Goでフロントエンドを作るフレームワークであるVectyを使ってみた際の備忘録です。また、状態管理のためにReduxを使用しています。
Redux Todos Exampleを参考にしています。
使用したバージョンなど
- Go 1.17
- github.com/hexops/vecty 0.6.0
- github.com/dannypsnl/redux/v2 2.2.3
備忘録集
インストール
Vectyのインストール
go get github.com/hexops/vecty
Reduxのインストール
go get github.com/dannypsnl/redux/v2
wasmserveの使用
Vectyで作成したページを簡単に表示する方法としてwasmserveを使用する方法があります。
下記コマンドでwasmserveをインストールできます。
go get -u github.com/hajimehoshi/wasmserve
インストール後、main.go
と同一階層でwasmserve
を実行することで、http://localhost:8080/
でページを表示することができます。
ブラウザをリロードするたびにgo build
が行われるため、コードを変更しリロードをすることで変更が反映されます。
ページの描画
Vectyは下記のような vecty.Core
を埋め込んだ構造体を定義し、Render()
内に描画処理を書きます。
type PageView struct {
vecty.Core
}
func (p *PageView) Render() vecty.ComponentOrHTML {
return elem.Body(
&components.AddTodo{},
components.NewVisibleTodoList(),
&footer{},
)
}
上記の構造体を下記のように vecty.RenderBody()
に渡すことでページが描画されます。
p := &pages.PageView{}
vecty.RenderBody(p)
propの定義
親コンポーネントから子コンポーネントに値を渡したいときは、下記のように渡したい値を子コンポーネントに定義した際にvecty:"prop"
とタグをつける必要があるようです。
type link struct {
vecty.Core
Type model.FilterType `vecty:"prop"`
IsActive bool `vecty:"prop"`
Label string `vecty:"prop"`
OnClick func(event *vecty.Event) `vecty:"prop"`
}
タグをつけることでRender中に親コンポーネントのプロパティを認識できるようになるらしいです。1
Render()の書き方
各コンポーネントが持つRender()は、下記のようにelem
パッケージのメソッドを用いて各コンポーネントを生成します。
func (t *todoListItem) Render() vecty.ComponentOrHTML {
var textdeco = "none"
if t.Completed {
textdeco = "line-through"
}
return elem.ListItem(
vecty.Markup(
vecty.Style("text-decoration", textdeco),
event.Click(t.OnClick),
),
vecty.Text(t.Text),
)
}
elemパッケージのメソッドの引数には、子コンポーネントとvecty.MarkupList
を渡すことができます。vecty.MarkupList
でスタイルやプロパティやイベントを設定できます。
Vectyではstyle
, prop
, event
パッケージに設定用のメソッドが用意されているようです。また、用意されていないものについてもvecty.Style()
2, vecty.Property()
3, &vecty.EventListener{}
4で指定できるようです。
Reduxの使い方
Storeの宣言
下記のようにrematch.Reducer
を埋め込んだ構造体を定義する必要があります。この時、State
という名称の変数は必須であり、これがReduxのStateと認識されます。
type todosStore struct {
rematch.Reducer
State []model.Todo
Add *rematch.Action `action:"AddTodo"`
Complete *rematch.Action `action:"CompleteTodo"`
}
さらに、*rematch.Action
でActionを定義することができ、タグaction
で指定したメソッドを下記のように定義することで、ActionをDispatch()
経由で受け取ることができます。この時、第1引数はState
と同じ型である必要があります。
func (t *todosStore) AddTodo(s []model.Todo, a addTodoAction) []model.Todo {
return append(s, a.payload)
}
Storeの初期化
以下のようにStoreを初期化します。TodosDispacher
はActionの発行に必要であり、TodosReducer
はStateをコンポーネントから取得するときに必要となります。
var TodosDispacher *store.Store
var TodosReducer *todosStore
func TodosStoreInit() {
nextTodoId = 0
TodosReducer = &todosStore{State: make([]model.Todo, 0)}
TodosDispacher = store.New(TodosReducer)
}
Actionの発行
Actionの発行は以下のように行います。この時、With()
に渡した変数が第2引数として渡されます。
storeutil.TodosDispacher.Dispatch(storeutil.TodosReducer.Add.With(storeutil.NewAddTodoAction(a.newItemTitle)))
Stateの取得
Stateの取得は以下のように行える。StateOf()
が返す値はinterface{}
であるため、型アサーションを行う必要があります。
todosState, _ := storeutil.TodosDispacher.StateOf(storeutil.TodosReducer).([]model.Todo)
再描画について
vecty.Rerender()
にコンポーネントを渡すことで、そのコンポーネントと子コンポーネントを再描画することができます。
func (a *AddTodo) onAdd(e *vecty.Event) {
storeutil.TodosDispacher.Dispatch(storeutil.TodosReducer.Add.With(storeutil.NewAddTodoAction(a.newItemTitle)))
a.newItemTitle = ""
vecty.Rerender(a)
}
Redux経由での再描画
上述した通り、vecty.Rerender()
で再描画されるのは、渡したコンポーネントとそのコンポーネントのみであり、Stateの変化に合わせて再描画することができない模様。そこで、下記のようにSubscribe()
に再描画を設定しておくことで、Dispatch()
が呼ばれた後に再描画を行うことができます。
func NewVisibleTodoList() *visibleTodoList {
c := &visibleTodoList{}
storeutil.TodosDispacher.Subscribe(func() {
vecty.Rerender(c)
})
storeutil.FilterDispacher.Subscribe(func() {
vecty.Rerender(c)
})
return c
}
タイトルの設定
以下のように設定できる。
vecty.SetTitle("Todos")
Github Pageでの公開
GoのWebassembleを公開するのと同様の方法でビルドすることで、公開することができます。以下にコマンドを示しておきます。
GOOS=js GOARCH=wasm go build -o ./docs/main.wasm main.go
cp $(go env GOROOT)/misc/wasm/wasm_exec.html ./docs/index.html
cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./docs/wasm_exec.js
その後./docs/index.html
のscriptタグ内を以下のように編集します。
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
go.run(inst);
inst = WebAssembly.instantiate(mod, go.importObject);
}).catch((err) => {
console.error(err);
});
</script>
最後にdocs
以下をGithub Pageとして公開すればよいです。
感想
良かった点
- Goでフロントエンドを書ける
- 感覚としてはだいぶReactっぽく書ける感じがする
辛い点
- ドキュメントがあんまりない
- これくらい?
- Vecty自体にはFluxを実現する仕組みがない
- 今回Reduxを使ってみたが、
interface{}
やreflect
が多用されていて辛い(しかたない感じはする)- こちらもドキュメントは無さそう
- 今回Reduxを使ってみたが、
- デバッガが使えない
- CSSをコンポーネントごとに設定するようなことはできなそう?
- 開発が停滞していそう(最近若干動き始めてるっぽい?)
まとめ
Goでフロントエンドを作れるVectyを使ってみました。
Goで書けるという点は魅力的ですが、まだまだ使いやすいかといわれると微妙な感じがしました。また、1.0.0に向けて破壊的変更もあるようでどうなるかはよくわかりません(そもそも1.0.0が出るかどうかも)。
とはいえ、Goで実際にフロントエンドで動くものが作れるというのを試せて満足です。