How I made it easy to develop on Vue.js with server-side rendering
I’ll start with a short story.
In my new project I decided to use Vue.js. I needed server-side rendering (SSR), CSS modules, code-splitting and more cool things. Of course, I needed hot module replacing (HMR) to fast development.
I didn’t want to use high-level frameworks, like Nuxt.js, because the ability to customization very important when project will be huge. But any high-level frameworks don’t allow it (I have similar experience with Next.js for React).
Main problem of local development with usage server-side rendering and hot reloading is that you aren’t enough run only webpack-dev-server. You must watch and recompile sources, that Node.js executes, otherwise server code won’t be updated, but client code will be.
Okey, I dived into the documentation and internet and unfortunately, I didn’t find worked examples and boilerplates. So, I created own.
I determined things, that must be included in my boilerplate to convenient development:
- CSS modules
- ESLint, Prettier
In development mode, code must be updated in browser real-time, also server-side code must be updated too.
In production mode resources must be minified, names of resources must have hash to cache static, paths to resources must be injected to html-template automatically.
You can check the realisation in GitHub repo, I’ll show you code and explain my decisions.
Note, that Vue.js has very deep documentation about SSR setup. You should check it too.
So, for Node.js server we’ll use Express and we’ll need vue-server-renderer. This package allows us to render code to html-string, according to server bundle, html-template and client manifest, which contains names and paths to resources.
Result file server.js will be:
As you can see, we have 2 files: vue-ssr-server-bundle.json and vue-ssr-client-manifest.json. They are generating during build; the first file contains code, which will be executed on the server, the second contains names and paths to resources.
Also, options have a property inject: false. It means, that we’ll inject assets manually, because we want finer-grained control.
Template looks like:
- meta.inject().title.text() and meta.inject().meta.text() needed for generation title and meta-description. It does package vue-meta, about that I’ll tell you later
- renderResourceHints() — it renders links rel=”preload/prefetch” to assets from client manifest
- renderStyles() — it renders links to styles from client manifest
- renderState() — it renders state to default variable window.__INITIAL_STATE__
- renderScripts() — it renders scripts for our application
Comment <! — vue-ssr-outlet — >will be replaced with app html-code. It is required comment.
Entry point to our Vue app is file entry-server.js.
Client entry point is file entry-client.js.
Module app.js creates Vue instance, which using on client and server.
We always create a new instance to avoid situation then several requests use one instance
App.vue — root component which contains tag <router-view></router-view>, which will inject components according to route.
Router looks like:
With Vue.use we connect two plugins Router and VueMeta.
In routes we specify components as
() => import('./client/components/About.vue')
It needs for code-splitting.
What about state management (with Vuex), settings have nothing really different. One thing, I divided store into modules and I use constants with names to ease code navigation.
Now, consider the nuances of Vue components.
Property metaInfo is responsible of meta rendering, using vue-meta package. We can specify a lot of properties (more).
title: 'Main page',
Components have method, which executes only on server side.
console.log('Run only on server');
Also, I wanted to use CSS modules. I like the idea, that you don’t have to take care about class names to avoid intersection between components. If you use CSS modules, result class will look like <class name>_<hash>
To do it, you should specify style module in the component.
padding: 3px 0;
And in the template you should specify attribute :class.
Also, in webpack config you should specify, that we’ll use modules.
Consider the webpack configs.
We have the base config, which extended by server and clients configs.
Server config doesn’t have differences with documentation. Except for CSS processing.
At first, all about CSS were in base config, because it needs for client and server. Production minification CSS was in base config too.
But I faced a problem, document appeared on server side and I got an error, of course. It was a mini-css-extract-plugin problem and was resolved by separation CSS building config for client and server.
VueSSRServerPlugin generates file vue-ssr-server-bundle.json, which contains server execution code.
Now, consider client config.
For local development we specify publicPath, linked to webpack-dev-server and generate filename without hash. Also, for devServer we specify writeToDisk:true property.
An explanation is needed.
Default, webpack-dev-server serves assets from memory, instead writes it to disk. In this case we face a problem, client manifest (vue-ssr-client-manifest.json) which is on disk, will contain outdated assets, because it won’t be updated. To avoid it, dev-server must write changes to disk, then, client manifest will be updated and actual assets will be used.
Actually, I’d like to get rid of it. One of decisions — in development mode we can include manifest from dev-server url, instead of /dist folder. But it is async operation. I’ll be glad to receive clean solution of problem.
To reload server process we use Nodemon, which watches for two files: dist/vue-ssr-server-bundle.json and app/server.js and it restarts app, then these files were changed.
To have the ability to restart app for server.js changes, we don’t specify this file as the entry point into nodemon. Instead this, we created file nodemon.js which includes server.js. Now, the entry point is a file nodemon.js.
In production mode the entry point is app/server.js.
So, we have a repo with settings and several tasks.
For local development:
yarn run dev
Client side: it runs webpack-dev-server, which watches for changes of Vue components and js code and generates client manifest with assets paths to dev-server, saves it to disk and updates code, styles in browser in real time.
Server side: it runs webpack as a watcher, builds server bundle (vue-ssr-server-bundle.json) and restarts app, then it changed.
In this case, code will be updated consistently on the client and server automatically.
Note, If you run it the first time, you’ll get error, that server bundle wasn’t found.
It’s normal. Just re-run this task.
For production build:
yarn run build
Client side: it builds and minifies assets and adds hash to filenames and generates client manifest with relative paths to assets.
Server side: it builds server bundle.
Also, I created task yarn run start-node, which runs server.js, but it only for example. In production app you should use process manager, like PM2.
I hope that my experience helps you to setup ecosystem faster and concentrate on feature development.