Vue.js server side rendering with ASP.NET Core

I really like working with Vue.js on the frontend, altough server side rendering have not been supported until the second version of it. In this sample I would like to show how to use Vue.js 2’s server-side rendering capabilities with ASP.NET Core. On the server side we use the Microsoft.AspNetCore.SpaServices package which offers ASP.NET Core APIs to invoke Node.js-hosted Javascript code with context information and inject the resulting HTML string into the rendered page.

In this example the application will show a list of messages where the last two messages (ordered by date) will be rendered by the server. Tthe remaining messages can be downloaded from the server by clicking on the “Fetch a message” button.

The structure of the project looks like this:

.
├── VuejsSSRSample
|   ├── Properties
|   ├── References
|   ├── wwwroot
|   └── Dependencies
├── Controllers
|   └── HomeController.cs
├── Models
|   ├── ClientState.cs
|   ├── FakeMessageStore.cs
|   └── Message.cs
├── Views
|   ├── Home
|   |   └── Index.cshtml
|   └── _ViewImports.cshtml
├── VueApp
|   ├── components
|   |   ├── App.vue
|   |   └── Message.vue
|   ├── vuex
|   |   ├── actions.js
|   |   └── store.js
|   ├── app.js
|   ├── client.js
|   ├── renderOnServer.js
|   └── server.js
├── .babelrc
├── appsettings.json
├── Dockerfile
├── packages.json
├── Program.cs
├── project.json
├── Startup.cs
├── web.config
├── webpack.client.config.js
└── webpack.server.config.js

As you can see the Vue app is located under the VueApp folder which has two components, a simple Vuex store with one mutation and one action, and also have some other files which we are talking about next: app.js, client.js, renderOnServer.js, server.js.

Implement Vue.js server side rendering

Image from https://www.npmjs.com/package/vue-server-renderer

To use server side rendering we have to create two different bundles from the Vue app: one for the server (which will run by Node.js) and one for the client which will run in the browser and hydrate the application on the client side.

app.js

Bootstrap the Vue instance in this module. It is used by both bundles.

import Vue from 'vue';
import App from './components/App.vue';
import store from './vuex/store.js';

const app = new Vue({
    store,
    ...App
});

export { app, store };

server.js

The entry point for the server bundle which exports a function with a context attribute which can be used to push any data from the render call.

import { app, store } from './app'

// push context related data (in our case the initial state of the application) with the context attribute
export default function(context) {
    return new Promise((resolve, reject) => {
        store.replaceState(context);
        resolve(app);
    });
};

client.js

The entry point for the client bundle which replaces the store’s current state with a global Javascript object called __INITIAL_STATE__ (this object will be created by the prerendering module) and it mounts the app to the specified element (.my-app).

import { app, store } from './app';

store.replaceState(__INITIAL_STATE__);

app.$mount('.my-app');

Webpack Configuration

In order to create the bundles we have to add two Webpack configuration files (one for the server, one for the client build) without forgetting to install Webpack if you haven’t installed it yet: npm install -g webpack.

webpack.server.config.js

const path = require('path');

module.exports = {
    target: 'node',
    entry: path.join(__dirname, 'VueApp/server.js'),
    output: {
        libraryTarget: 'commonjs2',
        path: path.join(__dirname, 'wwwroot/dist'),
        filename: 'bundle.server.js',
    },
    module: {
        loaders: [
          {
              test: /\.vue$/,
              loader: 'vue',
          },
          {
              test: /\.js$/,
              loader: 'babel',
              include: __dirname,
              exclude: /node_modules/
          },
          {
              test: /\.json?$/,
              loader: 'json'
          }
        ]
    },
};
___

webpack.client.config.js

const path = require('path');

module.exports = {
    entry: path.join(__dirname, 'VueApp/client.js'),
    output: {
        path: path.join(__dirname, 'wwwroot/dist'),
        filename: 'bundle.client.js',
    },
    module: {
        loaders: [
          {
              test: /\.vue$/,
              loader: 'vue',
          },
          {
              test: /\.js$/,
              loader: 'babel',
              include: __dirname,
              exclude: /node_modules/
          },
        ]
    },
};
___

Run webpack --config webpack.server.config.js. If it is successfully finished the server bundle can be found at /wwwroot/dist/bundle.server.js. To get the client bundle run webpack --config webpack.client.config.js. The output can be found at /wwwroot/dist/bundle.client.js

Implement the Bundle Renderer

This module will be executed by ASP.NET Core and it is responsible for

  1. rendering the server bundle which we had created before
  2. set the window.__INITIAL_STATE__ to the object sent from the server side
process.env.VUE_ENV = 'server';

const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, '../wwwroot/dist/bundle.server.js')
const code = fs.readFileSync(filePath, 'utf8');

const bundleRenderer = require('vue-server-renderer').createBundleRenderer(code)

module.exports = function (params) {
    return new Promise(function (resolve, reject) {
        bundleRenderer.renderToString(params.data, (err, resultHtml) => { // params.data is the store's initial state. Sent by the asp-prerender-data attribute
            if (err) {
                reject(err.message);
            }
            resolve({
                html: resultHtml,
                globals: {
                    __INITIAL_STATE__: params.data // window.__INITIAL_STATE__ will be the initial state of the Vuex store
                }
            });
        });
    });
};

Implement the ASP.NET Core part

As I had mentioned before, we use the Microsoft.AspNetCore.SpaServices package which provides a few Tag Helpers to easily invoke Node.js-hosted Javascript (in the background SpaServices uses the Microsoft.AspNetCore.NodeServices package to execute Javascript).

Views/_ViewImports.cshtml

In order to use SpaServices’ TagHelpers we need to add them to the _ViewImports.

@addTagHelper "*, Microsoft.AspNetCore.SpaServices"

Home/Index

public IActionResult Index()
{
   var initialMessages = FakeMessageStore.FakeMessages.OrderByDescending(m => m.Date).Take(2);

   var initialValues = new ClientState() {
       Messages = initialMessages,
       LastFetchedMessageDate = initialMessages.Last().Date
   };

   return View(initialValues);
}

It does fetch the two most recent Messages (order by date desc) from the MessageStore (just some static data for demo purposes) and create a ClientState object which will be used as the initial state of the Vuex store.

The Vuex store default state

const store = new Vuex.Store({
    state: { messages: [], lastFetchedMessageDate: -1 },
    // ...
});

The ClientState class

public class ClientState
{
    [JsonProperty(PropertyName = "messages")]
    public IEnumerable<Message> Messages { get; set; }

    [JsonProperty(PropertyName = "lastFetchedMessageDate")]
    public DateTime LastFetchedMessageDate { get; set; }
}

The Index View

Finally we have the initial state (from the server) and the Vue app so there is only one step left: render the Vue app with the initial values in the view by using asp-prerender-module and asp-prerender-data Tag Helpers.

@model VuejsSSRSample.Models.ClientState
<!-- ... -->
<body>
    <div class="my-app" asp-prerender-module="VueApp/renderOnServer" asp-prerender-data="Model"></div>
    <script src="~/dist/bundle.client.js" asp-append-version="true"></script>
</body>
<!-- ... -->

The asp-prerender-module attribute is used to specify which module we would like to render (in our case VueApp/renderOnServer). With the asp-prerender-data attribute we can specify an object which will be serialized and sent to the module’s default function as a parameter.

You can download the source code from http://github.com/mgyongyosi/VuejsSSRSample

comments powered by Disqus