Phoenix and ReactでTrelloをクローンします。 パート6-7

















バックエンドが認証リクエストを処理する準備ができたので、フロントエンドに進み、これらのリクエストを作成および送信する方法と、返されたデータを使用してユーザーが個人セクションにアクセスできるようにする方法を見てみましょう。







ルートファイル



続行する前に、React routesファイルをもう一度見てみましょう。







// web/static/js/routes/index.js import { IndexRoute, Route } from 'react-router'; import React from 'react'; import MainLayout from '../layouts/main'; import AuthenticatedContainer from '../containers/authenticated'; import HomeIndexView from '../views/home'; import RegistrationsNew from '../views/registrations/new'; import SessionsNew from '../views/sessions/new'; import BoardsShowView from '../views/boards/show'; import CardsShowView from '../views/cards/show'; export default ( <Route component={MainLayout}> <Route path="/sign_up" component={RegistrationsNew} /> <Route path="/sign_in" component={SessionsNew} /> <Route path="/" component={AuthenticatedContainer}> <IndexRoute component={HomeIndexView} /> <Route path="/boards/:id" component={BoardsShowView}> <Route path="cards/:id" component={CardsShowView}/> </Route> </Route> </Route> );
      
      





4番目の部分で見たように、 AuthenticatedContainer



、認証プロセスの結果として受け取ったjwtトークンが存在して正しくない限り、ユーザーがボード画面にアクセスするAuthenticatedContainer



禁止します。







コンポーネントを表示



次に、アプリケーションのログインフォームを描画するSessionNew



コンポーネントを作成する必要があります。







 import React, {PropTypes} from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { setDocumentTitle } from '../../utils'; import Actions from '../../actions/sessions'; class SessionsNew extends React.Component { componentDidMount() { setDocumentTitle('Sign in'); } _handleSubmit(e) { e.preventDefault(); const { email, password } = this.refs; const { dispatch } = this.props; dispatch(Actions.signIn(email.value, password.value)); } _renderError() { const { error } = this.props; if (!error) return false; return ( <div className="error"> {error} </div> ); } render() { return ( <div className='view-container sessions new'> <main> <header> <div className="logo" /> </header> <form onSubmit={::this._handleSubmit}> {::this._renderError()} <div className="field"> <input ref="email" type="Email" placeholder="Email" required="true" defaultValue="john@phoenix-trello.com"/> </div> <div className="field"> <input ref="password" type="password" placeholder="Password" required="true" defaultValue="12345678"/> </div> <button type="submit">Sign in</button> </form> <Link to="/sign_up">Create new account</Link> </main> </div> ); } } const mapStateToProps = (state) => ( state.session ); export default connect(mapStateToProps)(SessionsNew);
      
      





一般的に、このコンポーネントはフォームをレンダリングし、後者がsignIn



ときにsignIn



アクションのコンストラクターを呼び出します。 また、そのプロパティにアクセスするためにリポジトリに接続され、セッションコンバータを使用して更新されます。 その結果、ユーザーデータ検証エラーを表示できます。







アクションクリエーター



ユーザーのアクションの指示に従って、セッションアクションコンストラクターを作成します。







 // web/static/js/actions/sessions.js import { routeActions } from 'redux-simple-router'; import Constants from '../constants'; import { Socket } from 'phoenix'; import { httpGet, httpPost, httpDelete } from '../utils'; function setCurrentUser(dispatch, user) { dispatch({ type: Constants.CURRENT_USER, currentUser: user, }); // ... }; const Actions = { signIn: (email, password) => { return dispatch => { const data = { session: { email: email, password: password, }, }; httpPost('/api/v1/sessions', data) .then((data) => { localStorage.setItem('phoenixAuthToken', data.jwt); setCurrentUser(dispatch, data.user); dispatch(routeActions.push('/')); }) .catch((error) => { error.response.json() .then((errorJSON) => { dispatch({ type: Constants.SESSIONS_ERROR, error: errorJSON.error, }); }); }); }; }, // ... }; export default Actions;
      
      





signIn



関数は、ユーザーが指定した電子メールとパスワードを渡すPOST要求を作成します。 バックエンドでの認証が成功した場合、関数は受信したjwtトークンをlocalStorage



保存し、 localStorage



