16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ElixirAdvent Calendar 2019

Day 21

LiveViewでElixirを評価する(1)

Last updated at Posted at 2019-12-20

この記事は、「Elixir Advent Calendar 2019」21日目の記事です。
昨日は @enerickさんの「params 入門」でした。

はじめに

折角Elixirを触り始めたので、PhoenixとLiveviewも試してみました。
思ったより風呂敷が広がり過ぎてしまったので分けます。

当初、某合宿イベントのWebサイトと運営全般を一括管理できるCMS的なものを作ろうと思っていたのですが、LiveViewを触り始めた頃に、試しにTextareaにElixirを食わして、eval_stringした結果を反映、みたいなことをやってみると思った以上にサクサク動いて惚れてしまいました。

LiveViewでのコード評価

まずプロジェクトを作ります。

mix phx.new liveeval --no-ecto
 > Fetch and install dependencies? [Yn] Y

Liveview環境の下準備

次にLiveviewの設定をしていきます。
とりあえずhttps://github.com/phoenixframework/phoenix_live_view/blob/master/guides/introduction/installation.md の手順通りで。
英語が苦手でもElixirのトコは読めますよね。私も英語はさっぱりですが、コードを拾い読むだけで割となんとかなります。日本語の情報は古くなっているものもあるので、困ったらまず原典です。

まず、mix.exsにLiveviewを入れます。githubのURLを指定する方法もありますが、結構ライブラリが更新されるようなので、素直にバージョン指定した方が無難です。

mix.exs
  defp deps do
    [
   ... # 前半省略
      {:phoenix_live_view, "~> 0.4.1"},
      {:floki, ">= 0.0.0", only: :test}
    ]
  end

saltの生成

mix phx.gen.secret 32
 > AaLfpv964DzcET6IFfkM/vLXcrmKaH6D # <これをコピーしておく

生成された塩をconfig/config.exに設定とともに追加します。

config/config.ex
# Configures the endpoint
config :liveeval, LiveevalWeb.Endpoint,
  url: [host: "localhost"],
  live_view: [signing_salt: "AaLfpv964DzcET6IFfkM/vLXcrmKaH6D"] # <この行を追加し、後半の文字列を先ほどの文字列で置き換える
  secret_key_base: "***ここはデフォルトのまま***",
  render_errors: [view: LiveevalWeb.ErrorView, accepts: ~w(html json)],
  pubsub: [name: Liveeval.PubSub, adapter: Phoenix.PubSub.PG2],

次、/lib/liveeval/routes.ex。一行追加します。

/lib/liveeval/router.ex
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug Phoenix.LiveView.Flash   #<ここに追加
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

ここはパイプライン的に評価されるようなので、順番大事です。(一度ハマりました)

lib/liveeval_web.ex、controller, view, routerにそれぞれ設定を加えます。

lib/liveeval_web.ex
def controller do
  quote do
    ...
    import Phoenix.LiveView.Controller
  end
end

def view do
  quote do
    ...
    import Phoenix.LiveView,
      only: [live_render: 2, live_render: 3, live_link: 1, live_link: 2,
             live_component: 2, live_component: 3, live_component: 4]
  end
end

def router do
  quote do
    ...
    import Phoenix.LiveView.Router
  end
end

/lib/liveeval_web/Endpoint.exの最初の方に追加し、最後のPlug.Sessionのあたりを修正します。

/lib/liveeval_web/Endpoint.ex
defmodule Liveeval.Endpoint do
  use Phoenix.Endpoint
  @session_options [
    store: :cookie,
    key: "_liveeval_key",
    signing_salt: "Bqw6yIXz"
  ]

  socket "/live", Phoenix.LiveView.Socket, 
  websocket: [connect_info: [session: @session_options]]
 
 ...

  plug Plug.Session, @session_options # 変更
end

LiveView_pageの追加

