Vectyを使ってGoでフロントエンドを作ってみた話

# 注意
本記事はとりあえず動く形にしたくらいの理解度で書いた記事です。内容に誤りが含まれる可能性があります。

# 概要
Goでフロントエンドを作るフレームワークである[Vecty](https://github.com/hexops/vecty)を使ってみた際の備忘録です。また、状態管理のために[Redux](https://github.com/dannypsnl/redux)を使用しています。

作成したリポジトリは[こちら](https://github.com/suger-131997/todos-vecty)です。Webページは[こちら](https://suger-131997.github.io/todos-vecty/)。

[Redux Todos Example](https://github.com/reduxjs/redux/tree/master/examples/todos)を参考にしています。

# 使用したバージョンなど
- 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](https://github.com/hajimehoshi/wasmserve)を使用する方法があります。

下記コマンドでwasmserveをインストールできます。

```
go get -u github.com/hajimehoshi/wasmserve
```

インストール後、`main.go`と同一階層で`wasmserve`を実行することで、`http://localhost:8080/`でページを表示することができます。

ブラウザをリロードするたびに`go build`が行われるため、コードを変更しリロードをすることで変更が反映されます。


## ページの描画
Vectyは下記のような `vecty.Core` を埋め込んだ構造体を定義し、`Render()` 内に描画処理を書きます。

```go:pageview.go
type PageView struct {
	vecty.Core
}

func (p *PageView) Render() vecty.ComponentOrHTML {
	return elem.Body(
		&components.AddTodo{},
		components.NewVisibleTodoList(),
		&footer{},
	)
}
```

上記の構造体を下記のように `vecty.RenderBody()` に渡すことでページが描画されます。

```go:main.go
p := &pages.PageView{}
vecty.RenderBody(p)
```

## propの定義
親コンポーネントから子コンポーネントに値を渡したいときは、下記のように渡したい値を子コンポーネントに定義した際に`vecty:"prop"`とタグをつける必要があるようです。

```go:link.go
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`パッケージのメソッドを用いて各コンポーネントを生成します。

```go:todolistitem.go
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と認識されます。

```go:todosstore.go
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`と同じ型である必要があります。__

```go:todosstore.go
func (t *todosStore) AddTodo(s []model.Todo, a addTodoAction) []model.Todo {
	return append(s, a.payload)
}
```

### Storeの初期化
以下のようにStoreを初期化します。`TodosDispacher`はActionの発行に必要であり、`TodosReducer`はStateをコンポーネントから取得するときに必要となります。

```go:todosstore.go
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引数として渡されます。

```go:addtodo.go
storeutil.TodosDispacher.Dispatch(storeutil.TodosReducer.Add.With(storeutil.NewAddTodoAction(a.newItemTitle)))
```

### Stateの取得
Stateの取得は以下のように行える。`StateOf()`が返す値は`interface{}`であるため、型アサーションを行う必要があります。

```go:visibletodolist
todosState, _ := storeutil.TodosDispacher.StateOf(storeutil.TodosReducer).([]model.Todo)
```

## 再描画について
`vecty.Rerender()`にコンポーネントを渡すことで、そのコンポーネントと子コンポーネントを再描画することができます。

```go:addtodo.go
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()`が呼ばれた後に再描画を行うことができます。

```go:visibletodolist.go
func NewVisibleTodoList() *visibleTodoList {
	c := &visibleTodoList{}

	storeutil.TodosDispacher.Subscribe(func() {
		vecty.Rerender(c)
	})

	storeutil.FilterDispacher.Subscribe(func() {
		vecty.Rerender(c)
	})

	return c
}
```

## タイトルの設定
以下のように設定できる。

```go:main.go
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タグ内を以下のように編集します。

```html
<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っぽく書ける感じがする

## 辛い点
- ドキュメントがあんまりない
    - [これ](https://pkg.go.dev/github.com/hexops/vecty)くらい？
- Vecty自体にはFluxを実現する仕組みがない
    - 今回[Redux](https://github.com/dannypsnl/redux)を使ってみたが、`interface{}`や`reflect`が多用されていて辛い(しかたない感じはする)
        - こちらもドキュメントは無さそう
- デバッガが使えない
- CSSをコンポーネントごとに設定するようなことはできなそう？
- 開発が停滞していそう(最近若干動き始めてるっぽい？)

# まとめ
Goでフロントエンドを作れるVectyを使ってみました。
Goで書けるという点は魅力的ですが、まだまだ使いやすいかといわれると微妙な感じがしました。また、1.0.0に向けて破壊的変更もあるようでどうなるかはよくわかりません(そもそも1.0.0が出るかどうかも)。

とはいえ、Goで実際にフロントエンドで動くものが作れるというのを試せて満足です。

# 参考リンク集
- [Vecty examples](https://github.com/hexops/vecty/tree/main/example)
- [redux example](https://github.com/dannypsnl/redux/tree/v2/example)
- [WebAssembly Tutorial](https://morioh.com/p/385c092f41a9)

[^1]: https://github.com/hexops/vecty/blob/main/dom.go#L1016
[^2]: https://github.com/hexops/vecty/blob/e5d44d29c6a6bea841570b7708ad9a00c87d704d/style/style.go
[^3]: https://github.com/hexops/vecty/blob/e5d44d29c6a6bea841570b7708ad9a00c87d704d/prop/prop.go
[^4]: https://github.com/hexops/vecty/blob/e5d44d29c6a6bea841570b7708ad9a00c87d704d/event/event.gen.go