Phoenix フレームワークと Ember.js を使用した Todo アプリケーションの作成

https://resolve.digital/blog/posts/creating-a-todo-application-using-the-phoenix-framework-and-ember-js/
を翻訳しました。
翻訳の誤りなどあればご指摘お待ちしております。

---

我々は常にクライアントに関連する技術に目を光らせています。本日は [Phoenix](http://www.phoenixframework.org/) と呼ばれるWebアプリケーション・フレームワークを見ていきます。 Phoenix は Elixir （あなたが Ruby を書いていたならば、いくつかの点でお馴染みと感じる、燃えるような早い関数型プログラミング言語）の上に構築されています。

このチュートリアルでは、サーバ上で Phoenix と Elixir を使用して、単純なクライアント-サーバ Web アプリケーションを作成していきます。データストアとして PostgreSQL を使用します。クライアントサイドでは、Ember Javascript フレームワークがユーザーインターフェースを提供し、サーバへ変更を伝えます。このコンポーネントの組み合わせは、[PEEP（Postgres, Elixir, Ember, Phoenix）スタック](https://medium.com/@j_mcnally/the-peep-stack-b7ba5afdd055)として知られるようになってきました。

構築する Todo アプリは次のようなものです：

![図](https://resolve.digital/images/large/To%20Do%20application%20screenshot.png)

##Phoenix アプリの作成
始める前に、[Phoenix インストール・ガイド](http://www.phoenixframework.org/docs/installation)に従って、必要なすべての依存関係がインストールされていることを確認してください。

####1) スケルトン・アプリの作成
最初に、新しい Phoenix アプリを作成するために、以下を実行しましょう：

```shell-session
$ mix phoenix.new phoenix_todos_api --no-brunch
$ cd phoenix_todos_api
```

API のみのアプリはアセット管理を必要としませんので、[Brunch.io](http://brunch.io/) を除いています。「Fetch and install dependencies?」（依存関係をフェッチしてインストール）のプロンプトには、 `Y` と答えます。

####2) データベースの作成
次に、データベースを作成します。 `config/dev.exs` と `config/test.exs` を確認して PostgreSQL サーバの正しい資格情報を指定してください。デフォルトの資格情報は、次のとおりです：

```
# ...
username: "postgres",
password: "postgres",
# ...
```

代わりに、正しい資格情報を使用して、以下を実行します：

```shell-session
$ mix ecto.create
```

「rebar」をインストールするように指示される場合があります、継続してこれを受け入れます。アプリのコンパイルが完了した後、次のように表示されます：

```shell-session
$ The database for PhoenixTodosApi.Repo has been created.
```

####3) サーバの起動
次に、Phoenix の Web サーバを起動しましょう：

```shell-session
$ mix phoenix.server
```

これで、アプリに http://localhost:4000 でアクセス可能なはずで、Phoenix のデフォルトのランディング・ページが表示されます。

`Ctrl + C` を2回押すことでサーバが終了します。

####4) Todoリソースの生成
私たちの API の単一のリソースは、Todo 項目です。各 Todo 項目は `title` 文字列と `is_completed` ブール値を格納します。Phoenix は、JSON リソースのために必要なすべてのファイルを作成するためのジェネレータを提供し、それは以下で呼び出すことができます：

```shell-session
$ mix phoenix.gen.json Todo todos title:string is_completed:boolean
```

ジェネレータからの出力は、作成したファイルをリストします：

```shell-session
* creating web/controllers/todo_controller.ex
* creating web/views/todo_view.ex
* creating test/controllers/todo_controller_test.exs
* creating web/views/changeset_view.ex
* creating priv/repo/migrations/20150929044516_create_todo.exs
* creating web/models/todo.ex
* creating test/models/todo_test.exs
```

ご覧のとおり、コントローラ、モデル、それらに対応するテスト・ファイルが作成されます。また、データベースを更新するためのマイグレーションを取得し、最終的に、2つのビューも作成されます：1つは新しいコントローラ用の `TodoView` であり、もう1つはモデルのバリデーション・エラーをレンダリングするためにすべての JSON コントローラによって使用される、より一般的な `ChangesetView` です。

####5) ルートの追加
次に、入ってくる要求を新しく生成された `TodoController` に接続する方法を Phoenix に伝えるために、ルートを設定する必要があります。

`web/router.ex` を開きます。一番下に新しいスコープを追加します：

```
defmodule PhoenixTodosApi.Router do
  # ...
  scope "/", PhoenixTodosApi do
    pipe_through :api
    resources "/todos", TodoController, except: [:new, :edit]
  end
end
```

####6) データベースのマイグレート
次に、`Todos` を格納するテーブルを作成するために、データベースをマイグレートする必要があります。

```shell-session
$ mix ecto.migrate
…
17:55:14.586 [info]  == Running PhoenixTodosApi.Repo.Migrations.CreateTodo.change/0 forward
17:55:14.586 [info]  create table todos
17:55:14.617 [info]  == Migrated in 0.2s
```

####7) データベースに初期データ導入
再びサーバを起動します：

```shell-session
$ mix phoenix.server
```

http://localhost:4000/todos で Todo リストにアクセスすることができますが、まだ何も持っていないことに気づくでしょう。

それでは、いくつかの初期データをデータベースに導入することによって、この問題を修正しましょう。`priv/repo/seeds.exs` に次の行を追加します：

```
alias PhoenixTodosApi.Repo
alias PhoenixTodosApi.Todo

Repo.insert!(%Todo{title: "Create the Phoenix App", is_completed: true})
Repo.insert!(%Todo{title: "Prepare the Ember App", is_completed: false})
Repo.insert!(%Todo{title: "Ensure the Apps Work Together", is_completed: false})
```

データを導入するために以下を実行します：

```shell-session
$ mix run priv/repo/seeds.exs
```

これで、http://localhost:4000/todos から、いくつかの Todo が見られます。

```json
{
  "data": [{
    "title": "Create the Phoenix App",
    "is_completed": true,
    "id": 1
  }, {
    "title": "Prepare the Ember App",
    "is_completed": false,
    "id": 2
  }, {
    "title": "Ensure the Apps Work Together",
    "is_completed": false,
    "id": 3
  }]
}
```

以上により、基本的な API が稼働可能になりました。次はアプリケーションのクライアントサイドに取りかかります。

##Emberアプリの準備
クライアントサイドのために、[TodoMVC.com](http://todomvc.com/) の [Ember CLI](http://www.ember-cli.com/) 実装を使用します。

以下の手順を進める前に、Ember CLI の [Getting Started](http://www.ember-cli.com/user-guide/#getting-started) のインストールの部分に従うようにしてください。

####1) クローンと依存関係のインストール
Ember アプリのリポジトリのクローンを作成し、プロジェクトのディレクトリに移動します。

```shell-session
$ git clone https://github.com/ember-cli/ember-cli-todos.git
$ cd ember-cli-todos
```

npm 依存関係をインストールします：

```shell-session
$ npm install
```

そして、bower 依存関係もインストールします：

```shell-session
$ bower install
```

####2) Ember サーバの起動
必要なものがすべてインストールされたことを確認するために、アプリを起動します：

```shell-session
$ ember server
```

ブラウザで http://localhost:4200 を開きます。[^1]この時点で Todo を追加することができますが、メモリ内のストレージを使用しており、ページをリフレッシュした場合 Todo は永続化されません。

####Ember を Phoenix へ接続
ここまでで、Phoenix で書かれたサーバサイドの API アプリと、Ember で書かれたクライアントサイドのアプリができました。しかし、2つのアプリは互いに会話していません。次に、2つのアプリを互いに接続し、Todo を永続化します。

####Ember アプリへの変更
#####1) Phoenix を使用するように Ember Data を設定
メモリ内のストレージを使用する代わりに、[Ember Data](https://github.com/emberjs/data) が Phoenix API をバックエンドとして使用するように設定します。そのためには、 `app/adapters/application.js` の内容を以下のように置き換えます：

```js
import config from '../config/environment';
import FixtureAdapter from 'ember-data-fixture-adapter';
import DS from 'ember-data';

var adapter;
if (config.environment === 'test') {
  adapter = FixtureAdapter.extend({});
} else {
  adapter = DS.RESTAdapter.extend({
    host: 'http://localhost:4000'
  });
}

export default adapter;
```

アプリをテスト環境で起動する場合、元の `FixtureAdapter` を使用し続けますが、他のすべての環境では `RESTAdapter` を使用して http://localhost:4000 で Phoenix API を参照するように指示していることに注意してください。

#####2) シリアライザの設定
デフォルトでは、Ember Data は、JSON のキー名にキャメル・ケースを使用することを期待しますが、私たちの Phoenix API ではスネーク・ケースを使用しています。この問題を回避するために、また、Ember が使用するシリアライザをカスタマイズする必要があります。

まず `serializers` ディレクトリを作成します：

```shell-session
$ mkdir -p app/serializers/
```

そしてその後、`app/serializers/application.js` ファイルを作成し、以下のコードをコピーします：

```js
import config from '../config/environment';
import DS from 'ember-data';
import Ember from 'ember';

var serializer;
if (config.environment === 'test') {
  serializer = DS.JSONSerializer.extend({});
} else {
  serializer = DS.RESTSerializer.extend({
    keyForAttribute(attr) {
      return Ember.String.decamelize(attr);
    }
  });
}

export default serializer;
```

ここでもテスト環境では異なるシリアライザを使用していますが、それ以外の場合、Ember Data モデルの属性をスネーク・ケースに変換するために、Ember のビルトイン `decamelize` アダプターを使用した、カスタム `RESTSerializer` を提供します。これはかなり単純なアプローチですが、私たちの目的のためには機能します。

#####3) CORSの設定
クライアントとサーバを異なるポートからホストしようとしているので、それらが通信できるように、セキュリティポリシーを適切に配置する必要があります。まず、Ember サーバが提供している [CSP ヘッダ](https://en.wikipedia.org/wiki/Content_Security_Policy) を更新します。開発環境のために、`config/environment.js` の `ENV.contentSecurityPolicy` の内容を変更します：

```js
ENV.contentSecurityPolicy = {
  // ...
  'connect-src': "'self' http://localhost:*",
  // ...
}
```

ここで行った変更は、`connect-src` ルールに、ローカルホスト上の任意のポートへの接続を許可する `http://localhost:*` を追加したことです。

また、Phoenix アプリに [CORS ヘッダ](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)を追加する必要があり、そのためにはEmber アプリが実行されるポートを知る必要があります。それでは、プロジェクトの `.ember-cli` 設定ファイル（Ember アプリのルートディレクトリにあります）でポートを9000に設定しましょう：

```js
{
  // ...
  "port": 9000
}
```

####Phoenix アプリへの変更
アプリに [CORS ヘッダ](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)を追加して、Ember がアプリに対してリクエストすることをブラウザが許可するようにする必要があります。

#####1) cors_plug の設定
[cors_plug](https://github.com/mschae/cors_plug) と呼ばれるオープンソースの [plug](http://www.phoenixframework.org/docs/understanding-plug) を使用することでこれを行うことができます。これを使用するためには、`mix.exs` で依存関係に追加する必要があります：

```
def deps do
  # ...
  {:cors_plug, "~> 0.1.3"},
  # ...
end
```

そして、以下でインストールします：

```shell-session
$ mix deps.get
```

一旦インストールされれば、アプリのエンドポイント（`lib/phoenix_todos_api/endpoint.ex` にあります）に追加することができます：

```
defmodule PhoenixTodosApi.Endpoint do
  # ...
  plug CORSPlug, [origin: "http://localhost:9000"] # add this line
  plug PhoenixTodosApi.Router
end
```

パイプラインで `PhoenixTodosApi.Router` の上に置き、Ember アプリが動くように構成したポート使用して、origin として `http://localhost:9000` を渡しています。

#####2) JSON ルート・キーの設定
Ember で `RESTAdapter` は、JSONレスポンスをそれがデシリアライズされるべき Ember Data モデルにマッピングするためのルート・キーを必要とします。Todo モデルでは、Todo のコレクションは `todos` 、単一の Todo は `todo` です。しかし、`mix phoenix.gen.json Todo` を実行したとき、常に `data` をルート・キーとして応答する `TodoView` が設定されています。

では、Todo のコレクションのルート・キーとして `todos` を、単一の Todo のルート・キーとして `todo` を使用するように、JSON 出力を変更しましょう。まず、この変更を必要とするコントローラのテストを更新します。`test/controllers/todo_controller_test.exs` を開き、`["data"]` のすべてのインスタンスを `["todo"]` で置き換えます（4つの置き換えがあるはずです）。そして、 `:index` アクションのためのテストを見つけ、期待されるルートを `["todo"]` から `[todos]` に変更します：

```
test "lists all entries on index", %{conn: conn} do
  conn = get conn, todo_path(conn, :index)
  assert json_response(conn, 200)["todos"] == []
end
```

コントローラのテストを実行すると、4つの失敗が表示されます：

```shell-session
$ mix test test/controllers/todo_controller_test.exs
...
Finished in 0.4 seconds (0.3s on load, 0.1s on tests)
8 tests, 4 failures
```

テストをパスするには、`index.json` と `show.json` ビューで指定されているルート・キーを変更します。これらは `web/views/todo_view.ex` にあります：

```
def render("index.json", %{todos: todos}) do
  # '%{data:' replaced with '%{todos:':
  %{todos: render_many(todos, PhoenixTodosApi.TodoView, "todo.json")}
end

def render("show.json", %{todo: todo}) do
  # '%{data:' replaced with '%{todo:':
  %{todo: render_one(todo, PhoenixTodosApi.TodoView, "todo.json")}
end
```

再度テストを実行し、すべてパスすることを確認しましょう：

```shell-session
$ mix test test/controllers/todo_controller_test.exs
...
Finished in 0.4 seconds (0.3s on load, 0.1s on tests)
8 tests, 0 failures
```

これで Phoenix アプリは Ember アプリからのリクエストを受け容れる準備ができました。

## 一緒にアプリをテスト
各々を起動することでアプリが一緒に動作することを確認できます。Phoenix を起動します：

```shell-session
$ mix phoenix.server
```

つづけて Ember を起動します：

```shell-session
$ ember server
```

そして、ブラウザで http://localhost:9000 を開きます。

Ember アプリに、先ほど Phoenix アプリで追加したデータがあるはずです。Ember アプリで加えた変更は Phoenix アプリに永続化されます。`mix phoenix.server` からのログ出力を見ることでこの動作を確認できます。ページをリフレッシュしても、変更は永続化されています。

---
[^1]: 私（翻訳者）の環境ではここで Ember のビルドエラーが発生しました。 bower で正しくインストールされなかった moment を手動でインストールすることで、エラーが解消しました。`$ bower install moment`