Tutorial

Basic Server Side Rendering with Vue.js and Express

Published on March 14, 2017
    author

    Joshua Bemenderfer

    Basic Server Side Rendering with Vue.js and Express

    Server side rendering (SSR) is one of those things that’s long been touted as one of the greatest strengths of React, Angular 2+, and Vue 2. It allows you to render your apps on the server, then hydrate them with client side reactivity after the page loads, greatly increasing the responsiveness and improving the load time of your pages.

    Unfortunately, it’s not the most obvious thing to set up, and the documentation for rendering Vue.js apps on the server is spread across several places. Hopefully this guide should help clear things up for you. :)

    Installation

    We’ll start with vue-cli’s webpack-simple template to give us a common base to work with.

    # Create the project
    $ vue init webpack-simple vue-ssr-example
    $ cd vue-ssr-example
    
    # Install dependencies
    $ yarn # (or npm install)
    

    We’ll also need three other packages, express for the server, vue-server-renderer to render the bundle, which is produced by vue-ssr-webpack-plugin.

    # Install with yarn ...
    $ yarn add express vue-server-renderer
    $ yarn add vue-ssr-webpack-plugin -D # Add this as a development dependency as we don't need it in production.
    
    # ... or with NPM
    $ npm install express vue-server-renderer
    $ npm install vue-ssr-webpack-plugin -D
    

    Preparing the App

    The webpack-simple template doesn’t come with SSR capability right out of the box. There are a few things we’ll have to configure first.

    The first thing to do is create a separate entry file for the server. Right now the client entry is in main.js. Let’s copy that and create main.server.js from it. The modifications are fairly simple. We just need to remove the el reference and return the app in the default export.

    src/main.server.js
    import Vue from 'vue';
    import App from './App.vue';
    
    // Receives the context of the render call, returning a Promise resolution to the root Vue instance.
    export default context => {
      return Promise.resolve(
        new Vue({
          render: h => h(App)
        })
      );
    }
    

    We also need to modify index.html a bit to prepare it for SSR.

    Replace <div id=“app”></div> with , like so:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>vue-ssr-example</title>
      </head>
      <body>
        <!--vue-ssr-outlet-->
        <script src="/dist/build.js"></script>
      </body>
    </html>
    

    Webpack Configuration

    Now, we need a separate webpack configuration file to render the server bundle. Copy webpack.config.js into a new file, webpack.server.config.js.

    There are a few changes we’ll need to make:

    webpack.server.config.js
    const path = require('path')
    const webpack = require('webpack')
    // Load the Vue SSR plugin. Don't forget this. :P
    const VueSSRPlugin = require('vue-ssr-webpack-plugin')
    
    module.exports = {
      // The target should be set to "node" to avoid packaging built-ins.
      target: 'node',
      // The entry should be our server entry file, not the default one.
      entry: './src/main.server.js',
      output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'build.js',
        // Outputs node-compatible modules instead of browser-compatible.
        libraryTarget: 'commonjs2'
      },
      module: {
        rules: [
          {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: {
              loaders: {
              }
              // other vue-loader options go here
            }
          },
          {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
          },
          {
            test: /\.(png|jpg|gif|svg)$/,
            loader: 'file-loader',
            options: {
              name: '[name].[ext]?[hash]'
            }
          }
        ]
      },
      resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
      },
      // We can remove the devServer block.
      performance: {
        hints: false
      },
      // Avoids bundling external dependencies, so node can load them directly from node_modules/
      externals: Object.keys(require('./package.json').dependencies),
      devtool: 'source-map',
      // No need to put these behind a production env variable.
      plugins: [
        // Add the SSR plugin here.
        new VueSSRPlugin(),
        new webpack.DefinePlugin({
          'process.env': {
            NODE_ENV: '"production"'
          }
        }),
        new webpack.optimize.UglifyJsPlugin({
          sourceMap: true,
          compress: {
            warnings: false
          }
        }),
        new webpack.LoaderOptionsPlugin({
          minimize: true
        })
      ]
    }
    

    Build Config

    To simplify development, let’s update the build scripts in package.json to build both the client and server webpack bundles.

    Replace the single build script with these three. Usage stays the same, but you can now build the client or server bundles individually with build:client and build:server, respectively.

    package.json (partial)
    {
      ...
      "scripts": {
        ...
        "build": "npm run build:server && npm run build:client",
        "build:client": "cross-env NODE_ENV=production webpack --progress --hide-modules",
        "build:server": "cross-env NODE_ENV=production webpack --config webpack.server.config.js --progress --hide-modules"
      },
      ...
    }
    

    Server Script

    Now, we need the server script to, well, render the application.

    server.js (partial)
    #!/usr/bin/env node
    
    const fs = require('fs');
    const express = require('express');
    const { createBundleRenderer } = require('vue-server-renderer');
    
    const bundleRenderer = createBundleRenderer(
      // Load the SSR bundle with require.
      require('./dist/vue-ssr-bundle.json'),
      {
        // Yes, I know, readFileSync is bad practice. It's just shorter to read here.
        template: fs.readFileSync('./index.html', 'utf-8')
      }
    );
    
    // Create the express app.
    const app = express();
    
    // Serve static assets from ./dist on the /dist route.
    app.use('/dist', express.static('dist'));
    
    // Render all other routes with the bundleRenderer.
    app.get('*', (req, res) => {
      bundleRenderer
        // Renders directly to the response stream.
        // The argument is passed as "context" to main.server.js in the SSR bundle.
        .renderToStream({url: req.path})
        .pipe(res);
    });
    
    // Bind the app to this port.
    app.listen(8080);
    

    Running the App

    If all goes well, you should be able build the bundle and run the server with:

    # Build client and server bundles.
    $ npm run build
    # Run the HTTP server.
    $ node ./server.js
    

    If you visit http://localhost:8080, everything should look… the same. However, if you disable JavaScript, everything will still look the same, because the app is being rendered on the server first.


    Caveats

    • Any modules that are loaded from node_modules instead of the bundle cannot be able to be changed across requests, (ie, have a global state.) Otherwise you will get inconsistent results when rendering your application.
    • Make sure you write your tables properly (include the thead and/or tbody wrapper elements.) The client-side version can detect these issues, but the server-side version cannot, which can result in hydration inconsistencies.

    Bonus Round

    • Try making the app display something different depending on whether it was rendered on the client or the server. Hint: You can pass props to App from the root render function.
    • Sync Vuex state from the server to the client. It might involve some global variables!

    Reference

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Joshua Bemenderfer

    author

    While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    Leave a comment
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.