Livebook で Web アプリを実装してデプロイまでしてしまう

## はじめに

Livebook 0.9.0 がリリースされました

https://livebook.dev/

その中の目玉機能 Deploy を紹介します

なんと、 Livebook でそのまま Web アプリが実装できて、デプロイ、つまり実行環境への配置までできてしまうのです

論よりRun!

やってみましょう

## チュートリアルの起動

まず Livebook 0.9.0 を起動しましょう

Docker から起動する方は以下の記事を参照

https://qiita.com/RyoWakabayashi/items/5163112d0219fa48b237

Livebook のトップ画面は以下のようになっています

![スクリーンショット 2023-03-27 0.05.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/22323e8c-96a9-8445-5bea-c16eb5dc4dae.png)

ここで右側を見ると、「Deploy a chat app with Kino」という文言があります

ここがチュートリアルになっているので、クリックして開きます

![スクリーンショット 2023-03-27 0.13.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f6e0a58e-427d-dfa2-cda9-59364bdc8389.png)

このチュートリアル用ノートブックを実行していけばチャットアプリを公開することができます

簡単に解説していきます

## セットアップ

まず依存モジュールとして [Kino](https://github.com/livebook-dev/kino) の 0.9.0 以上をインストールします

```elixir
Mix.install([
  {:kino, "~> 0.9.0"}
])
```

## ボタンイベントの受信

最初は Kino.Control で出来ることを紹介しています

以下のようなコードで Livebook 上に「Click me!」と書かれたボタンを作ることができます

```elixir
click_me = Kino.Control.button("Click me!")
```

![スクリーンショット 2023-03-27 0.17.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/35dc58d6-559f-97a4-2ec1-e2db2c811cf0.png)

ただし、この時点ではクリックしても何も起きません

以下のようにボタンをサブスクライブ（購読）することで、 Livebook 上でボタンをクリックイベントを受け取ることができるようになります

```elixir
Kino.Control.subscribe(click_me, :click_me)
```

以下のコードを実行すると、現在の Livebook で受け取ったメッセージを表示できます

```elixir
Process.info(self(), :messages)
```

最初の実行結果は以下のようになります

```elixir
{:messages, []}
```

「Click me!」ボタンを一度クリックし、 `Process.info` のセルを再度実行すると以下のようになります

```elixir
{:messages, [click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click}]}
```

ボタンをさらにクリックしてから再実行すれば、クリックした数だけメッセージを受け取ったことがわかります

```elixir
{:messages,
 [
   click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click},
   click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click},
   click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click},
   click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click},
   click_me: %{origin: "lg2skzimyf23jho3shol2xwgrnbcujwd", type: :click}
 ]}
```

## ボタンイベントの列挙 

次に「Click me again!」ボタンを作ります

```elixir
click_me_again = Kino.Control.button("Click me again!")
```

続いてこのボタンを for に渡してみます

```elixir
for event <- click_me_again do
  IO.inspect(event)
end
```

すると、セルが実行状態のままになります（無限ループになります）

この状態で「Click me again!」ボタンをクリックすると、ボタンをクリックするたびにイベントを受け取って内容を表示できます

![button_events.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e116e6be-4b2c-549f-0a48-04289c407c84.gif)

## Kino.Frame とアニメーション

`Kino.Frame.new()` で空っぽのフレーム（枠）を作ります

```elixir
frame = Kino.Frame.new()
```

![スクリーンショット 2023-03-27 0.31.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/06e212d7-3a95-9bf1-9355-95f8eadaa49e.png)

以下のコードを実行すると、上で実行結果に作られた空のフレームに 1 から 100 の範囲からランダムな数字が表示されます

```elixir
Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")
```

![スクリーンショット 2023-03-27 0.34.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5410042c-4737-f873-8b57-030651d58a4c.png)

`Kino.Frame.render` を再度実行すると、結果は上書きされます

`Kino.Frame.render` だったところを `Kino.Frame.append` に変更すると、上書きではなく追記になります

```elixir
Kino.Frame.append(frame, "Got: #{Enum.random(1..100)}")
```

![スクリーンショット 2023-03-27 0.36.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ce34781-8e29-1da5-5d71-88cdcb3adef0.png)

以下のように書くと、100ミリ秒毎に数字をカウントして表示します

```elixir
Kino.animate(100, fn i ->
  Kino.Markdown.new("**Iteration: `#{i}`**")
end)
```

![animate_md.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/29a58ea3-0e86-577d-9fe5-08934ba0b67e.gif)

以下のようにボタンを作ってから `Kino.animate` に渡すと、ボタンのクリックイベントの度にカウントして表示します

```elixir
button = Kino.Control.button("Click") |> Kino.render()

