How To Get Phoenix & VueJS working Together?

I updated https://github.com/akeating/pespa to 1.3 a while back.

2 Likes

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.

8 Likes

Brunch and webpack both are horrid and I don’t use them in my work project, I just use raw npm scripts (which is really what phoenix should do by default as it shows how you could plug in anything then). ^.^

3 Likes

I think adopters simply need to be reminded that there is no Brunch “lock-in” with Phoenix - i.e. there is the freedom to use the frontend technologies that are the most appropriate for the job.

Brunch seems to be most appropriate for projects that primarily rely on server side EEx templates with some JavaScript widgets thrown in, using modules for code organization. Brunch is beginner-friendly for learning Phoenix with reference to the guides and the book - but IMO isn’t the right entry into the “modern JavaScript toolchain” - ultimately you have to start with npm/node. But JavaScript tooling is only a tangential concern to Phoenix’s role and existence and people need to stop assuming that every “out-of-the-box default” is automatically the best solution for their particular situation (while “Batteries included” products tend to be more popular, they can lead to “square peg in a round hole” solutions if defaults aren’t properly adapted).

Now Vue.js tooling strikes me as having a strong affinity/bias towards Webpack as a module bundler/build tool, so trying to use any other means (including Brunch) could possibly risk grief in the future. That being said Webpack is primarily optimized for “client-side generated DOM” based solutions, so the utility of server side EEx templates is somewhat limited; beyond --no-brunch --no-html may also be appropriate (possibly serving the frontend separately). However there could be use cases for Vue.js (with Webpack) + EEx templates as Vue.js promotes itself as “incrementally adoptable”.

1 Like

Wholly agree with @peerreynders last point, there is definitely a use case for Vue.js + Webpack + EEx templates… and that’s exactly what I am doing. As you said Vue.js is “incrementally adoptable” which works great as many pages just need a “sprinkle” of JS while other “pages” work better as a full SPA.

Webpack’s code splitting can easily build a larger bundle.js (including Vue itself and other required libraries used by most pages) which you can include in your layout and also generate small per-page javascript files for client-side code which differ for each page. You can define Vue instances targeting divs generated by EEx and pass initial data down in <script> tags or pull it using Phoenix JSON or the channels API.

When you do this you can also use Phoenix/Vue.js for other areas of your app as a full SPA.

2 Likes

Something like mbu would also be nice (perhaps integrated into mix itself) as it can handle non-elixir resources (with auto building via file watchers and all) quite easily, without any javascript build system needed (it’s built in elixir instead, even if you just have it delegate to webpack or so).

1 Like

Another option is using something like Nuxt.js. My latest designs ,make the assets/ folder a Nuxt app. I rip out Phoenix’s view layer entirely, and can use one of several options for serving app assets:

  • Use nuxt generate to compile app assets to static HTML/JS which is output to priv/static. I then use the static plug along with a bit of code to serve index.html in place of 404s. This makes the app release self-contained, but of course leaves the option of proxying something like /api to the app and hosting static assets in some other cache-friendly way.
  • Run the Nuxt server directly and use Phoenix for /api or whatever. This gives you SSR for free, along with a pretty nice and fast dev setup, much quicker than regenerating static assets on each change. Note that, for the latter, Phoenix can start the dev server directly but it isn’t cleaned up on exit. This PR fixes that, if anyone wants to poke it along that might be helpful.

I like this design because it starts simple and scales up. The easiest use case just involves serving everything out of a self-contained release. More complicated use cases spin up one or more Node servers in separate containers, then use SSR or other techniques if desired.

2 Likes

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.
7 Likes

It’s been some time since this was posted. Phoenix updated, VueJS updated. Who knows what has changed with Brunch & Wepack. (I’m behind the curve.)

With that mind, I’m about to start another VueJS + Phoenix project.

I’m going to give this solution a go…

It would require switching from Brunch to Webpack.

Am I creating any problems from myself moving from Brunch?

Is there any other simple solutions you would use instead for integrating VueJS?

1 Like

There are no inherent downsides to replacing Brunch since it’s really not very tightly integrated into Phoenix. No features of Phoenix rely on the assumption that you’re using Brunch.

However, if you don’t feel like setting up more complicated JS tooling, I think vue-brunch does a fine enough job of building Vue applications without any additional config.

2 Likes

I do it this way… Create an API only Phoenix application with mix phx.new my_app --no-html --no-brunch and use VueJS for all front-end UI. This is fine as long as your application is small - medium sized (since all front-end logic will ultimately be contained in a single JS file, you simply don’t want to end with a 4 MB JS file).

If its is a complex big project, then use normal Phoenix HTML pages with some VueJS integration on page level.

Note: I use webpack for the VueJS application.

2 Likes

I’m always going with laravel-mix, which is contrary to it’s name not dependent on laravel, but rather just a opinionated wrapper around webpack. It’s coming with support for vue built in.

4 Likes

Has anyone else had problems getting vue-brunch to build in Phoenix 1.3?

I was able to get it to work in 1.2, but after updating & building another project Brunch doesn’t seem to be concatenating components in /assets/components to /priv/static/css/components.css

1 Like

Could you share your config and your application structure? Are you getting any error messages?

1 Like

I didn’t change the file structure of the new project generator in Phoenix 1.3.

I added a folder for Vue components in assets & created a test component: assets/components/my-app.vue.

I setup brunch-config.js like this:

// Configure your plugins
  plugins: {
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/vendor/]
    },
    vue: {
      extractCSS: true,
      // out: '../public/styles/components.css'
      out: "/priv/static/css/components.css"
      
    }
  },

The JS from my-app.vue seems to be concatenating to app.js. The css doesn’t seem to be concatenating to components.css.

I posted a question to Stackoverflow.com with the specifics & the brunch debug output here:

1 Like

Hey there!

Has any of you succeeded in making Phoenix 1.3 work with Vue through Webpack?

I can’t seem to find any recent repo/tuto/blog that actually works for me. Emily, did you get there yet?

Any help is appreciated! =)
Cheers!

1 Like

I developed some apps with Phoenix 1.3 on backend and VueJS on frontend. Decoupled, as separate projects. Phoenix as an API only backend and all frontend written in VueJS. Worked fine.

As for Webpack, I guess it was something set up together automatically by vue-cli (the generator I used). I don’t recall that I needed to do anything with Webpack, used only defaults

2 Likes

Firstly, thank you for your quick reply!

The decoupled approach interests me a lot as well. It makes it possible to swap one side or another more naturally as needed and I think it’s more scalable as well.

One question that comes to mind is how do you serve the client-side? I guess you’re not using the Phoenix server to do it?

Also, (forseeing a follow-up question) are you using the same solution (guessing two separate servers) for production? I see that with npm run build, I can build it for use in production. Do I simply use a generic web server like Apache from there? I apologize for the non Elirix-related question. :slight_smile:

Can’t wait to know your take on this! =)

@jstlroot As for VueJS production, yes you can use any web server to serve the client side application. Under development and production you need to do this with your web server (I use Nginx):

Redirect all requests to the VueJS application except for all /api routes, they should be sent to the Phoenix back-end.

Yes, for development I use 3 servers actually:

Client >> 1. Nginx >>
2. VueJS or
3. Phoenix

Production:

Client >> 1. Nginx >>
Serving static files directly (HTML/JS)
or 2. passing request to Phoenix

Super cool! I’ll give that a spin!

Thank you :smiley: