Phoenix and ReactでTrelloをクローンします。 パート1〜3

画像






Trelloは私のお気に入りのアプリの1つです。 私はそれを開始以来使用しており、その動作方法、シンプルさと柔軟性がとても気に入っています。 新しいテクノロジーの研究を開始するたびに、実際の問題を解決するために研究したすべてを実践できる本格的なアプリケーションを作成し、これらのソリューションを確認することを好みます。 そのため、 ElixirとそのPhoenixフレームワークの勉強を始めたとき、出会ったこの素晴らしい資料をすべて実際に使用し、シンプルだが機能的なTrelloの献身を実装する方法のガイドとして共有する必要があることに気付きました。











翻訳者からのメモ

年の初め、エリクサーとフェニックスフレームワークに精通することを決めて、私はエリクサー、フェニックス、Reactを使用したTrelloクローンの実装に関する興味深いシリーズの記事でネットに出会いました。 私にはかなり面白そうで、ロシア語の翻訳は見つかりませんでしたが、共有したいと思いました。 最後に、両手が翻訳に到達しました。







私はReactエコシステムに完全に不慣れであることに注意する必要があります。この部分は現状のままです。 それに加えて、エリクサー/フェニックスのいくつかの瞬間はこの間に変化しました-プロジェクトはまだ立ち上がっていません。 また、Angular2 <-> Phoenix Channels <-> Elixir / Phoenix Framework bunchをやっているので、Angular2を使用してフロントエンドを実装し、それに関する記事を公開するための将来の時間を見つけたいと考えています。







私の意見では、元のサイクルでは記事ブロックが短すぎるため、ここでの1つの出版物には複数の部分が含まれ、元のリンクは小見出しの横にあります。







異議がある場合は、用語の元の名前を提供します。翻訳に矛盾がある場合は、代替文をご容赦ください。 エラー、タイプミス、不正確さの修正も歓迎します。







そして、序文を複製したことをおizeび申し上げます。ネタバレの下でも、キャットの前に作者からメモと序文を入れることはできませんでした。 導入がより重要であると決定しました。







技術スタックの紹介と選択



オリジナル







Trelloは私のお気に入りのアプリの1つです。 私はそれを開始以来使用しており、その動作方法、シンプルさと柔軟性がとても気に入っています。 新しいテクノロジーの研究を開始するたびに、実際の問題を解決するために研究したすべてを実践できる本格的なアプリケーションを作成し、これらのソリューションを確認することを好みます。 そのため、 ElixirとそのPhoenixフレームワークの勉強を始めたとき、出会ったこの素晴らしい資料をすべて実際に使用し、シンプルだが機能的なTrelloの献身を実装する方法のガイドとして共有する必要があることに気付きました。







私たちは何をするつもりですか



実際、既存のユーザーがログインして複数のボードを作成し、それらを他のユーザーと共有してリストやカードを追加できる1ページのアプリケーションを作成します。 ボードを表示すると、接続されているユーザーが表示され、変更はすぐに自動的に-Trelloスタイルで-各ユーザーのブラウザーに反映されます。







現在の技術スタック



フェニックスnpmを使用して静的リソースを管理し、 BrunchまたはWebpackを使用してすぐにそれらを収集するので、単一のコードベースを維持しながらフロントエンドとバックエンドを完全に分離するのは非常に簡単です。 したがって、バックエンドには次のものを使用します。









そして、1ページのフロントエンドアプリケーションを作成するには:









さらにいくつかのElixir依存関係とnpmパッケージを使用しますが、プロセスの後半でそれらについて説明します。







このスタックはなぜですか?



Elixirは、 Erlangに基づいた非常に高速で強力な関数型言語であり、Rubyに非常に類似した使いやすい構文を備えています。 彼は非常に信頼性が高く、並列処理に特化しており、仮想マシンのおかげでErlang( Erlang VMBEAM-およそTranslator )は数千の並列プロセスに対応できます。 私はElixirを初めて使用するので、まだ学ぶべきことがたくさんありますが、すでに学んだことから、非常に印象的であると言えます。