JSON構造をリポジトリに送信します。 何らかの理由で認証がエラーになった場合、関数は代わりにそれらをリダイレクトし、アプリケーションのログインフォームに表示できるようになります。







減速機



session



コンバーターを作成します。







 // web/static/js/reducers/session.js import Constants from '../constants'; const initialState = { currentUser: null, error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case Constants.CURRENT_USER: return { ...state, currentUser: action.currentUser, error: null }; case Constants.SESSIONS_ERROR: return { ...state, error: action.error }; default: return state; } }
      
      





コードからすべてが明らかであるため、追加できるものはほとんどありません。そのため、新しい状態を処理できるようにauthenticated



コンテナーを変更します。







認証済みコンテナ



 // web/static/js/containers/authenticated.js import React from 'react'; import { connect } from 'react-redux'; import Actions from '../actions/sessions'; import { routeActions } from 'redux-simple-router'; import Header from '../layouts/header'; class AuthenticatedContainer extends React.Component { componentDidMount() { const { dispatch, currentUser } = this.props; const phoenixAuthToken = localStorage.getItem('phoenixAuthToken'); if (phoenixAuthToken && !currentUser) { dispatch(Actions.currentUser()); } else if (!phoenixAuthToken) { dispatch(routeActions.push('/sign_in')); } } render() { const { currentUser, dispatch } = this.props; if (!currentUser) return false; return ( <div className="application-container"> <Header currentUser={currentUser} dispatch={dispatch}/> <div className="main-container"> {this.props.children} </div> </div> ); } } const mapStateToProps = (state) => ({ currentUser: state.session.currentUser, }); export default connect(mapStateToProps)(AuthenticatedContainer);
      
      





このコンポーネントに接続するときに認証トークンがすでに存在するが、 currentUser



ストアにない場合、コンポーネントはcurrentUser



アクションのコンストラクターを呼び出して、バックエンドからユーザーデータを取得します。 追加してください:







 // web/static/js/actions/sessions.js // ... const Actions = { // ... currentUser: () => { return dispatch => { httpGet('/api/v1/current_user') .then(function(data) { setCurrentUser(dispatch, data); }) .catch(function(error) { console.log(error); dispatch(routeActions.push('/sign_in')); }); }; }, // ... } // ...
      
      





これは、ユーザーがブラウザページを更新するか、最初にセッションを終了せずにルートURLに再度リダイレクトするときにカバーします。 言われたことに続いて、ユーザーを認証し、 currentUser



をstateに渡した後、このコンポーネントは通常のレンダリングを開始し、ヘッダーコンポーネントとそれ自体のネストされた子ルートを表示します。







ヘッダーコンポーネント



このコンポーネントは、ボードと終了ボタンへのリンクとともにグラバターとユーザー名を描画します。







 // web/static/js/layouts/header.js import React from 'react'; import { Link } from 'react-router'; import Actions from '../actions/sessions'; import ReactGravatar from 'react-gravatar'; export default class Header extends React.Component { constructor() { super(); } _renderCurrentUser() { const { currentUser } = this.props; if (!currentUser) { return false; } const fullName = [currentUser.first_name, currentUser.last_name].join(' '); return ( <a className="current-user"> <ReactGravatar email={currentUser.email} https /> {fullName} </a> ); } _renderSignOutLink() { if (!this.props.currentUser) { return false; } return ( <a href="#" onClick={::this._handleSignOutClick}><i className="fa fa-sign-out"/> Sign out</a> ); } _handleSignOutClick(e) { e.preventDefault(); this.props.dispatch(Actions.signOut()); } render() { return ( <header className="main-header"> <nav> <ul> <li> <Link to="/"><i className="fa fa-columns"/> Boards</Link> </li> </ul> </nav> <Link to='/'> <span className='logo'/> </Link> <nav className="right"> <ul> <li> {this._renderCurrentUser()} </li> <li> {this._renderSignOutLink()} </li> </ul> </nav> </header> ); } }
      
      





ユーザーが終了ボタンを押すと、 session



アクションコンストラクターのsingOut



