LoginSignup
12
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-18

注意

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

概要

Goでフロントエンドを作るフレームワークであるVectyを使ってみた際の備忘録です。また、状態管理のためにReduxを使用しています。

作成したリポジトリはこちらです。Webページはこちら

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() 内に描画処理を書きます。

pageview.go
type PageView struct {
    vecty.Core
}

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

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

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

propの定義

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

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

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と認識されます。

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と同じ型である必要があります。

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

Storeの初期化

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

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引数として渡されます。

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

Stateの取得

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

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

再描画について

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

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

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

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

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

    return c
}

タイトルの設定

以下のように設定できる。

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

<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が多用されていて辛い(しかたない感じはする)
      • こちらもドキュメントは無さそう
  • デバッガが使えない
  • CSSをコンポーネントごとに設定するようなことはできなそう?
  • 開発が停滞していそう(最近若干動き始めてるっぽい?)

まとめ

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

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

参考リンク集

12
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
12
4