Static Assets


Static Assets

Instead of implementing its own asset pipeline, Phoenix uses Brunch, a fast and developer-friendly asset build tool. Phoenix comes with a default configuration for Brunch, which works out of the box for Javascript and CSS, and it is very easy to bend it to our needs by adding support for various script and style languages, like CoffeeScript, TypeScript, or LESS.

Brunch has a good tutorial, which should be enough to get us started with asset management from the Phoenix perspective.

Installation

Brunch is a Node.js application. A newly generated Phoenix project contains package.json which lists packages for installation with npm, the Node Package Manager. If we agree to install dependencies when running mix phoenix.new, Phoenix will run npm for us. If we don’t, or if we change package.json, we can always do this ourselves:

npm install

This will install Brunch and its plugins into the node_modules directory.

Default Configuration And Workflow

The second important file is brunch-config.js. This is configuration for Brunch itself. Let’s see how it configures asset management for Phoenix.

According to this configuration Brunch will look for asset source files in assets.

Files and directories in assets/static will be copied to the destination priv/static/ without changes. Only the assets included in the :only option to Plug.Static in endpoint.ex are mounted at the root path. By default only js, css, images, and robots.txt are exposed.

The css and js directories inside of assets are a convention. Brunch will simply look for all files in assets excluding assets/static and sort all found files by their type.

Processed and concatenated javascript will be put into priv/static/js/app.js, styles will be in priv/static/css/app.css.

When Phoenix is running, asset source files are processed automatically when changed, but we can also run Brunch ourselves:

node node_modules/brunch/bin/brunch build

Or we can install Brunch globally:

npm install -g brunch

and then building assets is as simple as

brunch build

In addition to Javascript files found in assets the following source files will always be included into priv/static/js/app.js:

  • Brunch’s “bootstrapper” code which provides module management and require() logic
  • Phoenix Channels JavaScript client (deps/phoenix/assets/js/phoenix.js)
  • Some code from Phoenix.HTML (deps/phoenix_html/assets/js/phoenix_html.js)

Modules

By default each Javascript file will be wrapped in a module, and for this code to be executed it needs to be required and executed from another module. Brunch uses a file path without an extension as the name of a module. Let’s see how it works.

Brunch in Phoenix is configured to use ES6, so we can use ES6 module syntax.

Open assets/js/app.js and add the following code:

export var App = {
  run: function(){
    console.log("Hello!")
  }
}

If this ES6 syntax seems new, this code is essentially the same as the following more familiar CommonJS module syntax:

var App = {
  run: function run() {
    console.log("Hello!");
  }
};

module.exports = {
  App: App
};

Open default application layout lib/hello_web/templates/layout/app.html.eex, find line

<script src="<%= static_path(@conn, "/js/app.js") %>"></script>

and add the following code below:

<script>require("assets/static/js/app").App.run()</script>

When we load this page we should see Hello! in the browser Javascript console.

Take notice of "assets/js/app". This is not really a file name, this is the name of a module into which Brunch wrapped the code in "assets/js/app.js"

Let’s add one more file assets/js/greeter.js:

export var Greet = {
  greet: function(){
    console.log("Hello!")
  }
}

export var Bye = {
  greet: function(){
    console.log("Bye!")
  }
}

and modify assets/js/app.js to require the new module:

import { Greet } from "./greeter"

export var App = {
  run: function(){
    Greet.greet()
  }
}

Please reload the page. We should see Hello! in the browser Javascript console.

Object Bye was not imported into package "assets/js/app", even though Bye is declared as exportable.

Please pay attention to how differently we required module assets/js/app.js from the HTML page, and how we imported module assets/js/greeter from assets/js/app. This is because there is no preprocessing happening for HTML pages and we cannot use ES6 syntax.

Legacy Non-modularized Code

If we have some legacy Javascript code which doesn’t play well with module systems and/or we need global variables it defines, all we need to do is place our Javascript into directory assets/vendor. Brunch will not wrap these files in modules.

Let’s test it. Create assets/vendor if it does not exist yet and create file assets/vendor/meaning_of_life.js with just one line in it:

meaning_of_life = 42;

Reload the page. Open the JS console and type meaning_of_life. This will return 42. The variable is global.

