vue 3 Server Side Rendering with Vuex and Router
I have managed to find the solution to this thanks to the following resources:
Server Side Rendering with Vue.js 3 video: https://www.youtube.com/watch?v=XJfaAkvLXyU&feature=youtu.be and git repos: https://github.com/moduslabs/vue3-example-ssr
SSR + Vuex + Router app : https://github.com/shenron/vue3-example-ssr
migrating from Vue 2 to Vue 3https://v3.vuejs.org/guide/migration/introduction.html
migrating from VueRouter 3 to VueRouter 4https://next.router.vuejs.org/guide/migration/
migrating from Vuex 3 to Vuex 4https://next.vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html
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')}