メソッドが呼び出されます。 このメソッドを追加します。







 // web/static/js/actions/sessions.js // ... const Actions = { // ... signOut: () => { return dispatch => { httpDelete('/api/v1/sessions') .then((data) => { localStorage.removeItem('phoenixAuthToken'); dispatch({ type: Constants.USER_SIGNED_OUT, }); dispatch(routeActions.push('/sign_in')); }) .catch(function(error) { console.log(error); }); }; }, // ... } // ...
      
      





DELETE



リクエストをバックエンドに送信し、成功した場合、 phoenixAuthToken



localStorage



から削除し、前述のセッションコンバーターを使用してcurrentUser



をゼロにリセットするUSER_SIGNED_OUT



アクションも送信します。







 // web/static/js/reducers/session.js import Constants from '../constants'; const initialState = { currentUser: null, error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { // ... case Constants.USER_SIGNED_OUT: return initialState; // ... } }
      
      





もう一つ



ユーザーを認証してアプリケーションにログインするプロセスに行き着きましたが、プログラムする将来のすべての機能の基礎となる主要な機能であるユーザーソケットとチャンネルをまだ実装していません。 この点は非常に重要なので、次の部分に残しておくことをおuserSocket



ますuserSocket



外観と接続方法を確認し、フロントエンドとバックエンドの間にリアルタイムの変更を示す双方向チャネルを作成します。 。









ソケットとチャンネル



オリジナル







前のパートでは、認証プロセスを完了し、楽しみを始める準備ができました。 これからは、フロントエンドとバックエンドを接続するためにフェニックスのリアルタイム機能に大きく依存します。 ユーザーは、ボードに影響を与えるイベントの通知を受け取り、変更が画面に自動的に表示されます。







チャンネル全体をコントローラーと考えることができます。 ただし、1つの接続で要求を処理して結果を返すのとは異なり、特定のトピックで双方向イベントを処理し、接続された複数の受信者に送信できます。 これらを設定するために、Phoenixはソケットへの接続を認証および識別するソケットハンドラーを使用し、対応する要求を処理するチャネルを決定するチャネルルートも記述します。







ユーザーソケット



新しいPhoenixアプリケーションを作成すると、自動的に初期ソケット構成が作成されます:







 # lib/phoenix_trello/endpoint.ex defmodule PhoenixTrello.Endpoint do use Phoenix.Endpoint, otp_app: :phoenix_trello socket "/socket", PhoenixTrello.UserSocket # ... end
      
      





UserSocket



UserSocket



れますが、必要なメッセージを処理するには、いくつかの変更を加える必要があります。







 # web/channels/user_socket.ex defmodule PhoenixTrello.UserSocket do use Phoenix.Socket alias PhoenixTrello.{Repo, User} # Channels channel "users:*", PhoenixTrello.UserChannel channel "boards:*", PhoenixTrello.BoardChannel # Transports transport :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll # ... end
      
      





実際、2つの異なるチャネルがあります。









また、 connect



およびid



関数を実装する必要があります。これは次のようになります。







 # web/channels/user_socket.ex defmodule PhoenixTrello.UserSocket do # ... def connect(%{"token" => token}, socket) do case Guardian.decode_and_verify(token) do {:ok, claims} -> case GuardianSerializer.from_token(claims["sub"]) do {:ok, user} -> {:ok, assign(socket, :current_user, user)} {:error, _reason} -> :error end {:error, _reason} -> :error end end def connect(_params, _socket), do: :error def id(socket), do: "users_socket:#{socket.assigns.current_user.id}" end
      
      





token



をパラメーターとしてconnect



機能( ソケットに接続すると自動的に発生します-約Translator )を呼び出すと、 token



を確認し、 パート3で作成したGuardianSerializer



を使用してトークンからユーザーデータを受信し、このデータをソケットに保存します。必要に応じてチャンネルで利用できるようになります。 さらに、認証されていないユーザーがソケットに接続することも禁止します。







ご注意 翻訳者

接続関数には、 def connect(%{"token" => token}, socket) do ... end



and def connect(_params, _socket), do: :error



2つの説明があることに注意してください。
パターンマッチングメカニズムのおかげで、最初のパラメーターによって渡された連想配列に「トークン」キーがある場合(およびこのキーに関連付けられた値がトークンと呼ばれる変数に入る場合)、他の2番目がケース。 connect



関数は、ソケットに接続されたときにフレームワークによって自動的に呼び出されます。







