やってみよう!Elixir / Phoenix 関数型プログラミングの基礎⑤

webアプリ開発
目次

ルーター

インデックスページの作成

  • /にアクセスした場合、画面には「Greeting」と書かれたリンクが表示される。リンク先は/helloとする。
  • /helloにアクセスした場合、画面には「Hello World!」というメッセージと、「Back to Home」と書かれたリンクが表示される。リンク先は/とする。

indexアクションの追加

defmodule PhxSampleWeb.HomeController do
  use PhxSampleWeb, :controller

  #追加
  def index(conn, _params) do
    render(conn, :index)
  end
  #ここまで

  def hello(conn, _params) do
    render(conn, :hello)
  end
end

次に、このアクションのためのHEExテンプレートindex.html.heexを作成します。

<p>
  <a href="/hello" class="underline text-blue-700">Greeting</a>
</p>

続いて、「Hello World!」を表示するためのHEExテンプレートhello.html.heexを次のように書き換えます。

<p>
  <a href="/" class="underline text-blue-700">Back to Home</a>
</p>

router.exを書き換えます。

scope "/", PhxSampleWeb do
   pipe_through :browser

   get "/", HomeController, :hello
   get "/", HomeController, :index
   get "/hello", HomeController, :hello
  end
end

編集の結果、phx_sampleで有効なURLパスが/と/helloに増えます。GETメソッドでこれらのパスに対するHTTPリクエストが届いたとき、HomeControllerモジュールのindexアクションとhelloアクションがHTML文書を作って返します。

検証済みの経路(Verified routes)

indexアクションのためのHEExテンプレートindex.html.heexを次のように書き換えてください。

<p>
  <a href={~p(/hello)} class="underline text-blue-700">Greeting</a>
</p>

ブラウザの画面変化はありませんが、機能的には重要な変化が起きています。それはhref属性に指定されたパス/helloがルーティングに登録されているかどうかがコンパイル時にチェックされるということです。

<a>タグのhref属性に対して単に文字列を指定する代わりに、文字列の前にシギル~pを加え中括弧で囲んでいます。Elixirのコンパイラは、シギル~pが加えられた文字列が経路として正しいかどうかを検証し、正しくなければ警告を出します。この機能は検証された経路(Verified routes)と呼ばれます。

hello.html.heexの書き換え

helloアクションのためのHEExテンプレートhello.html.heexについても、href属性の値を中括弧とシギル~pを利用する形に書き換えてください。

<p class="text-red-500 font-bold text-2xl">Hello, {@name}!</p>
<p>
  <a href={~p(/)} class="underline text-blue-700">Back to Home</a>
</p>

リクエストパラメーター

  • インデックスページにある「Greeting」リンクを除去して、「Alice」、「Bob」、「Carol」という3つのリンクを<ul>タグを使った箇条書き形式で表示する。
  • 「Alice」がクリックされたら、「Hello Alice」と表示する
  • 「Bob」がクリックされたら、「Hello Bob」と表示する
  • 「Carol」がクリックされたら、「Hello Carol」と表示する

indexページの改造

indexアクションのためのHEExテンプレートindex.html.heexを次のように書き換えてください。

