LoginSignup
3
1

More than 1 year has passed since last update.

Goで動くフロントエンドが作れるSSRライブラリkyotoを使ってみた話

Last updated at Posted at 2022-01-09

2022/02/24 追記

kyotoは当記事を書いた時点から大幅な変更が行われており、下記の記事の内容はもう古いものとなっています。

現在時点で新しいバージョンはリリースされていませんが、1.0リリースに向けて現在も開発が進められているようです。

新しい機能を試したい場合はgo get github.com/kyoto-framework/kyoto@masterでmasterブランチの内容を取得するしかないようです。一応ドキュメントはありました。

注意

本記事は試しに使ってみた程度の記事です。

今回使用したkyotoは開発初期であり、今後も多くの変更が予定されているようです。本記事はv0.2を使用しています。

kyotoとは

kyotoはSSR(Server Side Rendering)のライブラリです。

ドキュメントにあるMotivationを以下に引用します。

Motivation
Main motivation is to reduce usage of popular SPA/PWA frameworks where it's not needed because it adds a lot of complexity and overhead. There is no reason to bring significant runtime, VirtualDOM, and Webpack into the project with minimal dynamic frontend behavior. This project proves the possibility of keeping most of the logic on the server's side.

上記のようにkyotoはほとんどのロジックをサーバーサイドに置きながら、(制限はあるものの)SPA/PWAのようなフロントエンドを実現することができます。似たようなものとしてRailsのHotwireやLaravelのLaravel Livewireがあるようです。

Server Side Actions(SSA)

kyotoではServer Side Actions(SSA)でフロントエンドの動作を実現できます。

SSAはブラウザとサーバー間で通信を維持した上で、ブラウザ側で起動したアクションをサーバー側で実行し、ブラウザはサーバから返ってきたHTMLを表示します。
現在SSAの通信はServer-sent events(SSE)で実装されているようですが、今後変更されるそうです(参考)。

今回作ったもの

Redux Todos Exampleを参考にtodoリストを作りました。

kyoto-todos.gif

使用したバージョンなど

起動方法

$ git clone https://github.com/suger-131997/todos-kyoto.git
$ cd todos-kyoto
$ go mod download
$ go run .

http://localhost:9000/

サーバー起動

以下のコードでページのルーティングとポートを指定しています。同時にSSA用のhandlerも設定しています。

main.go
func ssatemplate(p kyoto.Page) *template.Template {
	return template.Must(template.New("SSA").Funcs(kyoto.TFuncMap()).ParseGlob("*.html"))
}

func main() {
	mux := http.NewServeMux()

	// ルーティングの設定
	mux.HandleFunc("/", kyoto.PageHandler(&PageTodos{}))

	// SSA用の設定
	mux.HandleFunc("/ssa/", kyoto.SSAHandler(ssatemplate))

	// サーバー起動
	http.ListenAndServe("localhost:9000", mux)
}

ページ

各ページはhtmlのテンプレートと対応する構造体で作られます。

ページの構造体は使用するコンポーネントの構造体を要素として持ちます。
Init()はページを表示する際に初回に呼び出されて各種初期化を行います。
Template()内で使用するテンプレートを指定します。同時にコンポーネントのテンプレートも指定しています。

page.todos.go
type PageTodos struct {
	Todos kyoto.Component
}

func (*PageTodos) Template() *template.Template {
	return template.Must(template.New("page.todos.html").Funcs(kyoto.TFuncMap()).ParseGlob("*.html"))
}

func (p *PageTodos) Init() {
    //コンポーネントの初期化
	p.Todos = kyoto.RegC(p, &ComponentTodos{})
}

{{ template {コンポーネント名} {コンポーネントの構造体} }}とする事でページ内にコンポーネントを埋め込むことができます。
SSAを使用する場合は{{ dynamics {SSA用のhandlerのURL} }}をhtml内に記述して、SSAのクライアント用のコードを埋め込む必要があります。

page.todos.html
<html>
  <head>
    <title>kyoto todos</title>
    {{ dynamics `/ssa` }}
  </head>
  <body>
    {{ template "ComponentTodos" .Todos }}
  </body>
</html>

コンポーネント

コンポーネントもhtmlのテンプレートと対応する構造体で作られます。

以下に示すのはフィルターを切り替えるボタンのためのコンポーネントです。

