Universal Vue with Nuxt

Delivering on the promise of isomorphic JavaScript.

brd.bz/nuxt

@jpschroeder

justin@wearebraid.com

@wearebraid

justin@wearebraid.com

@devoidofgenius

cellinger@wearebraid.com

Concepts

  • Isomorphic JavaScript
  • Server Side Vue
  • Nuxt

Isomorphic JavaScript

Isomorphic JavaScript is JavaScript that is environment agnostic or shimmed per environment.

 

- Sprike Brehm (2014)
 

 Why Isomorphic JavaScript?

  • Efficiency: Write it once, use everywhere.
  • SEO: Google/Bing have unreliable async crawling.
  • Opengraph: Serve page specific meta tags.
  • Speed: Time to content can be significantly reduced.
  • No JS Support: Cause it still feels right?
  • Maintenance: Reduce duplication.

Universal

Universal Javascript = Universal Developers

Developer

The

...it’s too hard 😥

Universal apps introduce a huge amount of complexity. They require you to spend more of your time on maintenance, and less on features. And this isn’t to mention the limitation of requiring your app to be served by Node.js.

 

- Disgruntled Developer
 

(James Nelson)

The problem

JavaScript isn't universal.

// Window Object and methods
window.alert('part of window')

// DOM objects and methods
document.createElement('a')
el.addEventListener(...)

// Web APIs like fetch
fetch('http://google.com')

// Device APIs
navigator.geolocation

// Service Workers
navigator.serviceWorker

// Storage APIs
localStorage.setItem('x', 'y')
indexDB.open('mydb', 2)

// Push notifications
new Notification("Hi there!")
// File System
fs.open('file.txt')

// Events
e = new EventEmitter()
e.emit('changed')

// HTTP
http.createServer((req, res) => {
  res.send('hello')
}).listen(80)

// OS data
os.cpus()

// Paths
path.basename('/var/www/index.html')

// Process
process.pwd()

// stdin/stdout
readline.question('got node?')

Universal Vanilla

Step 1: Render

const http = require('http')
const page = require('./universal')
const fs = require('fs');

let server = http.createServer((req, res) => {
  switch (req.url) {
    case "/universal.js":
      res.writeHead(200, {'Content-Type': 'application/json'})
      res.end(fs.readFileSync('universal.js', 'utf8'))
      break
    default:
      res.writeHead(200, {'Content-Type': 'text/html'})
      res.end(page.render(true))
  }
})

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(3000)

server.js

Step 2: Hydrate