id



関数は、ソケットへの現在の接続を識別するために使用され、たとえば、特定のユーザーのすべてのアクティブなチャネルとソケットを完了するために使用できます。
必要に応じて、 PhoenixTrello.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})



呼び出して"disconnect"



メッセージを送信することにより、アプリケーションの任意の部分からこれを実行できますPhoenixTrello.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})









ちなみに、 <AppName>.Endpoint.broadcast(topic, message, payload)



使用すると、ユーザーの切断だけでなく、通常、関連トピックにサブスクライブしているすべてのユーザーにメッセージを送信できます。
この場合、 topic



は件名を含む文字列( "boards:877"



)、 message



message



を含む文字列( "boards:update"



)、 payload



は送信前にjsonに変換されるデータを含む連想配列です。 。
たとえば、オンラインのユーザーに、コントローラーまたは他のプロセスから直接REST APIを使用して行われた変更を送信できます。







ユーザーチャンネル



ソケットを設定しUserChannel



、非常に簡単なUserChannel



ましょう。







 # web/channels/user_channel.ex defmodule PhoenixTrello.UserChannel do use PhoenixTrello.Web, :channel def join("users:" <> user_id, _params, socket) do {:ok, socket} end end
      
      





このチャネルにより、ユーザーに関連するメッセージをどこからでも送信して、フロントエンドで処理できます。 特定のケースでは、ユーザーを参加者として追加したボードに関するデータを転送するために使用し、このユーザーのリストにこの新しいボードを追加できるようにします。 また、このチャンネルを使用して、ユーザーが所有する他のボードに関する通知や、思い浮かぶその他の通知を表示することもできます。







ソケットとチャンネル接続



続行する前に、前の部分で行ったことを思い出しましょう...ユーザー認証後、ログインフォームまたは以前に保存されたphoenixAuthToken



れたかどうかに関係なく、 currentUser



データを取得してReduxストアに転送し、タイトルにアバターとユーザー名を表示します。 これは、ソケットとチャネルにも接続するのに適した場所のように見えるので、リファクタリングを行いましょう。







 // web/static/js/actions/sessions.js import Constants from '../constants'; import { Socket } from 'phoenix'; // ... export function setCurrentUser(dispatch, user) { dispatch({ type: Constants.CURRENT_USER, currentUser: user, }); const socket = new Socket('/socket', { params: { token: localStorage.getItem('phoenixAuthToken') }, }); socket.connect(); const channel = socket.channel(`users:${user.id}`); channel.join().receive('ok', () => { dispatch({ type: Constants.SOCKET_CONNECTED, socket: socket, channel: channel, }); }); }; // ...
      
      





ユーザーデータをリダイレクトした後、 Phoenix



JavaScriptライブラリから新しいSocket



オブジェクトを作成し、接続の確立に必要なphoenixAuthToken



パラメーターを渡してから、 connect



関数を呼び出します。 新しいユーザーchannel



を作成して参加します。 join



に対する応答でok



メッセージを受信すると、 SOCKET_CONNECTED



アクションを指示して、ソケットとチャネルの両方をストレージに保存します。







 // web/static/js/reducers/session.js import Constants from '../constants'; const initialState = { currentUser: null, socket: null, channel: null, error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case Constants.CURRENT_USER: return { ...state, currentUser: action.currentUser, error: null }; case Constants.USER_SIGNED_OUT: return initialState; case Constants.SOCKET_CONNECTED: return { ...state, socket: action.socket, channel: action.channel }; case Constants.SESSIONS_ERROR: return { ...state, error: action.error }; default: return state; } }
      
      





これらのオブジェクトを格納する主な理由は、それらを多くの場所で必要とするためです。そのため、状態に格納すると、プロパティ( props



)を通じてコン​​ポーネントで使用できるようになります。







ユーザーを認証し、ソケットに接続してチャネルに参加すると、 AuthenticatedContainer



HomeIndexView



し、ユーザーに属するすべてのボードと、彼が参加者として招待されたボードを表示します。 次のパートでは、新しいボードを作成し、チャネルを使用して既存のユーザーを招待して、結果のデータを関係するユーザーに転送する方法を明らかにします。







それまでの間、 ライブデモと最終結果のソースコードを確認してください。








All Articles