component.todos.filterlink.go
type ComponentTodosFilterLink struct {
	Type          FilterType
	CurrentFilter *FilterType
}

func (c ComponentTodosFilterLink) IsActive() bool {
	return c.Type == *c.CurrentFilter
}

コンポーネント部分を{{ define {コンポーネント名} }}{{ end }}で囲むことでコンポーネントとして定義できます。
状態を持つコンポーネントの場合、top-level nodeに {{ componentattrs . }} を埋め込む必要があります。これによってhtml内にコンポーネントの情報が埋め込まれます。

component.todos.filterlink.html
{{ define "ComponentTodosFilterLink" }}
<button {{ componentattrs . }}
    onclick="{{ action `$ChangeFilter` .Type }}"
    {{ if .IsActive }}
        disabled
    {{ end }}
>{{ .Type.String }}</button>
{{ end }}

上記のようにonclick="{{ action {アクション名} {引数} }}"とする事でクリック時にSSAのアクションを呼び出すことができます。$がついているのは親コンポーネントのアクションを指定するためです(参考)。
コンポーネントを定義するためには以下に示すようにAction()をコンポーネントの構造体に定義します。
コンポーネントのアクションが呼び出された時にそのコンポーネントの再描画が行われます。

component.todos.go
func (c *ComponentTodos) Actions() kyoto.ActionMap {
	return kyoto.ActionMap{
		"Add": func(args ...interface{}) {
			c.Todos = append(c.Todos, Todo{c.NextTodoId, c.NewTitle, false})
			c.NewTitle = ""
			c.NextTodoId++
		},
		"Complete": func(args ...interface{}) {
			target := int(args[0].(float64))
			for i := 0; i < len(c.Todos); i++ {
				if c.Todos[i].Id == target {
					c.Todos[i].Completed = !c.Todos[i].Completed
				}
			}
		},
		"ChangeFilter": func(args ...interface{}) {
			c.CurrentFilter = FilterType(int(args[0].(float64)))
		},
	}
}

テキストボックスの入力取得

テキストボックスの入力をstringで受け取りたいときは、type="text"を指定した上で、下記のようにコンポーネントのメンバ変数をname, value, oninputに設定する必要がある。

component.todos.html
<div>
      <input name="NewTitle" value="{{ .NewTitle }}" type="text" oninput="{{ bind `NewTitle` }}" />
      <button onclick="{{ action `Add` }}" >Add Todo</button>
</div>

まとめ

動くフロントエンドが作れるSSRライブラリkyotoを使ってみました。
サーバーサイドでほとんどのロジックが動くおかげでかなり開発しやすいと感じました。基本的にGoとhtmlで完結できるという点もすごく魅力的です。

一方、公開するにはサーバーが必要となるため要件によっては、通常のフロントエンド用のライブラリを使用した方がいい場合も多そうです。(Github Pagesでの公開とかももちろんできない)。

開発も活発ですし、スターも結構ついているので今後追っていきたい気持ちです。

その他

類似してそうなライブラリ

記事を書いている途中に見つけた似てそうなものをまとめておきます。一切触ってないです。

よくわかってないこと

あまりにも取り留めないので折り畳み
  • SSAで渡されてくるint型がfloat64になっている
    • クライアント側で状態を管理しているから?
    • そもそもinterface{}で渡ってくるのどうにかなるといいな…
  • コンポーネント間で共有する値の取り扱いがいまいちわからない
    • 今回はポインタで共有してるけど正しいのかよくわからない。(状態はhtmlに埋め込まれてそうだけどポインタって?みたいな)
    • Contextがあるからこれを使えってことなのだろうか?
    • ContextでReduxみたいなものを共有する?
    • Server Side Stateなるものが今後作られるらしいからそれかも
  • 実際に外部サーバーに公開されたらどうなるんだろうか
    • いまローカルで動いているレベルのサクサクさが期待できるのか
    • Hotwireの方も似たような仕組みだし割と行けるのかも?
    • たくさん接続されても大丈夫的なこともHotwireの方で言われていたし(参考)
  • 再描画の制御がわかっていない
    • アクションが起動されたコンポーネントとその子コンポーネントが書き換わるけど、それ以外の場合はどうしたらいいか分からない
  • 使えてない機能&理解できていない機能がいくつもある
3
1
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
3
1