I work at Namecheap as a Senior Software Engineer. In our company, we often use Vue.js on the frontend with server-side rendering (SSR). Setting up SSR in the first place, however, isn’t always so easy. That’s why I decided to describe this process in simple steps to make it easier for understanding.
In this article we’ll cover how to set up production-ready SSR for Vue application using:
- Webpack 4
- Babel 7
- Node.js Express server
- Webpack-dev-middleware and webpack-hot-middleware for comfortable dev environment
- Vuex for state management
- vue-meta plugin for metadata management
Let me note that we won’t cover the basics of these technologies. We’ll concentrate on SSR only, and jump right into the action. I hope you find it helpful… Now let’s get into it!
Step 1. Configure Webpack
At this point, you probably already have a Vue app, and if you don’t, feel free to use my repo as a boilerplate.
First, let’s take a look at our folders and files structure:
As you can see, it’s pretty standard except for a couple of things that might catch your eye:
- there are two separate webpack configs for client and server builds:
- there are two respective entry files:
This is actually a key configuration point of our application. Here is a great diagram from the official documentation that provides the architecture overview we’re implementing:
Client config is the one that you’ve probably already dealt with. It’s basically for building the application into plain JS and CSS files.
Server config is an interesting one. We need it to generate a special JSON file — server bundle, that will be used on the server side for rendering the plain HTML of the Vue app. We use
vue-server-renderer/server-plugin for this purpose.
Another thing that is different from the client config is that we don’t need to process CSS files, so there are no loaders and plugins for it.
As you may have figured out, all common settings for client and server configs we put to the base config.
Step 2. Create Application Entries
Before we get into the client and server entries, let’s have a look at the
Note that instead of just creating an app instance, we export a factory function
createApp(). If our app were running in the browser env only, we wouldn’t have to worry about the users getting a fresh new Vue instance for each request. But since we’re creating the app in the node.js process, our code will be evaluated once and stay in the memory of the same context.
So if we use one Vue instance across multiple requests, it can lead to a situation when one user gets the app state of another’s. In order to avoid this scenario, we should create a new app instance for each request. Also, for the same reason, it’s not recommended that you use stateful singletons in the Vue app.
Every real-life app will have some metadata, like title or description, that should be different from page to page. You can achieve this with a
vue-meta plugin. Click here to understand why we’re using
In the client entry, we call
createApp(), passing the initial state injected by the server. After the router has completed the initial navigation, we mount the app to the DOM. Also in this file, you can import global styles and initialize directives or plugins that work with the DOM.
Server entry is pretty much described by the comments in the code. The one thing I’d add regarding the
router.onReady() callback is that if we use a
serverPrefetch() hook for data prefetching in some of our components, it waits until the promise returning from the hook is resolved. We’ll see an example of how to use it a bit later.
Now we can add scripts for building our app to the
Step 3. Run Express Server with Bundle Renderer
In order to render the app into plain HTML on the server side, we’ll use
vue-server-renderer module and the
./dist/vue-ssr-server-bundle.json file that we generated by running
build:server script. Let’s not think about development mode for now, we’ll discuss it in the next step.
First, we need to create a renderer by calling the
createBundleRenderer() method and passing two arguments: the bundle that we generated earlier and the next options:
Do you remember the problem with sharing the application state between multiple requests that we discussed in the previous step? This option aims to solve that. But creating a new V8 context and re-executing the bundle for each request is an expensive operation, so it’s recommended that you set this flag to
false due to possible performance issues. Also, beware of using stateful singletons in the app.
There is a special comment,
<! — vue-ssr-outlet — >, that will be replaced with HTML that’s generated by the renderer. And by the way, using the
template option, the renderer will automatically add a script with declaring
__INITIAL_STATE__ global variable that we use in
client-entry.js to create our app.
Now, when we have a renderer instance, we can generate HTML by calling the
renderToString() method, passing the initial state and current URL for the router.
Step 4. Set Up the Dev Environment
What do we need for a comfortable dev environment? I’d say the following:
- run only one node.js server without using an additional
vue-ssr-server-bundle.jsonfiles every time our source code is changed
- hot reloading
In order to accomplish all of these things, we can use the
setupDevServer() function in
server.js (see the previous step).
This function accepts two arguments:
app— the Express app;
onServerBundleReady()— callback that is called each time the source code is changed and new
vue-ssr-server-bundle.jsonis generated. It takes the bundle as an argument.
server.js we pass a callback
onServerBundleReady() as an arrow function that accepts a fresh bundle and re-creates the renderer.
Note that we require all dependencies inside of the
setupDevServer() function, we don’t need them to consume our process memory in production mode.
Now let’s add npm script for running the server in development mode using
“dev”: “cross-env NODE_ENV=development nodemon ./server.js”,
Step 5. Use ServerPrefetch()
Most likely you’ll need to get some data from the server when your app is initializing. You can do it by simply calling API endpoint once a root component is mounted. But in this case, your user will have to observe some spinner — not the best user experience. Instead, we can fetch the data during SSR using the
serverPrefetch() component hook that was added in 2.6.0 Vue version. Let’s add some endpoint to our server.
We’ll call this endpoint in
getUsers action. Now let’s take a look at an example of using the
serverPrefetch() hook in a component.
As you can see, we use
serverPrefetch() along with a
mounted() hook. We need it for cases when a user is sent to this page from another route on the client side, so the
users array is empty and we call the API.
Also, check out how we define the title and the description metadata for a particular page in the
metaInfo property provided by
Well, this is it. I think we covered all the main configuration points of setting up SSR for Vue.js and I hope these steps helped you to better understand this process.