Important detail: according to the default configuration there is no ES6 support for files in assets/vendor. Should we need to enable it, look for plugins: { babel: { ignore: in brunch-config.js.

JavaScript Libraries

We may need to use a JavaScript library like jQuery or underscore in our application. As we mentioned above, we could copy the libraries into assets/vendor. It may be a little bit easier to use npm to install it: We can simply add "jquery": ">= 2.1" to the dependencies in the package.json file in our projects root and run npm install --save. If the npm section in our brunch-config.js has a whitelist property, we will also need to add “jquery” to that. Now we can import $ from "jquery" in our module insideapp.js.

If we already have code that assumes jQuery is available as a global variable, we’ll either need to migrate our code (which is a must-do in the long run), or leave jQuery as a non-wrapped codebase (which is acceptable as a transition hack).

To do so, you would add a globals definition into the config. For example, if we wanted to expose jQuery globally as $, we would modify the config to look like this:

npm: {globals: {
    $: 'jquery',
    jQuery: 'jquery'
  }},

Additionally, some packages ship with stylesheets. To instruct Brunch to add these into the build, use the styles property in the npm config. For example, if we installed the Pikaday package and wanted to include its styles, we’d adjust the config like this:

npm: {styles: {
    bootstrap: ['dist/css/bootstrap.min.css']
  }},

Brunch Plugin Pipeline

All transformations Brunch performs are actually done by plugins. Brunch automatically uses installed plugins listed in package.json. Here is what the pipeline looks like for a newly generated Phoenix project:

Javascript
CSS

It is very easy to add more plugins. We can find a plethora of Brunch plugins on the Brunch website and among NPM packages.

The order in which plugins run is defined by the order in which they appear in package.json.

Let’s add support for CoffeeScript. Edit package.json by adding the following line before javascript-brunch:

"coffee-script-brunch": "^2",

and run

npm install

Let’s rename greeter.js into greeter.coffee and modify its contents to look like the following:

Greet =
  greet: -> console.log("Hello!")

Bye =
  greet: -> console.log("Bye!")

module.exports =
  Greet: Greet
  Bye: Bye

Once Brunch has built our assets, reload our page, and we should see Hello! in the browser Javascript console, just like before.

Adding support for other languages like SASS or TypeScript is as simple as this. Some plugins can be configured in brunch-config.js, but they will all work out of the box once installed.

Other Things Possible With Brunch

There are many more nice tricks we can do with Brunch which are not covered in this guide. Here are just a few:

  • It is possible to have multiple build targets, for example, app.js for our code and vendor.js for third-party libraries
  • It is possible to control the order of concatenation of files. This might be necessary when working with JS files in vendor if they depend on each other.
  • Instead of manually copying third-party libraries into assets/vendor we can use Bower to download and install them.

Should we want one of these, please read the Brunch documentation.

Phoenix Without Brunch

Should we decide not to use Brunch in our new Phoenix project, run the project generator task with option --no-brunch:

mix phoenix.new --no-brunch my_project

Using Another Asset Management System in Phoenix

To integrate another asset management system with Phoenix we will need to

  • configure that asset management system to put built assets into priv/static/.
  • let Phoenix know how to run a command which would watch asset source files and build assets on each change

While the first point is clear, how does Phoenix know which command to run to launch an asset build tool in watch mode? If we open config/dev.exs we will find configuration key :watchers. :watchers is a list of tuples containing an executable and its arguments. That is, with a config like the following:

watchers: [node: ["node_modules/brunch/bin/brunch", "watch"]]

the command launched in the development mode is

node node_modules/brunch/bin/brunch  watch

Let’s see how we can integrate Webpack to work with Phoenix. First we generate a project without Brunch support:

mix phoenix.new --no-brunch my_project

or remove Brunch from the project:

rm brunch-config.js && rm -rf node_modules/*

Modify package.json:

{
  "devDependencies": {
    "webpack": "^1.11.0"
  }
}

Install webpack:

npm install

Create webpack configuration file webpack.config.js:

module.exports = {
  entry: "./assets/js/app.js",
  output: {
    path: "./priv/static/js",
    filename: "app.js"
  }
};

Now we let Phoenix know how to run Webpack. Replace the :watchers definition by the following:

watchers: [node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color"]]

When we restart Phoenix, webpack will be rebuilding assets as they are changed.

Please note that this configuration is very basic. This is just a demonstration of how another asset build tool can be integrated with Phoenix. Please refer to Webpack documentation for details.