How to do real Server Side Rendering with Vue 2

Coding Friend, LLC
6 min readJan 11, 2017

--

I have an semi-finished boilerplate using the techniques taught in this article you are welcome to use. https://github.com/codingfriend1/Feathers-Vue

Server side rendering in Vue is a little unclear for larger projects. Most of the examples I’ve seen require heavy use of webpack. I’m going to show a more Node driven approach.

When the server renders the page it will just be a plain string without any vue functionality. That means if you try to navigate pages it will do a full page reload. So in order to get the benefits of both server side rendering and client side vue functionality like ajax routing you need to reboot the app client side after it’s rendered from the server. Just make sure your root vue component has the same root html as the index.html page like

<div id="app"></div>

Both the client rendered version and server rendered version must have exactly the same html or it will throw a hydration error (server and client html out of sync). The thing you have to be careful of is not all libraries will work server-side yet. For instance vue-material has direct references to the document or window that are not accessible server side and those components will not render on the server.

Server side rendering can happen in 5 files.

The index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
<title></title>
<script> window.__INITIAL_STATE__= "init_state" </script>
<link rel="stylesheet" href="/public/app/style.css">
</head>
<body>
<div id="app"></div>
<script src="/public/app/app.js"></script>
</body>
</html>

The unique parts to note here is we have an

<div id="app"></div>

We will boot our root instance from this element both server side and client side. The other unique thing to note is

<script> window.__INITIAL_STATE__= "init_state" </script>

The server side rendering will likely make changes to our centeralized store (I’m using Vue-Stash). We will save the updated state of the store to this __INITIAL_STATE__ variable so the client can load from it if it’s set.

One other note of interest is I have webpack separate the style.css and app.js. Since the html is already loaded from the server if the styles aren’t loaded till the bottom of the page it will flicker and temporarily show an un-styled page.

Express routes file

  • Use webpack to compile a renderer file. (This will import your main .vue component, router, and store. It needs webpack to handle the .vue files.) It any of your links import stylesheets they will be ignored, but you still need the appropriate webpack loader for each new file type. You can’t use any imports that have direct access to the window or document properties.
  • Import vue-server-renderer and use `createBundleRenderer()` passing in your compiled renderer file and caching options
  • Run renderToString({ url: req.url }) from the bundleRenderer created passing in an object with a url. It needs to be an object so the renderer can attach the store changes to it.
  • Read the index.html page with fs and .replace() the #app element with the rendered html.
  • Replace the window.__INITIAL_STATE__= “init_state” with our store state.
const isProd = process.env.NODE_ENV === 'production'
const path = require('path');
const fs = require('fs');
const express = require('express');
const app = express();
const serverRenderer = require('vue-server-renderer')
//load the index.html
const indexHTML = fs.readFileSync(path.join('public', 'index.html'), 'utf8')
//if we are in production mode set a 15 minute cache
const options = isProd? {
cache: require('lru-cache')({
max: 1000,
maxAge: 1000 * 60 * 15
})
}: undefined

app.get('/*', (req, res) => {
//Normally we would read this file outside of the route but if we are in development we want to dynamically load changes
const filePath = path.join( 'lib', 'renderer.js' );
const code = fs.readFileSync(filePath, 'utf8');
const renderer = serverRenderer.createBundleRenderer(code, options);
//we pass in req.url for vue-router. The renderer will attach the updated store state to this object
var context = { url: req.url }
//Render the html string
renderer.renderToString(context, (err, html) => {
if (err) {
console.log('Error rendering to string: ');
console.log(err);
console.log(err.message);
return res.status(200).send('Server Error');
}
html = indexHTML.replace('<div id="app"></div>', html); //we will set context.initialState with the renderer
const newStoreState = JSON.stringify(context.initialState)
//I have a script in my index page called
//<script> window.__INITIAL_STATE__= "init_state" </script>
//So I replace init_state with our updated store
html = html.replace('"init_state"', newStoreState);
return res.status(200).send(html);
})
})
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
})

The Renderer File

The renderer file must export a function that returns the root vue component. It can do this as a promise. However if you are using Vue-Router it’s likely you won’t want to render just the root component but any sub-views for the provided route.

So if someone visits /about you want to load the component associated within the /about route. You do this with

router.push('/about')

But we can use the req.url we passed in with the context.

Wait for beforeCreate and created lifecycle hook promises to resolve

If we have some ajax requests that we want to run before we load the page we can put those in the beforeCreate and created lifecycle hooks in our Vue component and wait for those to finish rendering before resolving the main app component. Make sure you are using an isomorphic (works on both server and client) ajax library like isomorphic fetch or axios. I recommend axios because it has a unit test mocker library.

//app is our root vue component
//router is the instance of VueRouter
import { app, router, store } from '../app/boot'export default function(context) { //Load the correct router view server side
router.push(context.url)
// Get the components belonging to that view
let matchedComponents = routing.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
return Promise.reject({ code: '404' })
}
// We wait for the "beforeCreate" and "created" hooks to finish their promises before rendering. You can run an isomorphic ajax library such as axios or isomorphic-fetch in it. It should be a function that returns a promise and when it resolves it will render the html. This allows you to fetch all your ajax data before the html is sent. The store is attached to this and passed in as the first parameter as wellreturn Promise.all(matchedComponents.map(async component => {
if (component.beforeCreate) {
try {
//I'm passing in the store as the this context
await component.beforeCreate.apply(store, store)
} catch(err) {}
}
if(component.created) {
try {
//I'm passing in the store as the this context
await component.created.apply(store, store)
} catch(err) {}
}
})).then(() => {
context.initialState = store
return app
})
};

Store Reconciliation

The server may have made changes to the store before rendering the initial html. We will write those changes to window.__INITIAL_STATE__. We need to set out local store to be the same as the server so vue does not throw an hydration error.

// The server may have made changes to the store before rendering the initial html. It writes those changes to window.__INITIAL_STATE__. We need to set out local store to be the same as the server so vue does not throw an hydration error (server and client html out of sync)
import Vue from 'vue'
import VueStash from 'vue-stash'
//defaultStore is a plain object imported from another file
import defaultStore from '../store'
Vue.use(VueStash)let store = defaultStoretry {
if(window && window.__INITIAL_STATE__ && window.__INITIAL_STATE__ !== "init_state") {
store = window.__INITIAL_STATE__
}
} catch(err) {}
export default store

Webpack

Our webpack file should compile the renderer using the .vue loader.

{
target: 'node',
entry: path.resolve(__dirname, 'src', 'renderer.js'),
output: {
libraryTarget: 'commonjs2',
path: path.resolve(__dirname, 'lib'),
filename: 'renderer.js'
},
loaders : [
{
test: /\.vue$/,
loader: 'vue',
exclude: /node_modules/,
}
]
}

Reboot Client Side

We will have some specific libraries that we only want to load client side and not server side such as authentication, toastr notifications, styles and css, jQuery, etc… We will have a separate file that will be a script in the index.html page that when loaded will reboot the app from the

<div id="app"></div>

dom using the same root component. Your root Vue component should have #app at the top of it’s template.

<div id="app">
<navigation></navigation>
<heading></heading>
<div class="container">
<router-view></router-view>
</div>
<foot></foot>
</div>

This might take a while to wrap your head around but I will be releasing a boilerplate in the future. Please ask questions so I can make this tutorial more clear.

--

--

Responses (5)