Add these functions to the bottom of mix.exs:
# Specifies which paths to compile per environment
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
defp elixirc_paths(_), do: ["lib", "web"]Next, add these entries to your def project config:
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
mix phoenix.gen.resourcerenamed tomix phoenix.gen.html
use Phoenix.HTML no longer imports controller functions. You must add import Phoenix.Controller, only: [get_flash: 2] manually to your views or your web.ex, ie:
# your_app/web/web.ex
defmodule MyApp.Web do
...
def view do
quote do
...
import Phoenix.Controller, only: [get_flash: 2]
end
end
endThe endpoint now requires a :root entry in your config/config.exs:
config :my_app, MyApp.Endpoint,
...
root: Path.expand("..", __DIR__),Code reloader must now be configured in your endpoint instead of Phoenix. Therefore, upgrade your config/dev.exs replacing
config :phoenix, :code_reloader, trueby
config :your_app, Your.Endpoint, code_reloader: trueTh live reloader is now a dependency instead of being shipped with Phoenix. Please add {:phoenix_live_reload, "~> 0.3"} to your dependencies in mix.exs
Additionally, the live_reload configuration has changed to allow a :url option and to work with :patterns instead of paths:
config :your_app, Your.Endpoint,
code_reloader: true,
live_reload: [
# url is optional
url: "ws://localhost:4000",
# `:patterns` replace `:paths` and are required for live reload
patterns: [~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
~r{web/views/.*(ex)$},
~r{web/templates/.*(eex)$}]]Next, the Code and live reloader must now be explicitly plugged in your endpoint. Wrap them inside lib/your_app/endpoint.ex in a code_reloading? block:
if code_reloading? do
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
endChannels received major updates in functionality and tweaking of the APIs and return signatures. Most notably, each channel now runs in its own process, supporthing handle_info/2 and more closely matching GenServer APIs. Additionally "synchronous" messaging is now supported from client to server. By synchronous, I mean being able to reply to an incoming event directly, while ensuring messaging ordering for the same incoming events. This not only lets you do proper request/response messaging where necessary, but it also fixes issues we have in our <= 0.10 apis where joins were not synchronous and messages could be dropped if you fired them before you were fully joined.
Changes:
- The
leave/2callback has been removed. If you need to cleanup/teardown when a client disconnects, trap exits and handle interminate/2, ie:
def join(topic, auth_msg, socket) do
Process.flag(:trap_exit, true)
{:ok, socket}
end
def terminate({:shutdown, :client_left}, socket) do
# client left intentionally
end
def terminate(reason, socket) do
# terminating for another reason (connection drop, crash, etc)
end-
replyhas been renamed topushto better signify we are only push a message down the socket, not replying to a specific request. Update your function calls accordingly. -
The return signatures for
handle_in/3andhandle_out/3have changed, ie:
handle_in(event :: String.t, msg :: map, Socket.t) ::
{:noreply, Socket.t} |
{:reply, {status :: atom, response :: map}, Socket.t} |
{:reply, status :: atom, Socket.t} |
{:stop, reason :: term, Socket.t} |
{:stop, reason :: term, reply :: {status :: atom, response :: map}, Socket.t} |
{:stop, reason :: term, reply :: status :: atom, Socket.t}
handle_out(event :: String.t, msg :: map, Socket.t) ::
{:ok, Socket.t} |
{:noreply, Socket.t} |
{:error, reason :: term, Socket.t} |
{:stop, reason :: term, Socket.t}For existing applications, you can simply change the return signatures of handle_in/handle_out from {:ok, socket} to {:noreply, socket}.
For code moving forward, you can now reply directly to an incoming event and pick up the reply on the client using the {:reply, {status, response}, socket} or {:reply, status, socket}. More examples below.
- update your
phoenix.jsto the new version: (https://raw.githubusercontent.com/phoenixframework/phoenix/918afc1a4f2d1c40154f156e101acf668b5ec93e/priv/static/phoenix.js) chan.send(...)has been renamed tochan.push(...)to match the server messaging commandPhoenix.Socketno longer connects automatically. You need to explicitly callsocket.connect()to start the connection, ie:
var socket = new Phoenix.Socket("/ws")
socket.connect()-
socket.close()has been renamed tosocket.disconnect() -
socket.join(..)api has changed. See the examples below for more details, but all it means is your js goes from:
socket.join("foo:bar", {}, function(chan){
})to
socket.join("foo:bar", {}).receive("ok", function(chan){
})
// or
var chan = socket.join("foo:bar", {})
chan.receive("ok", function(){
})We've overhauled the channel API to allow "synchronous" messaging, and I really love the results. By synchronous, I mean being able to reply to an incoming event directly, while ensuring messaging ordering for the same incoming events. This not only lets you do proper request/response messaging where necessary, but it also fixes issues we have in our <= 0.10 apis where joins were not synchronous and messages could be dropped if you fired them before you were fully joined. With these changes, we have a few high-level concepts which make up channels:
- The client and server push messages down the socket to communicate
- The server can reply directly to a pushed message
- The server can broadcast events to be pushed to all subscribers
The flows looks like this:
- client
push("ev1")-> serverhandle_in("ev1")->server push("ev2")-> clienton("ev2") - client
push("ev1")-> serverhandle_in("ev1")->server broadcast("ev2")-> N subscribershandle_out("ev2")-> N subscriberspush("ev2") -> N clientson("ev2")` - client
push("ev1")-> serverhandle_in("ev")-> server{:reply, :ok, ...}-> clientreceive("ok", ...)
Now let's see some cli/server code:
socket.join("rooms:lobby", {})
.after(5000, () => console.log("We're having trouble connecting...") )
.receive("ignore", () => console.log("auth error") )
.receive("ok", chan => {
// can now bind to channel crash/close events since channels are own processes
chan.onClose( () => console.log("The channel disconnected") )
chan.onError( () => console.log("The channel crashed!") )
$input.onEnter( e => {
// push without response
chan.push("new_msg", {body: e.text, user: currentUser})
})
chan.on("status_change", ({status}) => $status.html(status) )
chan.on("new_msg", msg => $messages.append(msg) )
// push with `receive`'d response, and optional `after` hooks
$createNotice.onClick( e => {
chan.push("create_notice", e.data)
.receive("ok", notice => console.log("notice created", notice) )
.receive("error", reasons => console.log("creation failed", reasons) )
.after(5000, () => console.log("network interruption") )
})
})defmodule Chat.RoomChannel do
use Phoenix.Channel
def join("rooms:lobby", message, socket) do
send(self, {:after_join, message})
{:ok, socket}
end
def join("rooms:" <> _private_subtopic, _message, _socket) do
:ignore
end
def handle_info({:after_join, msg}, socket) do
broadcast! socket, "user_entered", %{user: msg["user"]}
push socket, "status_change", %{status: "waiting for users"}
{:noreply, socket}
end
def handle_in("create_notice", attrs, socket) do
changeset = Notice.changeset(%Notice{}, attrs)
if changeset.valid? do
Repo.insert(changeset)
{:reply, {:ok, changeset}, socket}
else
{:reply, {:error, changeset.errors}, socket}
end
end
def handle_in("new_msg", msg, socket) do
broadcast! socket, "new_msg", %{user: msg["user"], body: msg["body"]}
{:noreply, socket}
end
# this is forward by the default `handle_out`, but show here for clarity
def handle_out("new_msg", msg, socket) do
push socket, "new_msg", msg
{:noreply, socket}
end
endNote that {:reply, {:ok, resp}, socket} on the server, triggers .receive("ok", resp => {}) on the client. The "status" of the reply can be anything, ie {:reply, {:queued, resp}, socket} on the server, triggers .receive("queued", resp => { }) on the client.
Also note that client joining, push, and receiving replies all have the same semantics and API now, which is quite nice.