- 技術スタックの紹介と選択
- Phoenix Frameworkプロジェクトの初期セットアップ
- ユーザーモデルとJWT認証
- ReactおよびReduxに登録するためのフロントエンド
- アプリケーションに入るための初期データベース充填とコントローラー
- ReactおよびReduxフロントエンド認証
- ソケットとチャネルの構成
- 新しいボードをリストして作成します
- 新しいボードユーザーを追加する
- ボードの接続ユーザーを追跡します
- リストとカードを追加する
- プロジェクトをHerokuに投稿します
この部分は最後の部分であり、特に長くなりますが、すでにサイクルを終了して先に進みたいと思います。 また、その準備と公開にこのような大きな休止があることをおaびします。 しかし、今回は無駄ではなく、今回はオリジナルの新しい記事に材料を提供しました。 翻訳者
ボードの参加者の接続を監視します
著者からの警告:このパートは、Presence機能が登場する前に書かれたものであり、GenServerの基本的な動作の簡単な紹介です。
前のパートを思い出してください。このパートでは 、新しい参加者をボードに招待する機会をユーザーに提供しました。 既存のユーザーの電子メールを追加すると、ユーザーと掲示板の間に新しい関係が作成され、新しいユーザーのデータがチャネルを介して送信されました。その結果、オンラインの掲示板の参加者全員にアバターが表示されました。 一見、これはクールですが、現在オンラインでボードを閲覧しているユーザーを強調することができれば、はるかに優れた便利な方法を実行できます。 さあ始めましょう!
問題
続行する前に、達成したいことについて考えてみましょう。 そのため、実際には、掲示板と、そのURLに予期せずアクセスできる複数の参加者がいて、自動的に掲示板チャネルに接続しています。 これが発生した場合、アバターを半透明にする必要があるオフラインの参加者とは対照的に、参加者のアバターは透明にならずに表示される必要があります。
接続された参加者がボードのURLを離れたり、アプリケーションを終了したり、ブラウザーウィンドウを閉じたりした場合、ボードのチャンネルチャンネルに接続しているすべてのユーザーにこのイベントを通知して、ユーザーがボードを表示しなくなったことを通知する必要があります。 これとその欠点を達成するためのいくつかの方法を見てみましょう。
- Reduxリポジトリのフロントエンドで接続された参加者のリストを管理します。 一見、これは適切なソリューションのように思えるかもしれませんが、すでにボードチャネルに接続している参加者に対してのみ機能します。 最近接続したユーザーにはこのデータはありません。
- データベースを使用して、接続された参加者のリストを保存します。 これも適切な方法であることが判明する可能性がありますが、非常に特定のユーザーの行動とデータを混在させることは言うまでもなく、参加者の接続または終了時に参加者のリストとその更新を要求するデータベースを常にプルするように強制します。
では、すべてのユーザーに迅速かつ効率的にアクセスできるように、この情報をどこに保存できますか? 簡単です。 で...忍耐を...永続的なステートフルプロセス。
GenServerの原則
「 恒久的なステートフルプロセス」というフレーズは、最初は威圧的に聞こえるかもしれませんが、 ElixirとそのGenServerのおかげで、実装するのは想像以上に簡単です 。
GenServerは他のElixirプロセスと同様のプロセスであり、状態の保存、コードの非同期実行などに使用できます。
これは、サーバー上で実行され、各ボードに接続ユーザーのIDのリストを含む連想配列(マップ)を持つ小さなプロセスであると想像してください。 このようなもの:
%{ "1" => [1, 2, 3], "2" => [4, 5] }
ここで、このプロセスが独自の初期化と連想状態配列の更新、ボードと接続ユーザーの追加と削除のためのアクセス可能なインターフェースを持っていると想像してください。 まあ、これは一般に、 GenServerプロセスであり、追跡、エラー報告、追跡機能などの対応する利点がある限り、「一般に」と言います。
ボードチャンネルモニター
それで、このプロセスの最初のバージョンを作成しましょう。これは、接続されたボード参加者のリストの追跡データを保存します:
# /lib/phoenix_trello/board_channel/monitor.ex defmodule PhoenixTrello.BoardChannel.Monitor do use GenServer ##### # Client API def start_link(initial_state) do GenServer.start_link(__MODULE__, initial_state, name: __MODULE__) end end
GenServerを使用する場合、外部クライアントのAPI関数とサーバー実装の両方を考慮する必要があります。 最初のステップはstart_link
関数を実装するstart_link
です。これは実際にGenServerを実行し、初期状態を引数として、この場合はモジュール名とサーバー名の間の空の連想配列に渡します。 アプリケーションの起動時にこのプロセスを開始したいので、監視ツリーの子孫のリストに追加しましょう。
# /lib/phoenix_trello.ex defmodule PhoenixTrello do use Application def start(_type, _args) do import Supervisor.Spec, warn: fals e children = [ # ... worker(PhoenixTrello.BoardChannel.Monitor, [%{}]), # ... ] # ... end end
これで、アプリケーションを起動するたびに、作成したstart_link
関数が自動的に呼び出され、空の連想配列%{}
が初期状態として渡されます。 何らかの理由でMonitor
実行が中断された場合、アプリケーションは新しい空の連想配列でMonitor
実行を再開します。 すごいですね。 すべての設定が完了したので、 Monitor
の状態の配列に参加者を追加してみましょう。
メンバー接続の処理
これを行うには、クライアント関数と、対応するフィードバック関数のサーバーハンドラー(以降、単にコールバック関数)の両方を追加する必要があります。
# /lib/phoenix_trello/board_channel/monitor.ex defmodule PhoenixTrello.BoardChannel.Monitor do use GenServer ##### # Client API # ... def member_joined(board, member) do GenServer.call(__MODULE__, {:member_joined, board, member}) end ##### # Server callbacks def handle_call({:member_joined, board, member}, _from, state) do state = case Map.get(state, board) do nil -> state = state |> Map.put(board, [member]) {:reply, [member], state} members -> state = state |> Map.put(board, Enum.uniq([member | members])) {:reply, Map.get(state, board), state} end end end
member_joined/2
関数を呼び出して、ボードとユーザーを渡すと、メッセージ{:member_joined, board, member}
GenServerプロセスを呼び出します。 このため、サーバーコールバック関数ハンドラーが必要です。 GenServer
からのhandle_call/3
関数handle_call/3
は、要求メッセージ、送信者、および現在の状態を受け取ります。 したがって、この場合、ボードの状態を解除し、ユーザーをそのユーザーのリストに追加しようとします。 ボードがまだない場合は、接続ユーザーを含む新しいリストを追加します。 回答として、このボードに属するユーザーのリストを返します。
member_joined
メソッドはどこmember_joined
ますか? ユーザー接続時のBoardChannelから:
# /web/channels/board_channel.ex defmodule PhoenixTrello.BoardChannel do use PhoenixTrello.Web, :channel alias PhoenixTrello.{User, Board, UserBoard, List, Card, Comment, CardMember} alias PhoenixTrello.BoardChannel.Monitor def join("boards:" <> board_id, _params, socket) do current_user = socket.assigns.current_user board = get_current_board(socket, board_id) connected_users = Monitor.user_joined(board_id, current_user.id) send(self, {:after_join, connected_users}) {:ok, %{board: board}, assign(socket, :board, board)} end def handle_info({:after_join, connected_users}, socket) do broadcast! socket, "user:joined", %{users: connected_users} {:noreply, socket} end # ... end
したがって、接続時にMonitor
を使用して追跡し、ソケットを介して現在のボードユーザーの更新されたリストを送信します。 これで、フロントエンドでこのニュースレターを処理して、接続ユーザーの新しいリストでアプリケーションステータスを更新できます。
// /web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { // ... connectToChannel: (socket, boardId) => { return dispatch => { const channel = socket.channel(`boards:${boardId}`); // ... channel.on('user:joined', (msg) => { dispatch({ type: Constants.CURRENT_BOARD_CONNECTED_USERS, users: msg.users, }); }); }; } }
残っているのは、ボードメンバーがこのリストにリストされているかどうかに応じて、アバターの透明度を変更することだけです。
// /web/static/js/components/boards/users.js export default class BoardUsers extends React.Component { _renderUsers() { return this.props.users.map((user) => { const index = this.props.connectedUsers.findIndex((cu) => { return cu.id === user.id; }); const classes = classnames({ connected: index != -1 }); return ( <li className={classes} key={user.id}> <ReactGravatar className="react-gravatar" email={user.email} https/> </li> ); }); } // ... }
ユーザー停止の処理
ユーザーをボードチャネルから切断するプロセスはほとんど同じです。 まず、必要なクライアント関数と対応するサーバーコールバック関数を追加して、 Monitor
を更新しましょう。
# /lib/phoenix_trello/board_channel/monitor.ex defmodule PhoenixTrello.BoardChannel.Monitor do use GenServer ##### # Client API # ... def member_left(board, member) do GenServer.call(__MODULE__, {:member_left, board, member}) end ##### # Server callbacks # ... def handle_call({:member_left, board, member}, _from, state) do new_members = state |> Map.get(board) |> List.delete(member) state = state |> Map.update!(board, fn(_) -> new_members end) {:reply, new_members, state} end end
ご覧のとおり、これはmember_join
とほぼ同じ機能member_join
が、逆の順序でデプロイされます。 関数では、ボードが状態で検索され、参加者が削除され、その後、ボード参加者の現在のリストが新しいリストに置き換えられ、回答で返されます。 接続と同様に、 BoardChannelからこの関数を呼び出すので、更新しましょう。
# /web/channels/board_channel.ex defmodule PhoenixTrello.BoardChannel do use PhoenixTrello.Web, :channel # ... def terminate(_reason, socket) do board_id = Board.slug_id(socket.assigns.board) user_id = socket.assigns.current_user.id broadcast! socket, "user:left", %{users: Monitor.user_left(board_id, user_id)} :ok end end
チャネルへの接続が中断されると、ハンドラーは以前と同様に、ソケットを介して参加者の更新されたリストを送信します。 チャネルへの接続を中断するために、現在のボードのビューをアンマウントするときに使用するアクションクリエーターを作成します。 また、メーリングuser:left
ハンドラーを追加する必要がありuser:left
。
// /web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { // ... connectToChannel: (socket, boardId) => { return dispatch => { const channel = socket.channel(`boards:${boardId}`); // ... channel.on('user:left', (msg) => { dispatch({ type: Constants.CURRENT_BOARD_CONNECTED_USERS, users: msg.users, }); }); }; }, leaveChannel: (channel) => { return dispatch => { channel.leave(); }; }, }
BoardShowView
leaveChannel
アクションコンストラクターを処理するためにleaveChannel
コンポーネントを更新することを忘れないでください:
// /web/static/js/views/boards/show.js import Actions from '../../actions/current_board'; // ... class BoardsShowView extends React.Component { // ... componentWillUnmount() { const { dispatch, currentBoard} = this.props; dispatch(Actions.leaveChannel(currentBoard.channel)); } } // ...
そしてそれだけです! これをテストするには、2つの異なるブラウザーを開き、異なるユーザーでアプリケーションを入力します。 次に、両方の同じボードに移動し、ボードに出入りするユーザーの1人と遊びます。 彼のアバターの透明度が前後にどのように変化するかがわかります。
私が初めてやったのと同じ方法でGenServerを使って楽しんでください。 しかし、私たちはほんの一部に影響を与えました。 GenServerとSupervisorはElixirが提供する非常に豊富なツールであり、完全に統合され、防弾です(元々、著者は防弾という用語を使用しています。 )動作するためにサードパーティの依存関係を必要としない-対照的に、たとえばRedis 。 次のパートでは、ソケットとチャネルを使用してリアルタイムでリストとカードを作成し続けます。
リストとカードを追加する
前のパートでは、 OTPおよびGenServer機能を使用して、チャネルボードに接続しているユーザーを追跡するためのシンプルだが既に有用なメカニズムを作成しました。 また、このリストをチャネル経由で送信する方法も学習しました。これにより、各参加者が同時に誰がボードを表示しているかを確認できます。 ここで、参加者にいくつかのカードとリストを追加させて、変更がすぐに画面に表示されるようにします...やってみましょう!
移行とモデル
ボードには複数のリストを含めることができ、そのリストには複数のカードも含めることができます。そのため、コンソールで次のmix
タスクを使用してList
モデルを生成することから始めましょう。
$ mix phoenix.gen.model List lists board_id:references:board name:string ... ... $ mix ecto.migrate
これにより、データベースにlists
テーブルと対応するモデルが作成されます。
# web/models/list.ex defmodule PhoenixTrello.List do use PhoenixTrello.Web, :model alias PhoenixTrello.{Board, List} @derive {Poison.Encoder, only: [:id, :board_id, :name]} schema "lists" do field :name, :string belongs_to :board, Board timestamps end @required_fields ~w(name) @optional_fields ~w() def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) end end
Card
モデルの生成は非常に似ています。
$ mix phoenix.gen.model Card cards list_id:references:lists name:string ... ... $ mix ecto.migrate
結果のモデルは次のようになります。
# web/models/card.ex defmodule PhoenixTrello.Card do use PhoenixTrello.Web, :model alias PhoenixTrello.{Repo, List, Card} @derive {Poison.Encoder, only: [:id, :list_id, :name]} schema "cards" do field :name, :string belongs_to :list, List timestamps end @required_fields ~w(name list_id) @optional_fields ~w() def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) end end
lists
スキームに一連のカードを追加することを忘れないでください。
# web/models/list.ex defmodule PhoenixTrello.List do # ... @derive {Poison.Encoder, only: [:id, :board_id, :name, :cards]} # ... schema "lists" do # .. has_many :cards, Card end # ... end
これで、フロントエンドに進み、必要なコンポーネントを作成できます。
リストフォームコンポーネント
続行する前に、 BoardsShowView
コンポーネントのrender
機能を思い出してみましょう。
// web/static/js/views/boards/show.js //... //... _renderLists() { const { lists, channel, id, addingNewCardInListId } = this.props.currentBoard; return lists.map((list) => { return ( <ListCard key={list.id} boardId={id} dispatch={this.props.dispatch} channel={channel} isAddingNewCard={addingNewCardInListId === list.id} {...list} /> ); }); } render() { const { fetching, name } = this.props.currentBoard; if (fetching) return ( <div className="view-container boards show"> <i className="fa fa-spinner fa-spin"/> </div> ); return ( <div className="view-container boards show"> <header className="view-header"> <h3>{name}</h3> {::this._renderMembers()} </header> <div className="canvas-wrapper"> <div className="canvas"> <div className="lists-wrapper"> {::this._renderLists()} {::this._renderAddNewList()} </div> </div> </div> {this.props.children} </div> ); }
最後に作成したBoardMembers
コンポーネントとは異なり、現在のボードに関連するすべてのリストを描画する必要もあります。 現時点ではリストはないので、 _renderAddNewList
関数に移りましょう。
// web/static/js/views/boards/show.js // ... _renderAddNewList() { const { dispatch, formErrors, currentBoard } = this.props; if (!currentBoard.showForm) return this._renderAddButton(); return ( <ListForm dispatch={dispatch} errors={formErrors} channel={currentBoard.channel} onCancelClick={::this._handleCancelClick} /> ); } _renderAddButton() { return ( <div className="list add-new" onClick={::this._handleAddNewClick}> <div className="inner"> Add new list... </div> </div> ); } _handleAddNewClick() { const { dispatch } = this.props; dispatch(Actions.showForm(true)); } _handleCancelClick() { this.props.dispatch(Actions.showForm(false)); } // ...
_renderAddNewList
関数は、 currentBoard.showForm
プロパティがtrue
に設定されているかどうかを確認し、 ListForm
コンポーネントの代わりに[ 新しいリストを追加... ]ボタンをListForm
ます。
ユーザーがボタンをクリックすると、対応するアクションがストアにshowForm
れ、 showForm
プロパティがtrue
設定され、フォームが表示されます。 次に、フォームコンポーネントを作成します。
// web/static/js/components/lists/form.js import React, { PropTypes } from 'react'; import Actions from '../../actions/lists'; export default class ListForm extends React.Component { componentDidMount() { this.refs.name.focus(); } _handleSubmit(e) { e.preventDefault(); const { dispatch, channel } = this.props; const { name } = this.refs; const data = { name: name.value, }; dispatch(Actions.save(channel, data)); } _handleCancelClick(e) { e.preventDefault(); this.props.onCancelClick(); } render() { return ( <div className="list form"> <div className="inner"> <form id="new_list_form" onSubmit={::this._handleSubmit}> <input ref="name" id="list_name" type="text" placeholder="Add a new list..." required="true"/> <button type="submit">Save list</button> or <a href="#" onClick={::this._handleCancelClick}>cancel</a> </form> </div> </div> ); } }
これは、リストの名前のテキストボックス、送信ボタン、および説明したものと同じアクションを指示するキャンセルリンクを含むフォームを持つ非常に単純なコンポーネントですが、 showForm
をfalse
に設定してフォームを非表示にします。 フォームが送信されると、コンポーネントはユーザー名とともにsave
コンストラクタを指示し、 lists:create
名前を送信しlists:create
BoardChannel
トピックをlists:create
:
// web/static/js/actions/lists.js import Constants from '../constants'; const Actions = { save: (channel, data) => { return dispatch => { channel.push('lists:create', { list: data }); }; }, }; export default Actions;
ボードチャンネル
次のステップは、 BoardChannel
lists:create
処理を教えることBoardChannel
lists:create
メッセージをlists:create
ます。
# web/channels/board_channel.ex defmodule PhoenixTrello.BoardChannel do # ... def handle_in("lists:create", %{"list" => list_params}, socket) do board = socket.assigns.board changeset = board |> build_assoc(:lists) |> List.changeset(list_params) case Repo.insert(changeset) do {:ok, list} -> list = Repo.preload(list, [:cards]) broadcast! socket, "list:created", %{list: list} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error creating list"}}, socket} end end # ... end
この機能は、チャネルに接続されたボードを使用して、受信したパラメーター( list_params
)に基づいてList
モデルの一連の変更(changeset)を構築し、データベースに追加します。 すべてが:ok
場合、作成されたリストはチャンネルを介して、作成者を含むすべての接続ユーザーに送信されるため、応答する必要はなく、単に:noreply
ます。 何らかの奇跡により、新しいリストの追加中にエラーが発生した場合、エラーメッセージは作成者のみに返され、何かがうまくいかなかったことがわかるようになります。
変換器
リストはほぼ完成です。 チャンネルは作成されたシートを送信するため、チャンネルが接続された現在のボードのアクションデザイナーのフロントエンドにこのハンドラーを追加します。
// web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { // ... connectToChannel: (socket, boardId) => { return dispatch => { const channel = socket.channel(`boards:${boardId}`); // ... channel.on('list:created', (msg) => { dispatch({ type: Constants.CURRENT_BOARD_LIST_CREATED, list: msg.list, }); }); }; }, // ... }
最後に、ボードのレデューサーを更新して、リストが返す状態の新しいバージョンにリストを追加する必要があります。
// web/static/js/reducers/current_board.js import Constants from '../constants'; export default function reducer(state = initialState, action = {}) { switch (action.type) { //... case Constants.CURRENT_BOARD_LIST_CREATED: const lists = [...state.lists]; lists.push(action.list); return { ...state, lists: lists, showForm: false }; // ... } }
また、 showForm
属性をfalse
に設定して、フォームを自動的に非表示にし、作成したばかりのリストとともに[ 新しいリストを追加 ]ボタンを再度表示する必要があります。
List
コンポーネント
これで、ボード上に少なくとも1つのリストがあり、 List
コンポーネントを作成できます。これを使用して描画します。
// /web/static/js/components/lists/card.js import React, {PropTypes} from 'react'; import Actions from '../../actions/current_board'; import CardForm from '../../components/cards/form'; import Card from '../../components/cards/card'; export default class ListCard extends React.Component { // ... _renderForm() { const { isAddingNewCard } = this.props; if (!isAddingNewCard) return false; let { id, dispatch, formErrors, channel } = this.props; return ( <CardForm listId={id} dispatch={dispatch} errors={formErrors} channel={channel} onCancelClick={::this._hideCardForm} onSubmit={::this._hideCardForm}/> ); } _renderAddNewCard() { const { isAddingNewCard } = this.props; if (isAddingNewCard) return false; return ( <a className="add-new" href="#" onClick={::this._handleAddClick}>Add a new card...</a> ); } _handleAddClick(e) { e.preventDefault(); const { dispatch, id } = this.props; dispatch(Actions.showCardForm(id)); } _hideCardForm() { const { dispatch } = this.props; dispatch(Actions.showCardForm(null)); } render() { const { id, connectDragSource, connectDropTarget, connectCardDropTarget, isDragging } = this.props; const styles = { display: isDragging ? 'none' : 'block', }; return ( <div id={`list_${id}`} className="list" style={styles}> <div className="inner"> <header> <h4>{this.props.name}</h4> </header> <div className="cards-wrapper"> {::this._renderCards()} </div> <footer> {::this._renderForm()} {::this._renderAddNewCard()} </footer> </div> </div> ); } }
リストの場合と同じように、最初にカードの形を描くことに焦点を当てます。 一般に、ボードのメインコンポーネントによって渡される( prop
)プロパティを使用してフォームをレンダリングまたは非表示にし、この状態プロパティを変更するアクションを指示するために、同じアプローチを使用します。
カードフォームコンポーネント
このコンポーネントは、 ListForm
コンポーネントと非常によく似ています。
// /web/static/js/components/cards/form.js import React, { PropTypes } from 'react'; import Actions from '../../actions/lists'; import PageClick from 'react-page-click'; export default class CardForm extends React.Component { _handleSubmit(e) { e.preventDefault(); let { dispatch, channel } = this.props; let { name } = this.refs; let data = { list_id: this.props.listId, name: name.value, }; dispatch(Actions.createCard(channel, data)); this.props.onSubmit(); } componentDidMount() { this.refs.name.focus(); } _handleCancelClick(e) { e.preventDefault(); this.props.onCancelClick(); } render() { return ( <PageClick onClick={::this._handleCancelClick}> <div className="card form"> <form id="new_card_form" onSubmit={::this._handleSubmit}> <textarea ref="name" id="card_name" type="text" required="true" rows={5}/> <button type="submit">Add</button> or <a href="#" onClick={::this._handleCancelClick}>cancel</a> </form> </div> </PageClick> ); } }
前と同様に、フォームを送信するときに、ユーザーが指定した名前のカードを作成するようにアクションを指示します。 これを行うために、アクションコンストラクターは新しいメッセージをチャネルに送信します。
// /web/static/js/actions/lists.js import Constants from '../constants'; const Actions = { // ... createCard: (channel, data) => { return dispatch => { channel.push('cards:create', { card: data }); }; }, }; // ...
BoardChannel
ハンドラーを追加しましょう:
# web/channels/board_channel.ex def handle_in("cards:create", %{"card" => card_params}, socket) do board = socket.assigns.board changeset = board |> assoc(:lists) |> Repo.get!(card_params["list_id"]) |> build_assoc(:cards) |> Card.changeset(card_params) case Repo.insert(changeset) do {:ok, card} -> broadcast! socket, "card:created", %{card: card} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error creating card"}}, socket} end end
リストを作成するときと同じ方法で、チャネルに接続されたボードとパラメーターとして渡されたリストを関連付けることにより、新しいCard
エントリが作成されます。 作成が成功した場合、記録はチャンネルに接続されているすべての参加者に転送されます。 最後に、 jsチャネルにコールバック関数を追加する必要があります。
// web/static/js/actions/current_board.js //... channel.on('card:created', (msg) => { dispatch({ type: Constants.CURRENT_BOARD_CARD_CREATED, card: msg.card, }); }); // ...
そして、コンバーターを介して新しいカードを状態に追加します。
// web/static/js/reducers/current_board.js // ... case Constants.CURRENT_BOARD_CARD_CREATED: lists = [...state.lists]; const { card } = action; const listIndex = lists.findIndex((list) => { return list.id == card.list_id; }); lists[listIndex].cards.push(card); return { ...state, lists: lists }; // ...
そしてそれだけです! 接続された各参加者の画面にカードが表示されます。
今何
このセクションでは、ユーザーの登録、システムの入力、ボードの作成、他のユーザーの招待、リストとカードの追加によるリアルタイムの作業に必要な基本機能の作成を完了しました。 リポジトリの最終バージョンには、リストの編集、リストやカードの移動によるソート、カードに関する詳細情報の表示、参加者の割り当て、コメントや色タグの追加など、さらに多くの機能がありますが、詳細は説明しませんそれらのどれも話さない、そうでなければ永遠の訓練になるでしょう。 :-D
ただし、心配しないでください。結果をHerokuに投稿して、結果を全世界と共有する方法について説明する部分があります。
プロジェクトをHerokuに投稿します
私たちはついにそれをやりました(そして私も-およそ翻訳者) 。 5回(元の約11通訳)の出版物の後、 Webpack 、 React 、およびReduxを使用して新しいPhoenixプロジェクトをセットアップする方法を学びました。 JWTトークンに基づいて安全な認証システムを作成し、必要なデータベースのスキームの移行を作成し、リアルタイム機能用のソケットとチャネルをプログラムし、ボードの接続された参加者を追跡するGenServerプロセスを構築しました 。 Herokuにプロジェクトを投稿して、このすべてを世界と共有する時が来ました 。 やりましょう!
Herokuをカスタマイズする
先に進む前に、 HerokuアカウントとHeroku Toolbeltがすでにインストールされているとします。 PhoenixアプリケーションをHerokuに配置するには、2つの異なるビルドパック (ビルドキット)を使用する必要があるため、 multi-buildpackを使用して新しいアプリケーションを作成しましょう。
$ heroku create phoenix-trello --buildpack https://github.com/ddollar/heroku-buildpack-multi
これにより、新しいHerokuアプリケーションが作成され、公開に使用するリモートheroku
gitリポジトリが追加されます。 前述したように、Phoenixアプリケーションには、2つの異なるビルドパッケージが必要です。
- heroku-buildpack-elixir :Elixirアプリケーションを構築するためのメインセット。
- heroku-buildpack-phoenix-static : .
.buildpacks
:
# .buildpacks https://github.com/HashNuke/heroku-buildpack-elixir https://github.com/gjaldon/phoenix-static-buildpack
, Elixir, , elixir_buildpack.config
:
# elixir_buildpack.config # Elixir version elixir_version=1.2.3 # Always rebuild from scratch on every deploy? always_rebuild=true
Elixir, , , . phoenix_static_buildpack.config
:
# phoenix_static_buildpack.config # We can set the version of Node to use for the app here node_version=5.3.0 # We can set the version of NPM to use for the app here npm_version=3.5.2
Webpack node
npm
. compile
, , :
# compile info "Building Phoenix static assets" webpack mix phoenix.digest
, mix- phoenix.digest
webpack
, .
, , prod.exs
, :
# config/prod.exs use Mix.Config # ... config :phoenix_trello, PhoenixTrello.Endpoint, # .. url: [scheme: "https", host: "phoenix-trello.herokuapp.com", port: 443], # .. secret_key_base: System.get_env("SECRET_KEY_BASE") # .. # Configure your database config :phoenix_trello, PhoenixTrello.Repo, # .. url: System.get_env("DATABASE_URL"), pool_size: 20 # Configure guardian config :guardian, Guardian, secret_key: System.get_env("GUARDIAN_SECRET_KEY")
, : URL Heroku SSL-. , secret_key_base
, ( url
) secret_key
guardian. Heroku , , :
$ mix phoenix.gen.secret xxxxxxxxxx $ heroku config:set SECRET_KEY_BASE="xxxxxxxxxx" ... ... $ mix phoenix.gen.secret yyyyyyyyyyy $ heroku config:set GUARDIAN_SECRET_KEY="yyyyyyyyyyy" ... ...
!
転記
:
$ git push heroku master ... ... ...
, , , Erlang Elixir , node , npm . , :
$ heroku run mix ecto.migrate
, !
おわりに
Phoenix Heroku . , . , , . , . , .
PSいつものように、タイプミスを報告してください。あなたがフレーズの最高の文言を持っているなら-少なくともPMで、少なくともコメントで、私を書くのが面倒にならないでください、それを更新してください。まあ、そのような出版物を継続するかどうかの質問への答えは、資料の評価を与えます。