Using Phoenix with Webpack and Docker
Phoenix 1.4 is set to use Webpack but Phoenix 1.3 currently uses Brunch. Here's how to get Webpack working with Phoenix 1.3.
I recently started a new Phoenix project and I found myself wanting to use SASS along with ES6 Javascript. I’ve used Webpack before but not Brunch.
I also happen to be a big fan of using Docker, so it’s no surprise that I wanted to get everything working with Docker.
So I set out to create a development environment where I could just run 1 command and have my Phoenix server along with a Webpack watcher and PostgreSQL all start up. That part was easy enough with Docker Compose.
But it was pretty tricky wiring everything up together, so this article explains it all and even includes a working example app that you can use as a reference.
The main focus of this article will be on getting Webpack working with Phoenix. The Docker bits are all optional.
# Here’s What We’ll Cover in This Article
- Generate a Phoenix application using the
--no-bunch
flag - Create a new
/asset
folder to store your front-end source code - Use Yarn instead of NPM for a better package management experience
- Set up a
package.json
file to install everything we need - Configure Webpack for:
- SASS / Autoprefixer
- ES6 Javascript
- File loader for images and fonts
- Minification in production
- CSS files will be output to their own bundle
- Bundle an
app.ccs
andapp.js
file
- Create example SCSS and JS files to test it out
- Configure Phoenix to start Webpack
- Make sure everything works
- Go over a few Docker specific things
# Setting up Webpack with Phoenix
The main focus of this article is on getting Phoenix and Webpack to work. It just so happens everything is ran through Docker (which is optional by the way).
Generating a Demo Application
The only thing we’ll need to change when generating a new application is to
disable Brunch, which we can do by running mix phx.new --no-brunch hello
.
The Docker example app will take care of installing Phoenix’s dependencies for you, but if you’re not using Docker you’ll want to let Phoenix install the dependencies for you.
Creating a New Asset Folder for Our Source Files
Technically you can put this anywhere you want but I think an assets/
folder
that is at the same depth as your lib/
folder is a reasonable spot for this.
Feel free to change this later if you want but keep in mind if you do change this you will need to update a few paths across a couple of files.
Using Yarn and Installing NodeJS
I’m not going to walk through these steps, especially since it’ll be different for every environment. This is partly why I like using Docker by the way.
Just make sure you npm install yarn
in your Node environment.
Setting up a package.json File
Create a package.json
file inside of your assets/
folder and then add this:
{
"repository": {},
"dependencies": {
"phoenix": "~1.3",
"phoenix_html": "~2.10"
},
"devDependencies": {
"autoprefixer": "~8.5",
"babel-core": "~6.26",
"babel-loader": "~7.1",
"babel-preset-es2015": "~6.24",
"css-loader": "~0.28",
"extract-text-webpack-plugin": "~3.0",
"file-loader": "~1.1",
"imports-loader": "~0.8",
"node-sass": "~4.9",
"postcss-loader": "~2.1",
"precss": "~1.3",
"sass-loader": "~7.0",
"style-loader": "~0.21",
"webpack": "~3.10",
"webpack-merge": "~4.1"
},
"scripts": {
"build": "NODE_ENV=production /node_modules/webpack/bin/webpack.js -p",
"watch": "NODE_ENV=development /node_modules/webpack/bin/webpack.js --config /app/assets/webpack.config.js --progress --color --watch"
}
}
I’ll be keeping these versions synced up to what’s included in the example repo.
The above files assumes you’ll have everything installed using my Docker
example app as a reference, but if you don’t plan to use Docker then you’ll
want to adjust the paths to webpack
in the scripts
section of the file.
If you decide not to use Docker, you would most likely end up removing the
leading /
since node_modules
would be relative to the current directory.
If you’re not using Docker this is where you’ll want to run yarn install
too.
Configuring Webpack:
Create a webpack.config.js
file inside of your assets/
folder and then add this:
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var merge = require("webpack-merge");
var webpack = require("webpack");
var env = process.env.NODE_ENV || "development";
var production = env === "production";
var node_modules_dir = "/node_modules"
var plugins = [
new ExtractTextPlugin("css/app.css")
]
if (production) {
plugins.push(
new webpack.optimize.UglifyJsPlugin({
compress: {warnings: false},
output: {comments: false}
})
);
} else {
plugins.push(
new webpack.EvalSourceMapDevToolPlugin()
);
}
var common = {
watchOptions: {
poll: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: [node_modules_dir],
loader: "babel-loader",
options: {
presets: ["es2015"]
}
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
},
{
loader: 'postcss-loader',
options: {
plugins() {
return [
require("precss"),
require("autoprefixer")
];
}
}
},
{
loader: 'sass-loader'
}
]
})
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader?name=/images/[name].[ext]"
},
{
test: /\.(ttf|otf|eot|svg|woff2?)$/,
loader: "file-loader?name=/fonts/[name].[ext]",
}
]
},
plugins: plugins
};
module.exports = [
merge(common, {
entry: [
__dirname + "/app/app.scss",
__dirname + "/app/app.js"
],
output: {
path: __dirname + "/../priv/static",
filename: "js/app.js"
},
resolve: {
modules: [
node_modules_dir,
__dirname + "/app"
]
}
})
];
The basic idea here is, if you make a change to app.scss
or app.js
then
Webpack is going to run its course and then produce its output in the
usual priv/static
directory.
That’s happening because there’s a line near the bottom that sets
path: __dirname + "/../priv/static",
. That ensures all of our bundles get
output there. In which case, Phoenix will pickup the changes automatically
because that’s where it expects assets to live.
If you’re using the Docker example app you won’t need to change anything.
If you don’t plan to use Docker then you’ll need to make a few changes:
- Change the
node_modules_dir
variable to point to yournode_modules
directory - Change the 3
/app
references near the bottom of the config to./
- Remove the
watchOptions
poll option (we need this for Docker support)
Creating Example SCSS and JS Files
What I like to do is create an app/
folder inside of the assets/
folder
and this is where all of our source assets will exist.
So inside of that app/
folder, create both an app.scss
and app.js
file.
We can leave them as empty files for now. Our goal is to have everything compile and run properly, and then we can change them later to test it out.
Configuring Phoenix to Start Webpack
If you use the Docker example app then you won’t need to do this step, but in case you’re not using Docker, it would be pretty nice if Phoenix started Webpack for you automatically.
That can be done by going to your config/dev.exs
file and then under your
app’s EndPoint
config, change the watchers list to look like this:
watchers: [yarn: ["run", "watch", cd: Path.expand("../assets", __DIR__)]]
That yarn command matches up to what we have defined in our package.json
file.
I don’t run Phoenix outside of Docker, so I’m not sure how well this is going to work out in practice. I like running Phoenix and Webpack as separate processes because if I need to restart the Phoenix server, I wouldn’t necessarily want to restart the Webpack watcher.
Let me know how it goes for you in the comments.
Starting Everything up
If you’re using the Docker set up you’ll just want to run docker-compose up --build
and wait a few minutes while everything builds for you automatically.
If you’re not using the Docker example app you’ll want to run mix phx.server
,
and possibly yarn run watch
in a different terminal if you don’t do the above
step to integrate Webpack as a watcher with Phoenix.
What you’re looking for here would be to have both Phoenix and Webpack successfully running in the foreground.
Your Webpack output should look something like this:
10% [0] building modules 1/1 modules 0 active
Webpack is watching the files…
Compiling 13 files (.ex)
Version: webpack 3.10.0
Child
Hash: fe7feb13ffe07e2b4bd8
Time: 2508ms
Asset Size Chunks Chunk Names
js/app.js 3.5 kB 0 [emitted] main
css/app.css 0 bytes 0 [emitted] main
[0] multi ./app/app.scss ./app/app.js 40 bytes {0} [built]
[1] ./app/app.scss 41 bytes {0} [built]
[2] ./app/app.js 13 bytes {0} [built]
I’ve removed some of the output to reduce the noise here, but the important thing is you shouldn’t see any errors. If you do, double check all of your paths.
Testing Everything Out
If you’re using the Docker example app then you won’t be able to see these changes in your browser because the default layout template just outputs a simple hello world message.
But, you should see the files get successfully compiled on the Webpack end.
Making a Javascript change:
Open up your assets/app/app.js
file and then add console.log("Hello!)
and
save it.
You should see a Webpack message that looks similar to this:
Child
Hash: e8374bafa3fc5db3dbb8
Time: 15ms
Asset Size Chunks Chunk Names
js/app.js 3.75 kB 0 [emitted] main
css/app.css 0 bytes 0 [emitted] main
[2] ./app/app.js 37 bytes {0} [built]
+ 2 hidden modules
Not too shabby. It only took 15ms for it to be updated, and in my case, that’s going through a Docker volume.
Making a SCSS change:
Open up your assets/app/app.scss
file and then add body { color: #3f0; }
and save it.
You should see a Webpack message that looks similar to this:
Child
Hash: cdcc0eb35bdf6305add7
Time: 44ms
Asset Size Chunks Chunk Names
css/app.css 24 bytes 0 [emitted] main
+ 1 hidden asset
[1] ./app/app.scss 37 bytes {0} [built]
+ 2 hidden modules
Even when I use Bootstrap 4 and a custom theme with a massive amount of SCSS it’s still quite fast. One theme that I use pulls in 10,000+ lines of SCSS and the whole thing compiles in 2.8 seconds using Docker on Windows.
Going over a Few Docker Specific Things
If you look through the example app, you might be wondering what’s up with a few design choices I made on the Webpack and Docker side of things.
Dealing with the mix.lock file:
In a previous article
I wrote about handling lock files.
The TL;DR is without doing a strategy that’s similar to what I did in the
ENTRYPOINT
script then your lock file will never be created on your Docker host.
This same approach also works with the yarn.lock
file.
Polling for file changes in the Webpack config:
Unfortunately Webpack and Docker doesn’t get along for when it comes to detecting file changes through Docker for Windows. This might be different with Docker for Mac, but the Windows file system will not pass through file changes to the container.
So to combat that, the polling option was turned on in the watch options so it works for everyone out of the box. Feel free to comment it out and try it on your end.
Using the Webpack watcher instead of the dev server:
I’ve tried using the webpack-dev-server and even set up a custom Express app along with Webpack’s middleware to compare its performance to what we’re doing here which is just writing the assets to disk.
When you’re dealing with Docker volumes, it makes no difference since we’re dealing with disk access. I benchmarked all 3 strategies and the compile speed was exactly the same.
Setting up a tmpfs volume mount in the Docker Compose file:
I’ll admit, I’m not really sure if this has any extra benefits, such as creating less writes to disk (which could be handy if you have an SSD).
I benchmarked a regular volume mount and the tmpfs driven volume mount which according to Docker makes the mount live in memory instead of hitting the disk and the performance was the same with both set ups.
I left the tmpfs option in there because it doesn’t seem to hurt anything and if it does produce less disk writes, that’s a good thing. If anyone knows the real answer behind this, please let me know in the comments.
# Final Thoughts
I’m really happy that Phoenix 1.4 will be using Webpack instead of Brunch by default.
I’m personally not a big fan of creating SPA style Javascript applications. I’m more of a “sprinkle JS when needed” type of person while leveraging server side templates and Turbolinks, but I do rely on a lot of SCSS and some JS to power my apps and Webpack is quite popular and well supported.
One cool thing about all of what we went over today is it should all work fine when Phoenix 1.4 is released. If anything does change, I’ll update the example app and this article.
Were you able to get everything working? Let me know below.