Emily
How To Get Phoenix & VueJS working Together?
I have VueJS GUIs with the project generated using Webpack.
I have Elixir modules that will need to be used by the VueJS GUIs.
I foresee I will eventually have an umbrella Phoenix project. Right now it is just VuejS, Javascript, and Elixir modules.
I need to merge the files from VueJS, Elixir, & Phoenix into 1 project.
I need it to eventually be a compiled standalone app.
I have no clue where to go next doing this according to conventions.
Anyone have a guide or tutorial for the next steps?
Most Liked
maz
building on @paulanthonywilson 's post, in case you weren’t aware you will need to do a couple other things to get Vue.js “working”
This is my naive implementation, there may be more steps for other features and nuances but this will get something showing up in a browser. Using Phoenix 1.4 RC0:
edit package.json:
{
"repository": {},
"license": "MIT",
"scripts": {
"deploy": "webpack --mode production",
"watch": "webpack --mode development --watch"
},
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.0",
"copy-webpack-plugin": "^4.5.0",
"css-loader": "^0.28.10",
"mini-css-extract-plugin": "^0.4.0",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"uglifyjs-webpack-plugin": "^1.2.4",
+ "vue": "^2.5.17",
+ "vue-loader": "^15.4.2",
"webpack": "4.4.0",
"webpack-cli": "^2.0.10"
}
}
cd <project root>
cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
^^ this should have npm add Vue as a dependency
in <project root>/assets/js/: add and save new file vue-hello-world.js:
import Vue from 'vue'
const helloWorldContainer = document.querySelector("#hello-world-container")
if (helloWorldContainer) {
new Vue({
el: "#hello-world-container",
data() {
return {
msg: "Hello World from Vue!"
}
}
});
}
^^ we will eventually add a hello-world-container div tag
in <project root>/assets/js/: edit file app.js and import/glue your vue-hello-world.js to the global app imports at the bottom:
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.css"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
//
import "phoenix_html"
// Import local files
//
// Local files can be imported directly using relative paths, for example:
// import socket from "./socket"
+ import "./vue-hello-world"
Open <project root>/templates/page/index.html.eex and add your div:
+ <div id="hello-world-container" >
+ <h1>{{ msg }}</h1>
+ </div>
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>A productive web framework that<br/>does not compromise speed and maintainability.</p>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides & Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md">v1.4 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
</ul>
</article>
</section>
From the project root run the app: mix phx.server
You should see a webpack log similar to:
[./js/vue-hello-world.js] 275 bytes {./js/app.js} [built]
Open localhost:4000. You should see the “Hello World from Vue!” message.
Finally, get the Chrome Vue plugin:
peerreynders
Seems to me lots of blogs about Phoenix get into Brunch like it’s somehow an essential part of Phoenix - it isn’t. When it comes to the development workflow there is one loose “connection” to Brunch in my_app/config/dev.exs - the watchers configuration. It contains the command to start up the process responsible for building bundles whenever the primary files change. Phoenix LiveReload is then responsible for supplying the re-built files to the browser via a hidden <iframe> which coordinates the necessary updates in the browser.
Vue with Webpack (instead of Brunch) can make use of the same mechanism. To demonstrate, start with a simple Vue.js component project:
$ tree assets
assets
├── build
│ └── webpack.dev.conf.js
├── images
├── index.html
├── js
├── package.json
└── src
├── App.vue
├── assets
│ └── phoenix.png
└── main.js
assets/index.html:
<!DOCTYPE html>
<html>
<head>
<!-- assets/index.html -->
<meta charset="utf-8">
<title>Vue.js Demo Page</title>
</head>
<body>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>
assets/src/App.vue:
<template>
<div class="foo">
<img src="./assets/phoenix.png">
<h1>{{ msg }}</h1>
<button id="change-message" @click="changeMessage">Change message</button>
<p>{{ passedProp }}</p>
</div>
</template>
<script>
export default {
name: 'hello',
data() {
return {
msg: 'Welcome to Your Vue.js App'
};
},
props: ['passedProp'],
methods: {
changeMessage() {
this.msg = 'new message';
}
}
};
</script>
<style>
.foo {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
assets\src\main.js:
// src/main.js
import Vue from 'vue';
import App from './App';
Vue.config.productionTip = false;
new Vue({
el: '#app',
template: '<App passedProp="Greetings!" />',
components: { App }
});
assets/build/webpack.dev.conf.js:
// build/web.config.dev.js
const path = require('path');
const webpack = require('webpack');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const PATHS = {
assetsRoot: '/',
assetsSubDirectory: '/'
};
function assetsPath(filePath) {
return path.posix.join(PATHS.assetsSubDirectory, filePath);
}
function resolve (dir) {
return path.join(__dirname, '..', dir);
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
filename: 'js/[name].js',
path: resolve(PATHS.assetsRoot)
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules :[
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env']
},
include:[resolve('src')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 20000,
name: assetsPath('images/[name].[hash:7].[ext]')
}
}
]
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin()
],
watchOptions: {
ignored: /node_modules/
}
};
assets/package.json:
{
"name": "assets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"devbuild": "node ./node_modules/webpack/bin/webpack.js --config ./build/webpack.dev.conf.js",
"watch": "./node_modules/.bin/webpack --watch-stdin --config ./build/webpack.dev.conf.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"css-loader": "^0.28.7",
"file-loader": "^1.1.5",
"friendly-errors-webpack-plugin": "^1.6.1",
"url-loader": "^0.6.2",
"vue-loader": "^13.0.5",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "^2.4.4",
"webpack": "^3.6.0"
},
"dependencies": {
"vue": "^2.4.4"
}
}
Check this project with:
assets $ npm i
...
added 623 packages in 10.714s
assets $ npm run watch
> assets@1.0.0 watch /Users/wheatley/sbox/vue/phx/assets
> webpack --watch-stdin --config ./build/webpack.dev.conf.js
Webpack is watching the files…
...
^C assets $
At this point it should be possible to view assets/index.html through a browser from the filesystem.
Now create a new Phoenix 1.3 project:
assets $ cd ..
$ mix phx.new my_app --no-brunch --no-ecto
and create a new directory my_app/assets and copy the following files into it:
my_app/assets
├── build
│ └── webpack.dev.conf.js
├── package.json
└── src
├── App.vue
├── assets
│ └── phoenix.png
└── main.js
Modify my_app/assets/build/webpack.dev.conf.js
// build/web.config.dev.js
const path = require('path');
const webpack = require('webpack');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const PATHS = {
assetsRoot: '../priv/static', // CHANGED
assetsSubDirectory: '/'
};
function assetsPath(filePath) {
return path.posix.join(PATHS.assetsSubDirectory, filePath);
}
function resolve (dir) {
return path.join(__dirname, '..', dir);
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
filename: 'js/[name].js',
path: resolve(PATHS.assetsRoot)
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'phoenix': resolve('../deps/phoenix/priv/static/phoenix.js'), // ADDED
'phoenix_html': resolve('../deps/phoenix_html/priv/static/phoenix_html.js'), // ADDED
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules :[
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env']
},
include:[resolve('src')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 20000,
name: assetsPath('images/[name].[hash:7].[ext]')
}
}
]
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin()
],
watchOptions: {
ignored: /node_modules/
}
};
Note the modified PATHS.assetsRoot and the additions to resolve.alias.
Modify my_app/assets/src/main.js:
// src/main.js
import Vue from 'vue';
import App from './App';
import 'phoenix_html'; // ADDED
// import socket from "./socket";
Vue.config.productionTip = false;
new Vue({
el: '#app',
template: '<App passedProp="Greetings!" />',
components: { App }
});
Install the supporting packages:
assets $ npm i
...
added 623 packages in 11.886s
Give it a quick check:
$ npm run watch
> assets@1.0.0 watch /Users/wheatley/sbox/vue/phx/my_app/assets
> webpack --watch-stdin --config ./build/webpack.dev.conf.js
Webpack is watching the files…
...
^C assets $
Now modify watchers in my_app/config/dev.exs:
config :my_app, MyAppWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [node: ["node_modules/webpack/bin/webpack.js", # CHANGED
"--watch-stdin",
"--config", "./build/webpack.dev.conf.js",
cd: Path.expand("../assets", __DIR__)]
]
to kick off Webpack watching the frontend source files.
The same thing can be accomplished by specifying a suitable npm run script, however this rather verbose incantantion is necessary for some MS Windows installations.
Replace my_app/lib/my_app_web/templates/layout/app.html.eex with
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Hello MyApp!</title>
</head>
<body>
<%= render @view_module, @view_template, assigns %>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
and my_app/lib/my_app_web/templates/page/index.html.eex with
<div id="app"></div>
Finally start Phoenix
assets $ cd ..
my_app $ mix phx.server
Compiling 12 files (.ex)
Generated my_app app
[info] Running MyAppWeb.Endpoint with Cowboy using http://0.0.0.0:4000
Webpack is watching the files…
DONE Compiled successfully in 924ms20:40:49
...
[14] ../deps/phoenix_html/priv/static/phoenix_html.js 1.23 kB {0} [built]
+ 6 hidden modules
Now open the browser at http://localhost:4000/ to see the “Hello MyApp!” page.
If you modify the message text in my_app/assets/src/App.vue Phoenix LiveReloader will update the page in the browser shortly after you save the file (and the bundle has been rebuilt).
With a basic setup like this it should be possible to go further by “scavenging” features from other non-Phoenix Vue “donor” projects like the ones generated by vue-cli, e.g. $ vue init webpack my-project.
bkolobara
I have converged to a similar setup in most of my web projects. If you can avoid server-side rendering having a SPA and treating the backend as an api makes things much simpler. Even better with GraphQL.
I like to keep my frontend and backend in different folders. Usually the fronted is a pure npm project and the backend is a mix project. This has multiple benefits:
- Idiomatic Webpack, TypeScript and ReactRouter setup with hot reloading on the frontend. No need to worry about how to fit it into Elixir’s folder structure or introducing additional boilerplate into Elixir to make the hot reloading work.
- The Webpack Dev Server proxies all /api call to the Elixir application running on another port.
- In production all the static files are hosted on a CDN (404s are redirected to index.html and /api calls are proxied to the production Elixir instance).
- If something doesn’t work googling becomes much easier. With already complex toolchains like JSX + TypeScript I prefer to stick to the simplest structure and avoid pushing it into another folder structure, in this case Elixir’s.