/lib/liveeval_web/live/live_page.ex
defmodule LiveevalWeb.LivePage do
  use Phoenix.HTML
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    <form phx-change="change" >
    <input type="text" name="text" value=<%= @text %> ></input>
    </form>
    <%= @text %>
    """
  end

  def mount(_session, socket) do
    {:ok, assign(socket, text: "")}
  end

  def handle_event("change", %{"text" => text}, socket) do
    {:noreply, assign(socket, text: text)}
  end
end

js周り

/assets/package.json
  "dependencies": {
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html",
    "phoenix_live_view": "file:../deps/phoenix_live_view"  <この行を追加
  },

package.jsonを書き換えたので更新しておきましょう。

$ npm install --prefix assets
/assets/js/app.js
import { Socket } from 'phoenix';
import LiveSocket from 'phoenix_live_view';
import css from "../css/app.css"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");

let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }});
liveSocket.connect()
/assets/css/app.css
@import "../../deps/phoenix_live_view/assets/css/live_view.css";

多分こんな感じでLiveViewが動く状態になってるはずです。一度実行してみましょう。

$ mix phx.server

テキストボックスが表示され、入力したテキストがすぐ下に反映されれば成功です。

LiveViewでCode.string_evalするコード

変更ファイル一覧

  • assets/
    • css/app.css
    • css/phoenix.css
    • js/app.js
  • lib/liveeval_web/
    • controllers/page_controller.ex
    • live/live_page.ex
    • router.ex

詳細は、このコミットを参照ください。
少しややこしいlive_page.exとapp.jsだけ掲載します。

/assets/js/app.js
import { Socket } from 'phoenix';
import LiveSocket from 'phoenix_live_view';
import css from "../css/app.css"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");

let Hooks = {}
Hooks.MyHook = {
    mounted() {
        this.el.addEventListener("input", e => {
            this.pushEvent("change", { "code": this.el.value })
        })
    }
}

let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks });
liveSocket.connect()

"phx-change"では<form>タグにしか付与できず、テキストエリアのイベントを拾えません。<form>のままでもいいのですが、Enter入力時にページが遷移してしまい、内容が消えてしまいます。

"phx-hook"を利用することで、任意のjavascriptと関連付けられ、mountedでロードされたタイミングをjs側でhookできます。またliveviewにイベントを投げることもできます。
ここではtextareaのinputイベントを拾い、changeというイベント名で、入力内容をliveviewに送信しています。

lib/liveeval_web/live/live_page.ex
defmodule LiveevalWeb.LivePage do
  use Phoenix.HTML
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    <textarea phx-hook="MyHook" class="elixir-codeblock" name="code" value="<%= @code %>" placeholder="input elixir code."> </textarea>
    <div class="result-block <%= @status %>"><%= @result %></div>
    """
  end

  def handle_params(%{"id" => id}, _url, socket) do
    socket = assign(socket, id: id)
    {:noreply, socket}
  end

  def mount(_session, socket) do
    {:ok, assign(socket, code: "", result: "", status: "empty")}
  end

  def handle_event("change", %{"code" => code}, socket) do
    send(self(), {:submit, code})
    {:noreply, assign(socket, code: code)}
  end

  def handle_info({:submit, code}, socket) do
    cleansing = String.replace(code, "System", "")

    [status, result] =
      try do
        {term, _} = Code.eval_string(cleansing)

        case term do
          nil -> ["empty", term]
          _ -> ["ok", Kernel.inspect(term)]
        end
      rescue
        e in _ ->
          ["error", Kernel.inspect(e)]
      end

    {:noreply, assign(socket, result: result, status: status)}
  end
end

liveview側ではhandle_eventで受け取り、評価結果をstatus, resultとしてhtmlに反映しています。

これで、ブラウザ上で入力したElixirコードが即時評価され、結果が表示されます。

liveeval.gif

でもeval_stringって・・

Code.eval_stringの説明に以下のような警告があります。

Warning: string can be any Elixir code and will be executed with the same privileges as the Erlang VM: this means that such code could compromise the machine (for example by executing system commands). Don't use eval_string/3 with untrusted input (such as strings coming from the network).
(【適当訳】けいこく:突っ込んだコードはVMと同権限で動きます。例えばSystem commandとかも実行できちゃいます。信用できるコード以外は突っ込んじゃだめです。ネットワーク越しに食わせるとかすんなよ!)

とのことです。危ないです。
でも、リアルタイムにElixirが評価されるのめっちゃ楽しい、この楽しさを共有したい・・。

とりあえず、評価前に気休め程度にSystemだけでも排除しときましょう。

sanitized = String.replace(code,"System","")

とりあえず素直にはSystem.haltとかやっても落ちなくなりました。アレコレやれば抜けれそうですが、そこまでしてシステムを落としたい人はElixir好きの人たちの中には居ないと信じてます。

とはいえ、このまま公開するのも怖いです。もう少し悪いことがしにくい環境を作りましょう。っと、この先も結構長くなってしまったので別記事に分けます。(すみません、完成まではたどり着けませんでした。)

おわりに

ノリで始めたら結構大きくなってしまい記事化するのが大変でしたが、Dockerとの連携など個人的にいろいろと勉強になりました。
Elixirは完全に理解したんですが、フロントエンドは何が分からないのか分からない状態で混乱します。

類似サービスは多くあるものの、やっぱりLiveviewのリアルタイム評価は面白いですね。
Elixirの教育やオンラインでのやり取りに結構いいプラットフォームになるんじゃないかと考えています。実運用するとなるとそれなりにマシンスペックが要りそうなので、どこかにホスティングして貰えませんかね。

今後、以下のような拡張を妄想してます。

  • 入力コードの永続化
  • 複数人が同じコードをリアルタイム共有
  • VM間(別コード間)の通信
  • 外部ライブラリの読み込み
  • git/gist連携

明日は@piacerexさんです。

16
2
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
16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?