Phoenix provides a good developer experience by auto reloading the webpage whenever you change the source code of your project in the development environment. Whether you are working on CSS/JS files or on your elixir module, providing an updated webpage before you switch your window from your code editor to browser is quite handy.
In this post, we will look into how this is working behind the scenes. Phoenix LiveReload as this feature is called is made possible by using Phoenix Channels, a built-in feature in Phoenix framework. Yes, this is the same one that is powering Phoenix LiveView, an actively developed new feature for Phoenix that is making javascript code redundant for many use cases.
To better follow this post, I suggest that you create a new Phoenix project using mix phx.new hello
and follow the code explanations by actually opening the relevant files in your project.
Live Reload functionality is made possible by these 4 components:
If you are already familiar with Phoenix Channel, you can skip this section. You won’t lose anything related to LiveReload. Phoenix Channel makes it possible to create persistent connection between client (browser) and the web server (cowboy). Traditionally, automatic updates to a webpage from the web server had been challenging as the HTTP protocol is client centric, meaning, it’s always the client who makes the request and the server responds to it. Once the server responds to a client, it cannot later decide to send any additional data to the client on its own at a later time without a new request originating from the client. So every response from the server needs a prior request from the client.
Phoenix Channel makes use of web sockets protocol (similar to http protocol) which is becoming the new standard for making 2 way persistent connection between the browser and the server. (Aside, Phoenix Channel is not directly tied with web sockets. In fact, Phoenix calls web sockets as transport layer. There are other transport layers such as LongPoll and these are beyond the scope of this post. It suffices to understand Phoenix Channel makes it possible for the server to communicate with a browser with which it had earlier make an handshake.)
In a brand new phoenix project created using mix phx.new
command, you can notice that dev.exs
has the following code:
config :hello, HelloWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
The code_reloader
config in the dev.exs
is read by Phoenix framework and it creates a boolean variable code_reloading?
which is available in the endpoint.ex
file in your project.
The following code is present in the endpoint.ex
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello
end
Endpoint is responsible for serving all web request to your phoenix project. So the above code is conditionally injecting several plugs and Phoenix socket configuration (used by Phoenix Channel) into this endpoint.ex file.
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
The above code defines a path /phoenix/live_reload/socket
and maps it to a module Phoenix.LiveReloader.Socket
. Any request coming to this path will initiate a persistent connection between the browser and the server using Phoenix Channels using the module Phoenix.LiveReloader.Socket
.
Now, let’s go http://localhost:4000 after starting Phoenix server using mix phx.server
. Looking at the html source code in the browser, you can notice the following iframe
code injected before the </body>
tag:
<iframe src="/phoenix/live_reload/frame" style="display: none;"></iframe>
This iframe code is not from your phoenix project but got injected from Phoenix LiveReload library. The content of this iframe is an html document containing just a single<script>
tag which is truncated below:
<script>
// first part
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof
...
// second part
var socket = new Phoenix.Socket("/phoenix/live_reload/socket");
var interval = 100;
// third and final part
...
socket.connect();
var chan = socket.channel('phoenix:live_reload', {})
chan.on('assets_change', function(msg) {
var reloadStrategy = reloadStrategies[msg.asset_type] || reloadStrategies.page;
setTimeout(function(){ reloadStrategy(chan); }, interval);
});
chan.join();
</script>
The first part of the javascript that I have truncated above is the minified version of phoenix.js
from phoenix framework. This part of the js provides the Phoenix channel js client library.
The second part of the script configures the socket path using the js client Phoenix.Socket
(provided in the first part above).
var socket = new Phoenix.Socket("/phoenix/live_reload/socket");
var interval = 100;
Note the channel connection is made to the path /phoenix/live_reload/socket
and if you remember correctly, this is the path added in endpoint.ex
conditionally if code_reloading?
is true.
The third part of the js makes the actual socket connection and join the channel phownix:live_reload
and starts listening on messages for the event assets_change
. So whenever the Phoenix Channel server triggers the event assets_change
and sends a message along with it, the javascript in the iframe will listen to it and respond.
We have a regular html generated from your project with an additional iframe injected by Phoenix LiveReload library. This iframe contains an html document with a <script>
tag which makes a phoenix channel connection to your application server running on localhost:4000 at the path /phoenix/live_reload/socket
. This can be verified by looking at the network tab for websocket connection in your browser’s developer tools.
Now, when you make changes to your css, or js or any of your elixir module, the File System library which is included as a dependency library to Phoenix LiveReload notifies the LiveReload channel about the files that got changed. Phoenix LiveReload them sends a message to the browser with the type of file that got changed. Remember, the actual client that initiated the Phoenix Channel connection is the iframe inside the project’s main html page. So the message from the server about the file change is received by the iframe and not by the parent html document.
Now, lets look into a what kind of message gets transmitted from the Phoenix Channel server and how the js client reacts to it. Open your developer tools in your browser and in the network tab, select "Preserve log" and filter the network traffic to show only websocket connection by selecting "WS". If you then make changes to app.css
file in your project, you will see a message in your websocket connection as in the screenshot below.
If you notice carefully, there are two messages: {asset_type: "js"}
followed by {asset_type: "css"}
. The reason for this is because of webpack bundler used by Phoenix to compile assets. All asset files (css, js, images, fonts) are watched by webpack server. The css file that I changed was app.css
. This css is included in the js file app.js
(as in the default phoenix project). Webpack compiles a new version of js and css files after every change to these files. Hence, even though I changed a css file (app.css), Phoenix server is reporting two events, one for the app.js file and another for the app.css both of which are emitted by Webpack bundler.
Let's take the message {assert_type: "js"}
and see how our js client in iframe responds to this.
The js client code responsible for handling the event and message is below:
chan.on('assets_change', function(msg) {
var reloadStrategy = reloadStrategies[msg.asset_type] || reloadStrategies.page;
setTimeout(function(){ reloadStrategy(chan); }, interval);
});
msg
in the above function is the javascript object received as message {asset_type: "js"}
.
Based on the value of asset_type
, we find the relevant reloadStrategy. Reload strategy is a js function which when called does the minimal changes required for refreshing the page. There are two strategies present in Phoenix LiveReload: css
and page
. Since the asset_type that got changed in js
in our case, page
reload strategy is called.
Let's look into the page
reload strategy function and what it does:
var pageStrategy = function(chan){
chan.off('assets_change');
window.top.location.reload();
};
window
refers to the iframe
document which is receiving the channel messages while window.top
refers to the top most parent document which is your actual phoenix project page that is embedding this iframe
. A call to location.reload
on window.top
then reloads your webpage on browser and there ends our Phoenix LiveReload magic!
A page got refreshed once you made a change to assets without you having to manually refresh the browser.
Simple isn’t it?
Now, I could go on to explain several other nitty gritties of Phoenix LiveReload (and there are still a lot in this tiny library) but I think it's best to leave you with a few exercises so you have the fun in finding the answers.
iframe
content in the response? And which code is responsible for doing this?iframe
is not recursively included in the HTML content of the iframe but only in the main html document?app.css
triggers a {asset_type: "js"}
and {asset_type: "css"}
message. Can you think of a scenario where only css
message is triggered?Feel free to post your answers on twitter by tagging me. I will be happy to check the answers and clarify any questions. Avoid posting the answers here as comments as it might deprieve the joy of findings the answers for other readers ;-)
If you have made so far and found the post interesting, please consider donating to the COVID-19 relief fund that I am collecting: https://gumroad.com/l/EEZzq or buy my books here: http://bit.ly/phoenixio 100% of all sales goes to helping people affected by COVID-19 financially. These include daily wagers, cab drivers and self-employed small business owners.