現在、Elixirで最も人気のあるWebフレームワークであるPhoenixを使用します。これは、 Rails Web開発で導入されたポイントや標準の一部を実装するだけでなく、上記の静的リソースの管理方法など、他の多くのクールな機能も提供します。そして、私にとって最も重要なことは、複雑さや追加の外部依存関係のないwebsocketを使用した組み込みのリアルタイム機能です(そして、信じられますが、時計のように機能します )。







同時に、 Reactreact-router、およびReduxを使用します。これは、この組み合わせを使用して単一ページのアプリケーションを作成し、その状態を管理するのが大好きだからです。 CoffieScriptをいつものように使用する代わりに、新しい年( 記事は2016年1月初旬-約Translator )にES6とES7で作業したいので、これを開始して参加する絶好の機会です。







最終結果



アプリケーションは、4つの異なるビューで構成されます。 最初の2つは、登録画面とログイン画面です。







ログイン







メイン画面には、ユーザー自身のボードと、他のユーザーが接続したボードのリストが含まれます。







ボードリスト







そして最後に、ボードのプレゼンテーションでは、すべてのユーザーが誰がそれに接続しているかを確認でき、リストとカードを管理できます。







ボードの内容







しかし、十分な話。 2番目のパートの準備を開始できるように、ここでやめましょう。新しいフェニックスプロジェクトの作成方法、ブランチの代わりにWebpackを使用するために変更する必要があるもの、フロントエンドのフレームワークの構成方法について説明します。









Phoenix Frameworkプロジェクトの初期セットアップ



オリジナル







したがって、現在のテクノロジースタックを選択したら、新しいPhoenixプロジェクトを作成することから始めましょう。 これを行う前に、 ElixirPhoenixがすでにインストールされている必要があるため、 インストール手順については公式サイトを使用してください







Webpackを使用した静的リソース



Ruby on Railsとは異なり、 Phoenixには独自のリソース処理パイプラインがありません(アセットパイプライン、 一部のロシア語Railsリソースは用語を「ファイルパイプライン」として翻訳します-ほぼ翻訳者 )。そして柔軟。 Brunchを使用する必要がないのは素晴らしいことです。これが望ましくない場合は、 Webpackを使用できます。 私はBrunchを扱ったことがないので、代わりにWebpackを使用します。







Phoenixには、ブランチに必要なnode.jsオプションの依存関係として含まれていますが、Webpackにはnode.jsも必要なので、後者をインストールしてください。







ブランチなしで新しいPhoenixプロジェクトを作成します。







$ mix phoenix.new --no-brunch phoenix_trello ... ... ... $ cd phoenix_trello
      
      





さて、今ではリソース構築ツールのない新しいプロジェクトがあります。 新しいpackage.json



ファイルを作成し、開発用の依存関係としてWebpackをインストールします( dev依存関係-コメント トランスレーター ):







 $ npm init ... (   Enter         ) ... ... $ npm i webpack --save-dev
      
      





package.json



は次のようになります。







 { "name": "phoenix_trello", "devDependencies": { "webpack": "^1.12.9" }, "dependencies": { }, }
      
      





プロジェクトの場合、多数の依存関係が必要なので、ここですべてをスクロールするのではなく、プロジェクトリポジトリのソースファイルを見て、そこからpackage.json



コピーしてください。 次のコマンドを実行して、すべてのパッケージをインストールする必要があります。







 $ npm install
      
      





また、 webpack.config.js



構成ファイルを追加して、 webpack.config.js



リソースの収集方法を指示する必要があります。







 'use strict'; var path = require('path'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var webpack = require('webpack'); function join(dest) { return path.resolve(__dirname, dest); } function web(dest) { return join('web/static/' + dest); } var config = module.exports = { entry: { application: [ web('css/application.sass'), web('js/application.js'), ], }, output: { path: join('priv/static'), filename: 'js/application.js', }, resolve: { extesions: ['', '.js', '.sass'], modulesDirectories: ['node_modules'], }, module: { noParse: /vendor\/phoenix/, loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { cacheDirectory: true, plugins: ['transform-decorators-legacy'], presets: ['react', 'es2015', 'stage-2', 'stage-0'], }, }, { test: /\.sass$/, loader: ExtractTextPlugin.extract('style', 'css!sass?indentedSyntax&includePaths[]=' + __dirname + '/node_modules'), }, ], }, plugins: [ new ExtractTextPlugin('css/application.css'), ], }; if (process.env.NODE_ENV === 'production') { config.plugins.push( new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({ minimize: true }) ); }
      
      





