Phoenix Framework - Channel 日本語翻訳

[Phoenix Framework](https://github.com/phoenixframework/phoenix) は Rails 風なリッチなフレームワークである割にデフォルトでもそこそこ速いことが知られています．例えば次のような[ベンチマーク](https://github.com/mroth/phoenix-showdown#comparative-benchmark-numbers)があります．

しかし，それよりも私がおもしろいと思った特長に，リアルタイム通信機能がフレームワークに統合されている点があります．しかしまだ日本ではあまり知られておりません．そこで Phoenix Framework の最新版である v0.17.0 における [wiki にある Channel の記事](http://www.phoenixframework.org/v0.17.0/docs/channels)を日本語へと翻訳しました．

翻訳や内容におかしなところがあれば指摘お待ちしております :relaxed: 

----

チャンネルはフェニックスの本当に刺激的で強力な部分で，アプリケーションへ簡単にソフトリアルタイムな特徴を追加することができるようになります．チャンネルは簡単なアイディア - メッセージを送る，受けとる に基づいています．送信者はトピックスに関するメッセージをブロードキャストします．受信者はそれらのメッセージを得ることができるように，トピックを購読します．送信者と受信者は同じトピックにおいていつでも役割を切り替えることができます．

エリクサーはメッセージパッシングに基づいているので，メッセージを送受信するのになぜこのような余分なメカニズムが必要になるのか不思議に思うかもしれません．チャンネルでは，送信者も受信者もどちらもエリクサーのプロセスである必要がありません．私たちがチャンネルを介した通信で教えられるものに対しては何でもすることができます - JavaScriptのクライアント，iOSのアプリ，別のフェニックスアプリケーション，腕時計など．また，チャンネルを介してブロードキャストしたメッセージは多くの受信者に届くことがあります．エリクサーのプロセスではひとつひとつと通信します．

"チャンネル" は複数のコンポーネントからなる階層構造を省略して表した言葉です．概要がもう少しわかるように，それぞれのさわりの部分を見てみましょう．

## The Moving Parts

### Socket Handlers

フェニックスは，サーバとは単一の接続を持ち，その接続を介して複数のチャンネルソケットを持ちます．`web/channels/user_socket.ex`のようなソケットハンドラは，認証とソケット接続の識別，全てのチャンネルで利用するためのデフォルトのソケットを割り当てるためのモジュールです．

### Channel Routes

`web/channels/user_socket.ex`のようなファイルであるソケットハンドラへ，重複しないようにルートが定義されています．それらはトピック文字に適合し，適合したリクエストを与えられたチャンネルモジュールへと送ります．星型文字 `*` はワイルドカードとして働くので，次の例のルートでは `sample_topic:pizza` と `sample_topic:oranges` はどちらも `SampleTopicChannel` へと送られます．

```elixir
channel "sample_topic:*", HelloPhoenix.SampleTopicChannel
```

### Channels

チャンネルはクライアントからのイベントを処理するのでコントローラに似ていますが，鍵となる違いが 2 つあります．チャンネルのイベントは，着信および発信の両方向に行くことができます．チャンネルの接続は1つのリクエスト/レスポンスでは終わらず，持続します．チャンネルはフェニックスにおけるリアルタイム通信のコンポーネントが最も抽象化されたものです．

どのチャンネルも，これらの4つのコールバック関数 - `join/3`, `terminate/2`, `handle_in/3`, `handle_out/3` のうち1つかそれ以上の節を実装します．

### PubSub

フェニックスのPubSub層は，`Phoenix.PubSub`モジュール及び様々なアダプタと(アダプタの)Genserverで構成されています．これらのモジュールはチャンネル通信をまとめるための根幹となる関数 - トピックを購読する，トピックから退会する，そしてメッセージをトピックに乗せてブロードキャストする を含んでいます．

必要なら自分たちでPubSubアダプターを定義することができます．詳しくは[Phoenix.PubSub docs](http://hexdocs.pm/phoenix/)を参照してください．

フェニックスの内部で使うことを意図されて作られているモジュールであることは特筆すべき点です．チャンネルはそれらを使い，動作させるためにたくさんの働きを裏で行います．私たちのアプリケーションでは，エンドユーザーがそれらを直接使う必要があるようにすべきではありません．

### Messages

`Phoenix.Socket.Message`モジュールは，以下に示すキーが有効なメッセージとなる構造を定義します．[Phoenix.Socket.Message docs](http://hexdocs.pm/phoenix/Phoenix.Socket.Message.html) より．
- `topic` - 文字列のトピックかトピック:サブトピックのペアの名前空間，つまり "messages" か "messages:123"
- `event` - 文字列のイベント名、つまり "join"
- `payload` - JSON形式の文字列に載せたメッセージ
- `ref` - 着信イベントに応答するために使用される一意の文字列

### Topics

トピックスは文字列識別子です - 様々な層がメッセージを最後に正しい場所へ収まるようにするために利用する名前です．上で見たようにトピックではワイルドカードを使えます．これにより "topic:subtopic" という形式をうまく扱えるようになります．多くの場合，トピックを `"users:123"` のようにモデル層のレコード ID で構成するでしょう．

### Transports

トランスポート層は大事な足回りです．`Phoenix.Channel.Transport`モジュールはチャンネルを行き来する全てのメッセージのディスパッチを扱います． 

### Transport Adapters

デフォルトのトランスポートの仕組みはWebSocketを介して行われ，WebSocketが動作/利用できない場合はLongPollingを代替手段として用います．他のトランスポートアダプターも使え，実際のところアダプタの決まりにのっとるなら自分たちで書くこともできます．例としては`Phoenix.Transports.WebSocket`を見てください．

### Client Libraries

フェニックスは現在のところJavaScriptクライアントをリリースしており，1.0リリースではiOSとAndroidのクライアントも含める予定です．

## Tying it all together

単純なチャットアプリケーションを構築することでこれらの概念を結びつけてみましょう．ソケットをエンドポイントにマウントし，チャンネルルートへと通すところから初めましょう．

```elixir
# lib/hello_phoenix/endpoint.ex
defmodule HelloPhoenix.Endpoint do
  use Phoenix.Endpoint

  socket "/socket", HelloPhoenix.UserSocket
  ...
end

# web/channels/user_socket.ex
defmodule HelloPhoenix.UserSocket do
  use Phoenix.Socket

  channel "rooms:*", HelloPhoenix.RoomChannel
  ...
end
```

これでクライアントが`"rooms:"`から始まるトピックのメッセージを送るたび，メッセージはRoomChannelへと送られます．次にチャットルームのメッセージを管理するために`RoomChannel`モジュールを定義します．

## Joining Channels

あなたのチャンネルで最初に大事なことは，特定のトピックに参加するクライアントを認可することです．認可のために，`web/channels/room_channel.ex`へ`join/3`を実装しなければなりません．

```elixir
defmodule HelloPhoenix.RoomChannel do
  use Phoenix.Channel

  def join("rooms:lobby", auth_msg, socket) do
    {:ok, socket}
  end
  def join("rooms:" <> _private_room_id, _auth_msg, socket) do
    {:error, %{reason: "unauthorized"}}
  end

end
```

私たちのチャットアプリでは`"rooms:lobby"`トピックには誰でも繋ぐことができるように許可しています，しかし他の部屋はプライベートでデータベースから取得するような特別な認可が必要になるだろうと考えています．この演習ではプライベートなチャットルームについて考えることはしませんが，終わったら気軽に試してみてください．ソケットがトピックに参加するのを認可するには`{:ok, socket}`か`{:ok, reply, socket}`を返します．アクセスを拒否するには`{:error, reply}`を返します．トークンによる認可についての詳しい情報は[`Phoenix.Token` documentation](http://hexdocs.pm/phoenix/Phoenix.Token.html)にあります．

チャンネルができたので，`web/static/js/app.js`でクライアントとサーバーのやりとりを見ましょう．

```javascript
import {Socket} from "deps/phoenix/web/static/js/phoenix"

let socket = new Socket("/socket")
socket.connect()
let chan = socket.channel("rooms:lobby", {})
chan.join().receive("ok", chan => {
  console.log("Welcome to Phoenix Chat!")
})
```

ファイルを保存するとブラウザが自動的に再更新するはずです，フェニックスのライブリローダーのおかげです．感謝しましょう．もし全てがうまく動いているなら，ブラウザのJavaScriptコンソールで"Welcome to Phoenix Chat!"という表示を見るはずです．今，クライアントとサーバーは永続的な接続を介してやりとりをしています．チャットができるようにしてそれを使いこなせるようにしましょう．

`web/templates/page/index.html.eex` へ，チャットメッセージを取っておくコンテナを追加，また，それらを送るための入力フィールドを追加します．

```html
<div id="messages"></div>
<input id="chat-input" type="text"></input>
```

また `web/templates/layout/app.html.eex` にあるapplication layoutへjQueryを追加します:

```html
  ...
    <%= @inner %>

  </div> <!-- /container -->
  <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
  <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
```

さて，`app.js` へいくつかイベントリスナーを追加しましょう:

```javascript
let chatInput         = $("#chat-input")
let messagesContainer = $("#messages")

let socket = new Socket("/socket")
socket.connect()
let chan = socket.channel("rooms:lobby", {})

chatInput.on("keypress", event => {
  if(event.keyCode === 13){
    chan.push("new_msg", {body: chatInput.val()})
    chatInput.val("")
  }
})

chan.join().receive("ok", chan => {
  console.log("Welcome to Phoenix Chat!")
})
```

これから，enterが押されるのを検知し，チャンネルを介してイベントをメッセージの内容と一緒に`push`できるようにします．このイベントには"new_msg"と名前をつけました．ここで，新しいメッセージを待ち受け，来たらメッセージのコンテナに追加する，チャットアプリケーションの別の構成要素について扱ってみましょう．

```javascript
let chatInput         = $("#chat-input")
let messagesContainer = $("#messages")

let socket = new Socket("/socket")
socket.connect()
let chan = socket.channel("rooms:lobby", {})

chatInput.on("keypress", event => {
  if(event.keyCode === 13){
    chan.push("new_msg", {body: chatInput.val()})
    chatInput.val("")
  }
})

chan.on("new_msg", payload => {
  messagesContainer.append(`<br/>[${Date()}] ${payload.body}`)
})

chan.join().receive("ok", chan => {
  console.log("Welcome to Phoenix Chat!")
})
```

`chan.on`を使って`"new_msg"`イベントを待ちうけ，DOMにメッセージの内容を追加します．さて，全体像を完成させるためにサーバーの受信と発信イベントを処理しましょう．

## Incoming Events

`handle_in/3` で受信イベントを扱います．`"new_msg"`のようなイベント名でパターンマッチすることができ，それからクライアントがチャンネル越しに渡してくる内容を取得することができます．私たちのチャットアプリケーションでは，`rooms:lobby`にいる他の購読者へ通知するのに単に`broadcast!/3`で新しいメッセージを知らせるだけでよいです．

```elixir
defmodule HelloPhoenix.RoomChannel do
  use Phoenix.Channel

  def join("rooms:lobby", auth_msg, socket) do
    {:ok, socket}
  end
  def join("rooms:" <> _private_room_id, _auth_msg, socket) do
    {:error, %{reason: "unauthorized"}}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast! socket, "new_msg", %{body: body}
    {:noreply, socket}
  end

  def handle_out("new_msg", payload, socket) do
    push socket, "new_msg", payload
    {:noreply, socket}
  end
end
```

`broadcast!/3` はこの`socket`のトピックに接続されている全てのクライアントに通知し，各々の `handle_out/3` コールバックを実行します．`handle_out/3`は必須のコールバックではありませんが，ブロードキャストを各クライアントに届ける前にカスタマイズやフィルタするためにあります．`handle_out/3`のデフォルトでは，私たちが定義したのと同じように単にクライアントにメッセージを送るようになっています．この内容をここに含めたのは，発信イベントをフックすることでメッセージを強力にカスタマイズ/フィルタリングできるからです．どのように行うか見てみましょう．

## Intercepting Outgoing Events

私たちのアプリケーションではこれを実装しませんが，ユーザーが新規にルームに参加するユーザーに関するメッセージを無視することができると想像してみてください．私たちはこの振舞いを，送信イベントを止めたいとフェニックスへと明示的に伝えることで実装することができ，そこでこれらのイベントのために`handle_out/3`コールバックを定義しました．(もちろん，これは`User`モデルに`ignoring?/2`という関数があり，ユーザーを`assigns`マップから渡す前提で書いています)

```elixir
intercept ["user_joined"]

def handle_out("user_joined", msg, socket) do
  if User.ignoring?(socket.assigns[:user], msg.user_id) do
    {:noreply, socket}
  else
    push socket, "user_joined", msg
    {:noreply, socket}
  end
end
```

これが私たちの基本的なチャットアプリの全てとなります．複数のブラウザタブを立ち上げてメッセージを送るとブロードキャストされて全てのタブでメッセージが表示されるのを目撃することになるでしょう！

## Socket Assigns

connection構造体と似たように，`%Plug.Conn{}`はチャンネルソケットへ値を割り当てることができます．`Phoenix.Socket.assign/3` は便利なように `assign/3` としてチャンネルモジュールにインポートされています:

```elixir
  socket = assign(socket, :user, msg["user"])
```

ソケットは割り当てられた値を`socket.assigns`の中のマップに保持します．

## Example Application

作成したアプリケーションの例をみるには，このプロジェクト (https://github.com/chrismccord/phoenix_chat_example) をチェックアウトしてください．

ライブデモは (http://phoenixchat.herokuapp.com/) で見ることができます．