<ol class="list-disc ml-4">
  <%= for name <- ~w(Alice Bob Carol) do %>
    <li>
      <a href={~p(/hello/#{name})} class="underline text-blue-700">
        {name}
      </a>
    </li>
  <% end %>
</ol>

“Alice”,"Bob","Carol"という3個の要素を持つリストからforマクロで1個ずつ要素を取り出して、<li>…</li>で囲まれた<a>要素を生成しています。

HEExテンプレートの中でforマクロを使う場合の基本的な構文は次の通りです。

<%= for A <- B do %>
  C
<% end %>

典型的な使い方ではAに変数、Bにリストをあてはめます。そして、Cの中でAを埋め込むことにより、HTML断片の繰り返しを生成します。5行目の{name}により変数nameの値が埋め込まれます。

<a>href属性には次のような式が指定されています。

~p(/hello?name=#{name})

#{name} の部分は、"Alice" などの文字列で置き換わりますので、実際には /hello?name=Alice のような URL が href 属性の値になります。URL の末尾にある ?name=Alice の部分はクエリ文字列と呼ばれます。

クエリ文字列を含む URL へのリクエストがあると、ルーターはその部分をマップ型の値に変換してアクションに渡します。例えば、?name=Alice は次のようなマップに変換されます。

%{"name" => "Alice"}

このマップをリクエストパラメータ(request parameters)と呼びます。

helloアクションの改造

helloアクションhome_controller.exを次のように書き換えてください。

def hello(conn, %{"name" => name} = _params) do
  render(conn, :hello, name: name)
end

こうすることで、アクション hello がリクエストパラメータを受け取れるようになります。ブラウザから /hello?name=Alice という URL に対してリクエストが届くと、変数 name"Alice" がセットされます。

hello アクションのための HEEx テンプレートを次のように書き換えてください。

<p class="text-red-500 font-bold text-2xl">Hello, {@name}!</p>
<p>
  <a href={~p(/)} class="underline text-blue-700">Back to Home</a>
</p>

リダイレクション

例外Phoenix.ActionClauseError

前節で行った変更により JauntyGreeter は /hello?name=Alice のようなクエリ文字列の URL を扱えるようになりました。ここで考慮すべきことがあります。もしもユーザーがブラウザのアドレスバーからクエリ文字列を削除して Enter キーを押したら、どうなるでしょうか?

実際に試してみると、ブラウザにエラー画面が表示されます。

Phoenix.ActionClauseError という名前の例外が発生しています。エラー画面の中に次のようなメッセージがあります:

no function clause matching in JauntyGreeterWeb.HomeController.hello/2

関数 HomeController.hello/2 において、マッチする関数節(function clause)が存在しないと言っています。パターンマッチングを用いて関数が定義されているときに頻出するエラーメッセージです。

続くエラーメッセージをご覧ください:

The following arguments were given to JauntyGreeterWeb.HomeController.hello/2:

「次のような引数が関数 HomeController.hello/2 に与えられた」と言っています。

その下には与えられた引数の値が列挙されています。

# 1
%PlugConn{...}`

# 2
%{}

注目すべきは第 2 引数の値が空のマップ(%{})となっていることです。

以上の観察を踏まえて、関数 HomeController.hello/2 のコードを見返してみましょう。

 def hello(conn, %{"name" => name} = _params) do
   render(conn, :hello, name: name)
 end

第 2 引数が %{"name" => name} = _params と書かれています。第 2 引数として与えられたマップが "name" キーを持つ場合にのみこの関数節がマッチします。今は、第 2 引数に空のマップが与えられているので、マッチする関数節が存在せず、例外 Phoenix.ActionClauseError が発生したのです。

ホームページへのリダイレクション

ユーザーがブラウザのアドレスバーに /hello と入力したときには、強制的にホームページに遷移させることにしましょう。

heme_controller.ex を次のように書き換えてください

def hello(conn, _params) do
  redirect(conn, to: ~p(/))
end

Phoenix.Controller モジュールの関数 redirect/2 は、別の URL への強制遷移、すなわちリダイレクション(redirection)を発生させます。この関数は 2 つの引数を取ります:

  • Plug.Conn 構造体
  • :to オプションまたは :external オプション

リダイレクション先の URL がホスト名を含まない場合は :to オプションを、含む場合は :external オプションを指定してください。ここでは、to: ~p(/) を指定することで、ホームページに遷移させています。

パスパラメーター

この節では hello アクションの URL パスを /hello?name=Alice のような形式から /hello/Alice のような形式に変更します。

ルーティング設定の変更

router.ex を次のように書き換えてください

scope "/", JauntyGreeterWeb do
   pipe_through :browser

   get "/", HomeController, :index
   get "/hello", HomeController, :hello
  get "/hello/:name", HomeController, :hello
  end
end

追加された経路のパス /hello/:name に含まれる :name に注目してください。この部分は任意の文字列にマッチします。すなわち、/hello/Alice/hello/Bob が正しい URL パスとして認識されれます。そして、この可変部分にある文字列からマップ型の値が作られ、アクションに送られます。例えば、ブラウザが hello/Alice という URL にアクセスした場合、次のようなマップが作られます。

%{"name" => "Alice"}

これをパスパラメータと呼びます。パスパラメータとリクエストパラメータを総称して「パラメータ」と呼びます。

HEExテンプレートの書き換え

新たなルーティング設定に合わせて index.html.heex を次のように書き換えてください

<ol class="list-disc ml-4">
 <%= for name <- ~w(Alice Bob Carol) do %>
    <li>
     <a href={~p(/hello?name=#{name})} class="underline text-blue-700">
       <a href={~p(/hello/#{name})} class="underline text-blue-700">
         {name}
       </a>
    </li>
  <% end %>
</ol>

ブラウザでインデックスページを開き、従来どおり正常に機能することを確認してください。