ここでは、2つのwebpackエントリポイントが必要であることを示します。1つはJavaScript用、もう1つはスタイルシート用で、両方ともweb/static



ディレクトリにあります。 出力ファイルはpriv/static



作成されpriv/static



ES6 / 7およびJSXのいくつかの機能を使用するため、これらの目的のために作成されたいくつかのプリセットでBabelを使用します。







最後のステップは、開発サーバーが起動するたびにWebpack 起動するようPhoenixに指示することです。これにより、Webpackは開発プロセス中に変更を追跡し、フロントエンドビューによって参照される対応するリソースファイルを生成できます。 これを行うには、「observer」の説明をconfig/dev.exs









 config :phoenix_trello, PhoenixTrello.Endpoint, http: [port: 4000], debug_errors: true, code_reloader: true, cache_static_lookup: false, check_origin: false, watchers: [ node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color"] ] ...
      
      





開発サーバーを起動すると、 Webpackも機能し、変更を追跡していることがわかります。







 $ mix phoenix.server [info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000 Hash: 93bc1d4743159d9afc35 Version: webpack 1.12.10 Time: 6488ms Asset Size Chunks Chunk Names js/application.js 1.28 MB 0 [emitted] application css/application.css 49.3 kB 0 [emitted] application [0] multi application 40 bytes {0} [built] + 397 hidden modules Child extract-text-webpack-plugin: + 2 hidden modules
      
      





別のこと。 priv/static/js



ディレクトリを調べると、 phoenix.js



ファイルがphoenix.js



ます。 このファイルにはwebsocket



channels



を使用するために必要なものがすべて含まれているため、必要に応じて接続できるようにweb/static/js



ソースを使用してベースディレクトリに移動します







メインのフロントエンド構造



これで、プログラミングを開始するためのすべてができました。 まず、フロントエンドアプリケーション構造を作成することから始めましょう。これには、特に次のパッケージが必要です。









スタイルシートについてはまだ修正しているため、時間を無駄にするつもりはありませんが、通常はcss-burittoを使用します。これは個人的な意見として、適切なSassファイル構造を作成するのに非常に便利です。







Reduxリポジトリ(reduxストア)を構成する必要があるため、次のファイルを作成します。







 //web/static/js/store/index.js import { createStore, applyMiddleware } from 'redux'; import createLogger from 'redux-logger'; import thunkMiddleware from 'redux-thunk'; import { syncHistory } from 'react-router-redux'; import reducers from '../reducers'; const loggerMiddleware = createLogger({ level: 'info', collapsed: true, }); export default function configureStore(browserHistory) { const reduxRouterMiddleware = syncHistory(browserHistory); const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore); return createStoreWithMiddleware(reducers); }
      
      





実際、3つのミドルウェアレイヤーを持つストアをセットアップしています。









また、状態レデューサーの組み合わせを渡す必要があるため、このファイルの基本バージョンを作成します。







 //web/static/js/reducers/index.js import { combineReducers } from 'redux'; import { routeReducer } from 'redux-simple-router'; import session from './session'; export default combineReducers({ routing: routeReducer, session: session, });
      
      





出発点として必要なのは、ルーティングの変更を自動的に状態に送信するrouterReducer



、次のようなsession



2つのコンバーター( routerReducer



)のみです。







 //web/static/js/reducers/session.js const initialState = { currentUser: null, socket: null, error: null, }; export default function reducer(state = initialState, action = {}) { return state; }
      
      





後者の初期状態には、訪問者の認証後に渡すcurrentUser



オブジェクト、チャネルへの接続に使用するsocket



、およびユーザー認証中の問題を追跡するためのerror



が含まれます。







これが完了したら、メインのapplication.js



ファイルに移動して、 Root



コンポーネントをレンダリングapplication.js



ます。







 //web/static/js/application.js import React from 'react'; import ReactDOM from 'react-dom'; import { browserHistory } from 'react-router'; import configureStore from './store'; import Root from './containers/root'; const store = configureStore(browserHistory); const target = document.getElementById('main_container'); const node = <Root routerHistory={browserHistory} store={store}/>; ReactDOM.render(node, target);
      
      





ブラウザーの履歴を含むオブジェクトを作成し、リポジトリーを構成し、最後にメインアプリケーションテンプレートにRoot



コンポーネントを描画します。これがRoot



Reduxアダプター(ラッパー) Provider



になりroutes









 //web/static/js/containers/root.js import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; import invariant from 'invariant'; import routes from '../routes'; export default class Root extends React.Component { _renderRouter() { invariant( this.props.routerHistory, '<Root /> needs either a routingContext or routerHistory to render.' ); return ( <Router history={this.props.routerHistory}> {routes} </Router> ); } render() { return ( <Provider store={this.props.store}> {this._renderRouter()} </Provider> ); } }
      
      





次に、非常に単純なルートファイルについて説明します。







 //web/static/js/routes/index.js import { IndexRoute, Route } from 'react-router'; import React from 'react'; import MainLayout from '../layouts/main'; import RegistrationsNew from '../views/registrations/new'; export default ( <Route component={MainLayout}> <Route path="/" component={RegistrationsNew} /> </Route> );
      
      





アプリケーションはMainLayout



コンポーネント内にMainLayout



れ、 MainLayout



は登録画面を描画します。 このファイルの最終バージョンは、後で実装する認証メカニズムのために多少複雑になりますが、これについては後で説明します。







最後に、メインのPhoenixアプリケーションテンプレートにRoot



コンポーネントを描画するhtmlコンテナを追加する必要があります。







 <!-- web/templates/layout/app.html.eex --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content="ricardo@codeloveandboards.com"> <title>Phoenix Trello</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>"> </head> <body> <main id="main_container" role="main"></main> <script src="<%= static_path(@conn, "/js/application.js") %>"></script> </body> </html>
      
      





リンクおよびスクリプトタグは、 Webpackによって生成された静的リソースを参照することに注意してください。







フロントエンドルーティングを管理するので、メインテンプレートとRoot



コンポーネントのみを描画するPageController



コントローラーのアクション(アクション) index



イベントハンドラーにhttpリクエストを送信するようにPhoenixに指示する必要があります。







 # master/web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", PhoenixTrello do pipe_through :browser # Use the default browser stack get "*path", PageController, :index end end
      
      





今のところすべてです。 次の出版物では、データベース、 User



モデル、および新しいユーザーアカウントを作成する機能の最初の移行を作成する方法について説明します。









ユーザーモデルとJWT認証



オリジナル







ユーザー登録



プロジェクトが完全に構成されたので、データベースを移行するためのUser



モデルと手順を作成する準備ができました。 このパートでは、これを行う方法と、訪問者が新しいユーザーアカウントを作成できるようにする方法について説明します。







ユーザーモデルと移行



フェニックスは、データベースとのやり取りの仲介としてEctoを使用します。 Railsの場合、EctoはActiveRecordsに似ていると言えますが、異なるモジュール間で同様の機能を共有しています。







先に進む前に、データベースを作成する必要があります( ただし、その前にconfig/dev.exs



-translator commentでデータベース接続設定を構成する必要があります
)。







 $ mix ecto.create
      
      





次に、新しい移行およびEctoモデルを作成します。 モデルジェネレータは、モジュールの名前、スキームに名前を付けるための複数形、およびフォーム:



必須フィールド:



パラメーターとして受け取ります。







 $ mix phoenix.gen.model User users first_name:string last_name:string email:string encrypted_password:string
      
      





結果の移行ファイルを見ると、Rails移行ファイルとの類似性がすぐにわかります。







 # priv/repo/migrations/20151224075404_create_user.exs defmodule PhoenixTrello.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :first_name, :string, null: false add :last_name, :string, null: false add :email, :string, null: false add :crypted_password, :string, null: false timestamps end create unique_index(:users, [:email]) end end
      
      





フィールドのコンテンツにnull



禁止を追加し、電子メールフィールドの一意のインデックスさえ追加しました。 これは、他の多くの開発者が行うように、アプリケーションに依存するよりも、データの整合性に対する責任をデータベースに移したいためです。 これは個人的な好みの問題だと思います。







それでは、データベースにusers



テーブルを作成しましょう。







 $ mix ecto.migrate
      
      





User



モデルを詳しく見てみましょう。







 # web/models/user.ex defmodule PhoenixTrello.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :first_name, :string field :last_name, :string field :email, :string field :encrypted_password, :string timestamps end @required_fields ~w(first_name last_name email) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) end end
      
      





2つの主要なセクションが表示されます。









ご注意 翻訳者:

Ectoの最新バージョンが更新されました。 たとえば、空のアトムは廃止予定としてマークされているため、代わりに空の連想配列(マップ) %{}



使用する必要があります。また、cast / 4関数はcast / 3およびvalidate_required / 3バンドルに置き換えることをお勧めします
当然、最新のPhoenixジェネレーターはこれらの推奨事項に従います。







(changeset)



, , , null email. User



, , . encrypted_field



, .







:







 # web/models/user.ex defmodule PhoenixTrello.User do # ... schema "users" do # ... field :password, :string, virtual: true # ... end @required_fields ~w(first_name last_name email password) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 5) |> validate_confirmation(:password, message: "Password does not match") |> unique_constraint(:email, message: "Email already taken") end end
      
      





, :









. encrypted_password



. comeonin , mix.exs :







 # mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project # ... def application do [mod: {PhoenixTrello, []}, applications: [ # ... :comeonin ] ] end #... defp deps do [ # ... {:comeonin, "~> 2.0"}, # ... ] end end
      
      





:







 $ mix deps.get
      
      





comeonin User



encrypted_password



changeset :







 # web/models/user.ex defmodule PhoenixTrello.User do # ... def changeset(model, params \\ :empty) do model # ...     |> generate_encrypted_password end defp generate_encrypted_password(current_changeset) do case current_changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) _ -> current_changeset end end end
      
      





, . , comeonin encrypted_password



, .







ルーター



, User



, , router.ex



:api



:







 # web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #... pipeline :api do plug :accepts, ["json"] end scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do post "/registrations", RegistrationController, :create end end #... end
      
      





, POST



/api/v1/registrations



(action) :create



RegistrationController



, json … , :)







コントローラー



, . , . , , , , front-end json jwt . — , , .







jwt, Guardian, . mix.exs:







 # mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project #... defp deps do [ # ... {:guardian, "~> 0.9.0"}, # ... ] end end
      
      





mix deps.get



config.exs:







 # config/confg.exs #... config :guardian, Guardian, issuer: "PhoenixTrello", ttl: { 3, :days }, verify_issuer: true, secret_key: <your guardian secret key>, serializer: PhoenixTrello.GuardianSerializer
      
      





GuardianSerializer



, Guardian, :







 # lib/phoenix_trello/guardian_serializer.ex defmodule PhoenixTrello.GuardianSerializer do @behaviour Guardian.Serializer alias PhoenixTrello.{Repo, User} def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } def for_token(_), do: { :error, "Unknown resource type" } def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) } def from_token(_), do: { :error, "Unknown resource type" } end
      
      





, RegistrationController



:







 # web/controllers/api/v1/registration_controller.ex defmodule PhoenixTrello.RegistrationController do use PhoenixTrello.Web, :controller alias PhoenixTrello.{Repo, User} plug :scrub_params, "user" when action in [:create] def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) conn |> put_status(:created) |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(PhoenixTrello.RegistrationView, "error.json", changeset: changeset) end end end
      
      





(pattern matching), create



"user"



. User . , Guardian ( encode_and_sign



) , jwt json . , , json , .







JSON



Phoenix JSON - Poison . Phoenix, - . — User



, :







 # web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # ... @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]} # ... end
      
      





json (channel), . !







back-end, , front-end, , , React Redux . .








All Articles