module.exports = {
  markup: `
  <!DOCTYPE html>
  <html>
    <head>
      <title>Universal JS</title>
      <script>var module = {}</script>
      <script src="universal.js"></script>
      </head>
    <body>
      <div>Current time is: <span id="time">{{ time }}</span></div>
      <small id="copyright">{{ copyright }}</small>
      <script>setInterval(() => module.exports.render(), 100)</script>
    </body>
  </html>
  `,
  render: function (ssr) {
    let content = this.content()
    let markup = this.markup
    for (let field in content) {
      if (ssr) {
        markup = markup.replace(`{{ ${field} }}`, content[field])
      } else {
        document.getElementById(field).innerHTML = content[field]
      }
    }
    return ssr ? markup : true
  },
  content: function () {
    let date = new Date()
    return {
      time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`,
      copyright: `© ${date.getFullYear()} <a href="https://wearebraid.com">Braid LLC.</a> Absolutely No Rights Reserved.`
    }
  }
}

universal.js

Server Side Vue

Step 1: Render

npm install vue-server-renderer
const agent = require('vue-server-renderer')
const renderer = agent.createRenderer()
const Vue = require('vue')
const app = require('./app')

renderer.renderToString(app)
  .then(html => console.log(html))
  .catch(err => console.error(err))
module.exports = new Vue({
  template: `<div>Hello World</div>`
})

app.js

render.js

Step 2: Hydrate

<!DOCTYPE html>
<html>
  ...
  <body>
    <div id="app"></div>
    <script>
      require('./app').$mount('#app')
    </script>
  </body>
</html>

Caveats

  • Only 2 lifecyle hooks (beforeCreate, created).
  • Node is a long-running process (watch out!).
  • Memory allocation (no teardown).
  • Vue code must be universal.

Working Example

Step 1: Render

const http = require('http')
const fs = require('fs')
const renderer = require('vue-server-renderer').createRenderer()
const template = fs.readFileSync('./index.html', 'utf-8')
const app = require('./universal.js')

const server = http.createServer((req, res) => {
  switch (req.url) {
    case "/universal.js":
      res.writeHead(200, {'Content-Type': 'application/json'})
      res.end(fs.readFileSync('./universal.js', 'utf-8'))
      break;
    default:
      renderer.renderToString(app())
        .then(html => res.end(template.replace('{{ app }}', html)))
        .catch(err => {throw err})
  }
})

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(3000)

server.js

Step 1: Render

const Vue = require('vue')

module.exports = function app () {
  return new Vue({
    template: `
    <div>
      <div>
        Current time is:
        <span id="time">
            {{ hours }}:{{ minutes }}:{{ seconds }}.{{ milliseconds }}
        </span>
      </div>
      <small>
        <span v-html="copy"></span>
        {{ year }}
        <a href="https://www.wearebraid.com">Braid LLC.</a>
        Absolutely No Rights Reserved.
      </small>
    </div>`,
    data () {
      return {
        date: new Date(),
        copy: '©'
    ... etc ...

app.js

Gross. ES5? No .vue files?

Step 2: Hydrate

<!DOCTYPE html>
<html>
  <head>
    <title>02-Example: Vue</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
    <script>
      var require = function (){ return window.Vue };
      var module = {}
    </script>
  </head>
  <body>
    <div id="app">{{ app }}</div>
    <script src="/universal.js"></script>
    <script>module.exports().$mount('#app')</script>
  </body>
</html>

Look at this absolute trash...

index.html

Build Process

index.html

package.json

server.js

universal.js

components

      Timer.vue

App.vue

entry-client.js

entry-server.js

index.html

package.json

server.js

universal.js

webpack.base.config.js

webpack.client.config.js

webpack.server.config.js

+

brd.bz/nuxt

03-vue-webpack

Challenges

  • Routing
  • Static assets
  • Code splitting
  • Vuex store
  • Meta tags

What is Nuxt?

  • Universal Vue framework
  • Static site generator
  • Opinionated Vue scaffold

“Nuxt.js is a framework for creating Universal Vue.js Applications.”

nuxtjs.org

Justin 

Installation

npm install nuxt

From starter

From scratch

vue init nuxt-community/starter-template project
{
  "scripts": {
    "dev": "nuxt"
  }
}

package.json

assets

components

layouts

middleware

nuxt.config.js

package.json

pages

plugins

static

store

What’s in the box

  • HTTP server
  • Server side rendering
  • Automatic code splitting
  • .vue, sass/less, es6/es7
  • Hot Module Reloading
  • Head management

Maintainers are French, so get used to this.

Pages

Pages are just supercharged Vue components. 

assets

components

layouts

middleware

nuxt.config.js

package.json

pages

plugins

static

store

  • Page files are routes
  • Extra methods
  • asyncData()
  • fetch()
  • head()
  • layout()
  • middleware()
  • scrollToTop()
  • transition()
  • validate()

Routing

pages

index.vue

about.vue

people

index.vue

_person.vue

routes: [
  {
    path: "/",
    component: "index.vue",
    name: "index"
  },
  {
    path: "/about",
    component: "about.vue",
    name: "about"
  }
]
routes: [
  {
    path: "/",
    component: "index.vue",
    name: "index"
  },
  {
    path: "/about",
    component: "about.vue",
    name: "about"
  },
  {
    path: "/people",
    component: "people/index.vue",
    name: "people"
  }
]
routes: [
  {
    path: "/",
    component: "index.vue",
    name: "index"
  },
  {
    path: "/about",
    component: "about.vue",
    name: "about"
  },
  {
    path: "/people",
    component: "people/index.vue",
    name: "people"
  },
  {
    path: "/people/:person",
    component: "people/_person.vue",
    name: "people-person"
  }
]

<nuxt-link to="/about">

Context Object

{
  app,
  isClient,
  isServer,
  isStatic,
  isDev,
  isHMR,
  route,
  store,
  env,
  params,
  query,
  req,
  res,
  redirect,
  error,
  nuxtState,
  beforeNuxtRender
}
import axios from 'axios'

export default {
  fetch ({ store, params }) {
    return axios.get(`http://your-api/${params.person}`)
    .then(res => store.commit('people/setPerson', res.data))
  }
}

Honorable Mentions

  • Nuxt Child
  • Nuxt Generate
  • Nuxt Render
  • serverMiddleware

Example Time!

Universal Vue with Nuxt

By Justin Schroeder

Universal Vue with Nuxt

  • 432
Loading comments...

More from Justin Schroeder