Configuring a Phoenix app with Vue.js & Webpack
With the new project architecture proposed for Phoenix 1.3 there’s a lack of information on how to set a custom frontend. So, in this post I’ll set a Vue.js app for the front, served by webpack.
Why webpack? To me Brunch is not enough, the tooling that supports Vue is currently better on Webpack so we’ll use it.
Getting started
To get this settings you should have Phoenix version ~1.3 installed. If you don’t have it, at the moment of this writing, you can just run:
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
Assuming you have Mix and Elixir installed, right?
Then let’s create a new Phoenix project by running (on your desired path):
mix phx.new phoenix_vue
And select no when asking to install dependencies, this will make easier to change settings. Notice that we’re not running mix phoenix.new
anymore.
We could have added --no-brunch
to our command but it’s easier this way since we can just delete
the brunch configuration and dependencies, plus it will create the assets folder (where our Vue
app will live) for us.
Then let’s move to our project directory and install Phoenix deps.
cd phoenix_vue
mix deps.get
No we can start with our configuration, first we’ll visit the assets directory and remove the brunch-config.js
file:
cd assets
rm brunch-config.js
And while here, I’ll rename the js folder to src (just a personal preference) and remove the css folder.
rm -rf css
Lets update our package.json
file to look like this (basically remove al brunch dependencies):
{
"repository": {},
"license": "MIT",
"scripts": {
},
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html"
},
"devDependencies": {
}
}
We’re now ready to fetch our personalized dependencies (you can use npm or yarn):
yarn add vue
And the dev dependencies too:
yarn add -D babel-core babel-loader babel-preset-stage-2 copy-webpack-plugin css-loader
extract-text-webpack-plugin file-loader image-webpack-loader style-loader url-loader
vue-loader vue-style-loader vue-template-compiler webpack webpack-merge
We can also add suport for sass and stylus preprocessors:
yarn add -D node-sass sass-loader stylus stylus-loader
Let’s add some scripts to our package.json
file:
{
// ...
"scripts": {
"start": "yarn run dev",
"dev": "MIX_ENV=dev webpack --watch-stdin --progress --color",
"deploy": "MIX_ENV=prod webpack -p"
},
// ...
}
And then let’s create our main Webpack configuration file on the assets folder:
touch webpack.config.js
Open it and copy this:
'use strict'
// Modules
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// Environment
const Env = process.env.MIX_ENV || 'dev'
const isProd = (Env === 'prod')
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = (env) => {
const devtool = isProd ? '#source-map' : '#cheap-module-eval-source-map'
return {
devtool: devtool,
entry: {
app: './src/main.js'
},
output: {
path: path.resolve(__dirname, '../priv/static'),
filename: 'js/[name].js'
},
resolve: {
extensions: ['.js', '.vue', '.json', '.css', '.scss', '.styl'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: ['css-loader', 'sass-loader', 'stylus-loader'],
fallback: 'vue-style-loader'
})
}
}
}, {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
}, {
test: /\.(gif|png|jpe?g|svg)$/i,
exclude: /node_modules/,
loaders: [
'file-loader?name=images/[name].[ext]',
{
loader: 'image-webpack-loader',
options: {
query: {
mozjpeg: {
progressive: true
},
gifsicle: {
interlaced: true
},
optipng: {
optimizationLevel: 7
},
pngquant: {
quality: '65-90',
speed: 4
}
}
}
}
]
}, {
test: /\.(ttf|woff2?|eot|svg)$/,
exclude: /node_modules/,
query: { name: 'fonts/[hash].[ext]' },
loader: 'file-loader'
}
]
},
plugins: isProd ? [
new ExtractTextPlugin({
filename: 'css/[name].css',
allChunks: true
}),
new CopyWebpackPlugin([{
from: './static',
to: path.resolve(__dirname, 'priv/static'),
ignore: ['.*']
}]),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true,
beautify: false,
comments: false
})
] : [
new ExtractTextPlugin({
filename: 'css/[name].css',
allChunks: true
}),
new CopyWebpackPlugin([{
from: './static',
to: path.resolve(__dirname, 'priv/static'),
ignore: ['.*']
}])
]
}
}
Basically we’re declaring our app’s entry point ./src/main.js
and exporting everything to the
priv/static
folder of our project root path. We’re also extracting all styles from Vue components
files to a single css file. Basic support for images and font files is also added.
Go to the src directory and create the components folder and some files:
cd src
mkdir components
touch App.vue main.js components/Hello.vue
Edit the 3 files to look like these:
// => main.js
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
new Vue({
el: '#app',
template: '<App/>',
components: { App }
})
<!-- App.vue -->
<template>
<div id="app">
<header>
<h1>Phoenix Vue</h1>
</header>
<main>
<hello></hello>
</main>
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
name: 'app',
components: {
Hello
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
main {
margin-top: 60px;
}
</style>
<!-- components/Hello.vue -->
<template>
<div class="hello">
<h3>Hello from Vue.js!</h3>
<p>{{msg}}</p>
</div>
</template>
<script>
export default {
name: 'hello',
data() {
return {
msg: 'Simple example using Phoenix, Vue and Webpack.'
}
}
}
</script>
<style scoped>
p {
margin-top: 40px;
}
</style>
Finally let’s add our .babelrc
file to the assets folder:
{
"presets": [
"stage-2"
],
"comments": false,
"env": {
"development": {
"presets": ["stage-2"]
}
}
}
For now, we’re done with the Vue & Webpack part, now lets configure our Phoenix app.
Return to our main project folder phoenix_vue
. First we’ll edit our .gitignore
, find the ‘Static
artifacts’ section and add the following:
# Static artifacts
/assets/node_modules
/assets/dist/
/assets/npm-debug.log
/assets/yarn-debug.log
/assets/yarn-error.log
/assets/.sass-cache
/assets/.tern-port
Next we’re going to edit the config/dev.exs
file, let’s add this to the watchers section:
# ...
watchers: [
node: ["node_modules/.bin/webpack", "--colors", "--watch-stdin", "--progress",
cd: Path.expand("../assets", __DIR__)]
]
# ...
This way we can watch changes on the assets folder and recomplie with webpack using Phoenix’s hot reaload functionality.
That’s it for config, now lets update our Phoenix views:
cd lib/phoenix_vue/web
Let’s edit the templates/layout/app.html.eex
file and clean the body.
<body>
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
And the templates/page/index.html.eex
file to simply show:
<div id="app"></div>
And that’s it! If we go to the project root path and run iex -S mix phx.server
we will see our
Vue.js content served by Phoenix.
You can find a working example here: https://github.com/pggalaviz/phoenix-vue.