vue 3 Server Side Rendering with Vuex and Router vue 3 Server Side Rendering with Vuex and Router vue.js vue.js

vue 3 Server Side Rendering with Vuex and Router


I have managed to find the solution to this thanks to the following resources:

client entry file (src/main.js)

import buildApp from './app';const { app, router, store } = buildApp();const storeInitialState = window.INITIAL_DATA;if (storeInitialState) {  store.replaceState(storeInitialState);}router.isReady()  .then(() => {    app.mount('#app', true);  });

server entry file (src/main-server.js)

import buildApp from './app';export default (url) => new Promise((resolve, reject) => {  const { router, app, store } = buildApp();  // set server-side router's location  router.push(url);  router.isReady()    .then(() => {      const matchedComponents = router.currentRoute.value.matched;      // no matched routes, reject with 404      if (!matchedComponents.length) {        return reject(new Error('404'));      }      // the Promise should resolve to the app instance so it can be rendered      return resolve({ app, router, store });    }).catch(() => reject);});

src/app.js

import { createSSRApp, createApp } from 'vue';import App from './App.vue';import router from './router';import store from './store';const isSSR = typeof window === 'undefined';export default function buildApp() {  const app = (isSSR ? createSSRApp(App) : createApp(App));  app.use(router);  app.use(store);  return { app, router, store };}

server.js

const serialize = require('serialize-javascript');const path = require('path');const express = require('express');const fs = require('fs');const { renderToString } = require('@vue/server-renderer');const manifest = require('./dist/server/ssr-manifest.json');// Create the express app.const server = express();// we do not know the name of app.js as when its built it has a hash name// the manifest file contains the mapping of "app.js" to the hash file which was created// therefore get the value from the manifest file thats located in the "dist" directory// and use it to get the Vue Appconst appPath = path.join(__dirname, './dist', 'server', manifest['app.js']);const createApp = require(appPath).default;const clientDistPath = './dist/client';server.use('/img', express.static(path.join(__dirname, clientDistPath, 'img')));server.use('/js', express.static(path.join(__dirname, clientDistPath, 'js')));server.use('/css', express.static(path.join(__dirname, clientDistPath, 'css')));server.use('/favicon.ico', express.static(path.join(__dirname, clientDistPath, 'favicon.ico')));// handle all routes in our applicationserver.get('*', async (req, res) => {  const { app, store } = await createApp(req);  let appContent = await renderToString(app);  const renderState = `    <script>      window.INITIAL_DATA = ${serialize(store.state)}    </script>`;  fs.readFile(path.join(__dirname, clientDistPath, 'index.html'), (err, html) => {    if (err) {      throw err;    }    appContent = `<div id="app">${appContent}</div>`;    html = html.toString().replace('<div id="app"></div>', `${renderState}${appContent}`);    res.setHeader('Content-Type', 'text/html');    res.send(html);  });});const port = process.env.PORT || 8080;server.listen(port, () => {  console.log(`You can navigate to http://localhost:${port}`);});

vue.config.js

used to specify the webpack build things

const ManifestPlugin = require('webpack-manifest-plugin');const nodeExternals = require('webpack-node-externals');module.exports = {  devServer: {    overlay: {      warnings: false,      errors: false,    },  },  chainWebpack: (webpackConfig) => {    webpackConfig.module.rule('vue').uses.delete('cache-loader');    webpackConfig.module.rule('js').uses.delete('cache-loader');    webpackConfig.module.rule('ts').uses.delete('cache-loader');    webpackConfig.module.rule('tsx').uses.delete('cache-loader');    if (!process.env.SSR) {      // This is required for repl.it to play nicely with the Dev Server      webpackConfig.devServer.disableHostCheck(true);      webpackConfig.entry('app').clear().add('./src/main.js');      return;    }    webpackConfig.entry('app').clear().add('./src/main-server.js');    webpackConfig.target('node');    webpackConfig.output.libraryTarget('commonjs2');    webpackConfig.plugin('manifest').use(new ManifestPlugin({ fileName: 'ssr-manifest.json' }));    webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }));    webpackConfig.optimization.splitChunks(false).minimize(false);    webpackConfig.plugins.delete('hmr');    webpackConfig.plugins.delete('preload');    webpackConfig.plugins.delete('prefetch');    webpackConfig.plugins.delete('progress');    webpackConfig.plugins.delete('friendly-errors');    // console.log(webpackConfig.toConfig())  },};

src/router/index.js

import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router';import Home from '../views/Home.vue';import About from '../views/About.vue';const isServer = typeof window === 'undefined';const history = isServer ? createMemoryHistory() : createWebHistory();const routes = [  {    path: '/',    name: 'Home',    component: Home,  },  {    path: '/about',    name: 'About',    component: About,  },];const router = createRouter({  history,  routes,});export default router;

src/store/index.js

import Vuex from 'vuex';import fetchAllBeers from '../data/data';export default Vuex.createStore({  state() {    return {      homePageData: [],    };  },  actions: {    fetchHomePageData({ commit }) {      return fetchAllBeers()        .then((data) => {          commit('setHomePageData', data.beers);        });    },  },  mutations: {    setHomePageData(state, data) {      state.homePageData = data;    },  },});

Github sample code

I found I needed to go through the building the code step by step doing just SSR, just Router, just Vuex and then put it all together.

My test apps are in github

https://github.com/se22as/vue-3-with-router-basic-sample

  • "master" branch : just a vue 3 app with a router
  • "added-ssr" branch : took the "master" branch and added ssr code
  • "add-just-vuex" branch : took the "master" branch and added vuex code
  • "added-vuex-to-ssr" branch : app with router, vuex and ssr.


You can also use Vite which has native SSR support and, unlike Webpack, works out-of-the-box without configuration.

And if you use vite-plugin-ssr then it's even easier.

The following highlights the main parts of vite-plugin-ssr's Vuex example

<template>  <h1>To-do List</h1>  <ul>    <li v-for="item in todoList" :key="item.id">{{item.text}}</li>  </ul></template><script>export default {  serverPrefetch() {    return this.$store.dispatch('fetchTodoList');  },  computed: {    todoList () {      return this.$store.state.todoList    }  },}</script>
import Vuex from 'vuex'export { createStore }function createStore() {  const store = Vuex.createStore({    state() {      return {        todoList: []      }    },    actions: {      fetchTodoList({ commit }) {        const todoList = [          {            id: 0,            text: 'Buy milk'          },          {            id: 1,            text: 'Buy chocolate'          }        ]        return commit('setTodoList', todoList)      }    },    mutations: {      setTodoList(state, todoList) {        state.todoList = todoList      }    }  })  return store}
import { createSSRApp, h } from 'vue'import { createStore } from './store'export { createApp }function createApp({ Page }) {  const app = createSSRApp({    render: () => h(Page)  })  const store = createStore()  app.use(store)  return { app, store }}
import { renderToString } from '@vue/server-renderer'import { html } from 'vite-plugin-ssr'import { createApp } from './app'export { render }export { addContextProps }export { setPageProps }async function render({ contextProps }) {  const { appHtml } = contextProps  return html`<!DOCTYPE html>    <html>      <body>        <div id="app">${html.dangerouslySetHtml(appHtml)}</div>      </body>    </html>`}async function addContextProps({ Page }) {  const { app, store } = createApp({ Page })  const appHtml = await renderToString(app)  const INITIAL_STATE = store.state  return {    INITIAL_STATE,    appHtml  }}function setPageProps({ contextProps }) {  const { INITIAL_STATE } = contextProps  return { INITIAL_STATE }}
import { getPage } from 'vite-plugin-ssr/client'import { createApp } from './app'hydrate()async function hydrate() {  const { Page, pageProps } = await getPage()  const { app, store } = createApp({ Page })  store.replaceState(pageProps.INITIAL_STATE)  app.mount('#app')}