Phoenix Framework: the assets pipeline
- Part 1: To the basics and beyond
- Part 2: The assets pipeline
- Part 3: Long running processes
Updates
From the time I wrote part 1
of this short series, Atom has gained a new Elixir plugin based on Samuel Tonini’s Alchemist Server.
From the Emacs plugin, it inherits all the most notable features such as
autocomplete, jump to definition/documentation for the function/module under
the cursor, quote/unquote code and interactive macro expansion.
A feature reference along with some screenshots can be found at the atom-elixir page.
It also looks pretty good.
The assets pipeline
Assets pipelines are one of the most important features in modern web frameworks.
When working on this task, Phoenix developers have proven that they value
pragmatism over purity and have chosen to base their implementation on Brunch, a Node.js build tool that takes care of everything
related to assets management.
This choice has probably saved man-years of work, that would have inevitably delayed the release of a fully working pipeline system.
A very common counter argument is that this adds node as a dependency, but I
think it’s a negligible inconvenient, node is most probably already present on
the majority of developers machines.
Brunch installation is just a npm install
away and it runs automatically when
you create a new Phoenix project
$ mix phoenix.new brunch_demo
* creating brunch_demo/config/config.exs
* creating brunch_demo/config/dev.exs
...
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build
As you can guess, the local brunch is installed in node_modules/brunch/bin/brunch
.
You can install it globally with the usual -g
flag: npm install -g brunch
.
Phoenix automatically runs Brunch when assets change.
If you launch mix phoenix.server
and make some changes to web/static/js/app.js
you can see that Brunch is working in the background.
[info] Running BrunchDemo.Endpoint with Cowboy using http on port 4000
28 Apr 13:31:56 - info: compiled 5 files into 2 files, copied 3 in 2.2 sec
28 Apr 13:33:08 - info: compiled app.js and 3 cached files into app.js in 118ms
Defaults
By default Brunch watch for changes in css
and js
folders inside web/static
and automatically recompile and package them for HTTP serving.
It is configured to work with ES6 and transpile it to ES5 through
Babel, so you can start using ES6 today, without hassles.
Javascript files are automatically wrapped into a module, that needs to be required
before being used.
Every file inside web/static/js
will be converted and loaded on demand.
// web/static/js/app.js
export var App = {
run: function(){
console.log("Hello from Phoenix!")
}
}
<!-- web/templates/layout/app.html.eex -->
<script>require("web/static/js/app").App.run()</script>
<!-- shows "Hello from Phoenix!" in the browser's console -->
If you have legacy code that won’t work if modularized or doesn’t need modularization,
you can put it in web/static/vendor
and it will be copied as it is.
This is also the easier way to create global variables
// web/static/vendor/globals.js
global_variable = "this is global";
// open up the console and type global_variable
// can you guess the result?
Any of this default can be changed in brunch-config.js
.
For example this is the part that ignores the module wrapping for the vendor folder.
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
}
},
Last but not least, Phoenix automatically loads
- Brunch’s “bootstrapper” code which provides module management and require() logic
- Phoenix Channels JavaScript client (deps/phoenix/web/static/js/phoenix.js)
- Some code from Phoenix.HTML (deps/phoenix_html/web/static/js/phoenix_html.js)
Plugins
Brunch is configured to load a numbers of plugin, specifically
- javascript-brunch: enables processing of Javascript files
- babel-brunch: the ES6 transpiler
- uglify-js-brunch: Javascript minifier
- css-brunch: enables processing of CSS files
- clean-css-brunch: CSS minifier
Plugins are installed through npm
, for example if you wanna use Coffeescript in
your application you can simply npm install --save coffee-script-brunch
and
your coffee files will be automatically picked up and processed.
Do you need SASS? npm install --save sass-brunch
, and so on.
Tool belt
Manually copying files or libraries inside the vendor folder is not exactly the
best way to handle external dependencies.
One of the features of Brunch is that it allows the developers to take advantage of the Node
ecosystem.
Brunch works seamlessly with Bower, which IMHO is the simplest way to handle
third-party frontend dependencies.
Do you need underscore
?
Just bower install --save underscore
, (restart the server if the files are not being compiled automatically) and type _
in the console
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}
One problem I’ve found is that not every folder provided by the packages is being copied (or concatenated) in the output folder (usually priv/static
unless you changed it).
I was working with an old version of materialize
and the font was not being loaded.
The solution was pretty easy, just open up lib/<app_name>/endpoint.ex
and look for this line
plug Plug.Static,
only: ~w(css fonts images js favicon.ico robots.txt)
In my case materialize
was using font
as a folder name, but it wasn’t whitelisted
in the Static Plug configuration. I added font
to the list and it fixed the issue.
There’s more than a way
One thing that the Phoenix framework does and does really well is not being
strongly opinionated.
Brunch is just the default assets manager, but it’s really simple to use another one, just change this line in dev.ex
watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]]
There’s an example in the Pheonix documentation on how to use Phoenix with webpack, I’m gonna go further, I’ll show you how to write the skeleton of an assets manager and get it started by Phoenix.
Create a new file in lib/watcher.exs
(exs means Exlixir script) and paste this code inside it
# lib/watcher.exs
defmodule Watcher do
def start do
IO.puts "* Start monitoring #{Path.absname("web/static")}"
IO.inspect System.argv
IO.inspect System.get_env
end
end
Watcher.start
and then change the configuration this way
watchers: [mix: ["run", "lib/watcher.exs", "random input"]]
and start the Phoenix server
mix phoenix.server
[info] Running BrunchDemo.Endpoint with Cowboy using http on port 4000
* Start monitoring <app_path>/web/static
["random input"]
%{"CLICOLOR" => "1", "PROMPT_COMMAND" => "_update_ps1; update_terminal_cwd",
"_system_arch" => "x86_64",
"DISPLAY" => "/private/tmp/com.apple.launchd.4Zqdr48hnr/org.macosforge.xquartz:0",
...
Ok, it works but it’s not really useful, we’re gonna add a new feature, that watches
for file changes inside the web/static
folder and logs to the screen.
First we’re gonna need to install a filesystem watcher component, there is one written in Erlang that we can import directly from Github.
Add this line to mix.exs
# mix.exs
defp deps do
# ...
{:fs, github: "synrc/fs", override: true}]
# override tells the Elixir compiler that this package overrides the default
# :fs Erlang module
end
download the library with mix deps.get
and then update the watcher.exs
code
# lib/watcher.exs
defmodule Watcher do
def start do
IO.puts "* Start monitoring #{Path.absname("web/static")}"
IO.inspect System.argv
IO.inspect System.get_env
# starts the listener
:fs.start_link(:watcher, Path.absname("web/static"))
:fs.subscribe(:watcher)
loop
end
def loop do
receive do
{_watcher_process, {:fs, :file_event}, {path, flags}} ->
# logs events to the screen
IO.puts("* #{path} -> #{Enum.join flags, ", "}")
end
loop
end
end
Watcher.start
Start the server again, change some file inside web/static
and enjoy the results.
# save app.js
* <app_path>/web/static/js/app.js -> inodemetamod, modified
# save a new file
* <app_path>/web/static/js/app2.js -> created, modified, finderinfomod, xattrmod
# delete a file
* <app_path>/web/static/js/app2.js -> renamed
That’s it for now, next time we’ll talk about long running processes.