åé¡
ç¶è¡ããåã«ãéæãããããšã«ã€ããŠèããŠã¿ãŸãããã ãã®ãããå®éã«ã¯ãæ²ç€ºæ¿ãšããã®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 # /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 # /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 # /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 // /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 /éç/ js /ãã¥ãŒ/ããŒã/ show.js // 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 /éç/ js /ãã¥ãŒ/ããŒã/ show.js // 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 /éç/ js /ã³ã³ããŒãã³ã/ãªã¹ã/ form.js // 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 /ãã£ã³ãã«/ board_channel.ex # 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 // /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 // /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
ããã«ãããæ°ããHerokuã¢ããªã±ãŒã·ã§ã³ãäœæãããå
¬éã«äœ¿çšãããªã¢ãŒãheroku
gitãªããžããªãè¿œå ãããŸãã åè¿°ããããã«ãPhoenixã¢ããªã±ãŒã·ã§ã³ã«ã¯ã2ã€ã®ç°ãªããã«ãããã±ãŒãžãå¿
èŠã§ãã
- heroku-buildpack-elixir ïŒElixirã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ããããã®ã¡ã€ã³ã»ããã
- heroku-buildpack-phoenix-static : .
.buildpacks
:
# .buildpacks https:
, 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ã§ãå°ãªããšãã³ã¡ã³ãã§ãç§ãæžãã®ãé¢åã«ãªããªãã§ãã ããããããæŽæ°ããŠãã ããããŸãããã®ãããªåºçç©ãç¶ç¶ãããã©ããã®è³ªåãžã®çãã¯ãè³æã®è©äŸ¡ãäžããŸãã