Kino.animate(button, 0, fn _event, counter ->
  new_counter = counter + 1
  md = Kino.Markdown.new("**Clicks: `#{new_counter}`**")
  {:cont, md, new_counter}
end)
```

![button_animate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/45d05020-d03c-21bd-a7c4-954c87472f8c.gif)

## 入力フォームのイベント受信

ここまでやってみたことを組み合わせてみます

まず空のフレームを用意します

```elixir
frame = Kino.Frame.new()
```

`Kino.Input.text` でテキスト入力を作り、それらをまとめて `Kino.Control.form` に渡すことで入力フォームを作ります

```elixir
inputs = [
  name: Kino.Input.text("Name"),
  message: Kino.Input.text("Message")
]

form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:message])
```

![スクリーンショット 2023-03-27 0.45.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/27a023ad-6f35-0dfe-3369-c95e0847448b.png)

この時点では入力しても何も起こりません

このフォームのイベントを `Kino.listen` でサブスクライブし、結果をフレームに表示します

名前とメッセージは必須入力とし、空の場合はエラーメッセージを表示します

```elixir
Kino.listen(form, fn %{data: %{name: name, message: message}, origin: origin} ->
  if name != "" and message != "" do
    content = Kino.Markdown.new("**#{name}**: #{message}")
    Kino.Frame.append(frame, content)
  else
    content = Kino.Markdown.new("_ERROR! You need a name and message to submit..._")
    Kino.Frame.append(frame, content, to: origin)
  end
end)
```

この状態で入力フォームにテキストを入れて「Send」ボタンをクリックすると、フレームにメッセージが追加されていきます

![form_animate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c0a7aa07-d0be-96df-cd07-351d527a4996.gif)

このように、 Kino を使うと Livebook 上で Web の入力フォームと、その結果表示が実装できます

## デプロイ（実行環境への配置）

いよいよ実装した Web 画面をデプロイしましょう

Livebook 左メニューのロケットアイコンをクリックします

![スクリーンショット 2023-03-27 0.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/557c1e42-cafb-6b23-9fc2-e9517c2e1082.png)

するとデプロイ用のメニューが出てくるので、 Slug (URL の末尾の部分) を入れて、「Deploy」ボタンをクリックします

![スクリーンショット 2023-03-27 1.00.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b3e7581e-619d-0d1f-a64e-d622ae12339f.png)

すると、下にデプロイ状況が表示されますが、いつまで経っても「Booting」のまま進みません

![スクリーンショット 2023-03-27 1.02.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1dd2c4f6-da98-d603-7f9f-d84e480def69.png)

これは現在のノートブックに無限ループが含まれているためです

一旦、デプロイ状況の右下ゴミ箱アイコンをクリックしてデプロイを削除します

無限ループを含んでいる「Enumerating controls」のセクションを全部消してから再度デプロイします

すると、デプロイ状況が「Running」になります

![スクリーンショット 2023-03-27 1.06.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/904fa21d-df0a-b1a6-8b77-15345cab6706.png)

表示されている URL をクリックしてデプロイした画面を開いてみましょう

![スクリーンショット 2023-03-27 1.08.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6031a567-ca3a-65a3-60db-2b82b15993d1.png)

実装した画面が表示されました

チャットもそのまま動きます

![chat_solo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f066aefa-c173-da08-e34c-9c8b9a30aadc.gif)

別のブラウザやシークレットウィンドウなどで同じ URL を開いてみましょう

パスワード保護をかけていたので、パスワード入力を求められます

![スクリーンショット 2023-03-27 1.12.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a564184d-52be-2e2d-89a3-ef49458f067f.png)

「Password-protected」テキスト入力右端のアイコンをクリックし、パスワードを表示します

![スクリーンショット 2023-03-27 1.13.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/936e53d6-3264-ad23-e303-d2d2ae7b5c73.png)

表示されたパスワードをコピーして入力し「Authenticate」をクリックすると、別ブラウザでも同じ画面を開くことができます

それぞれの入出力は当然同期して動きます

![ping_pong.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c03f6285-7107-15f4-31be-9e3fc0f403f7.gif)

ローカルホストで動かした Livebook を ngrok を使って公開すれば、チャットも外部に公開可能です

https://ngrok.com/

また、そもそも Livebook を Fly.io で動かしていればそのまま共有できます

https://fly.io/

## まとめ

とうとう Livebook もここまできたか、という感じです

まさか Web アプリ開発を対話的にやってしまうとは、、、

チュートリアルではチャットを作りましたが、 Livebook で出来るものならなんでもデプロイ可能なので、あんなことやこんなことまで出来そうですね