Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

vue 3 Server Side Rendering with Vuex and Router

I have created a Vue3 application using the Vue CLI to create my application with Vuex and Router. The application runs well.

Note: I followed this useful doc for the Vuex with Vue3 https://blog.logrocket.com/using-vuex-4-with-vue-3/

Requirement Now I would like to change my Vue3 application to have Server Side Rendering support(i.e. SSR).

I watched this awesome video on creating an SSR application using Vue3 : https://www.youtube.com/watch?v=XJfaAkvLXyU and I can create and run a simple application like in the video. However I am stuck when trying to apply it to my main Vue3 app.

My current sticking point is how to specify the router and vuex on the server code.

My Code

The client entry file (src/main.js) has the following

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

createApp(App).use(store).use(router).mount('#app');

The server entry file (src/main.server.js) currently has the following

import App from './App.vue';
export default App;

And in the express server file (src/server.js) it currently has

const path = require('path');
const express = require('express');
const { createSSRApp } = require('vue');
const { renderToString } = require('@vue/server-renderer');

...
...

server.get('*', async (req, res) => {
  const app = createSSRApp(App);
  const appContent = await renderToString(app);

I need to change this code so that the app on the server side is using the router and vuex like it is on the client.

Issues

In the express server file i can not import the router and vuex like in the client entry file as it fails due to importing outside a module, therefore in the express server I can not do the following

const app = createSSRApp(App).use(store).use(router);

I have tried changing the server entry file (src/main.server.js) to the following, but this does not work either.

import App from './App.vue';
import router from './router';
import store from './store';

const { createSSRApp } = require('vue');

export default createSSRApp(App).use(store).use(router);

Does anyone know how to do SSR in Vue 3 when your app is using Vuex and Router.

How i did this in Vue 2 is below and what i am trying to change over to Vue 3

My Vue2 version of this application had the following code

src/app.js creates the Vue component with the router and store specified

Client entry file (src/client/main.js) gets the app from app.js, prepopulates the Vuex store with the data serialized out in the html, mounts the app when the router is ready

import Vue from 'vue';
import { sync } from 'vuex-router-sync';
import App from './pages/App.vue';
import createStore from './vuex/store';
import createRouter from './pages/router';

export default function createApp() {
  const store = createStore();
  const router = createRouter();
  sync(store, router);

  const app = new Vue({
  router,
  store,
  render: (h) => h(App),
  });

  return { app, router, store };
}

Server Entry file (src/server/main.js), gets the app from app.js, get the matched routes which will call the "serverPrefetch" on each component to get its data populated in the Vuex store, then returns the resolve promise

import createApp from '../app';

export default (context) => new Promise((resolve, reject) => {
  const { app, router, store } = createApp();

  router.push(context.url);

  router.onReady(() => {
  const matchedComponents = router.getMatchedComponents();
  if (!matchedComponents.length) {
    return reject(new Error('404'));
  }

  context.rendered = () => {
    context.state = store.state;
  };

  return resolve(app);
  }, reject);
});

Express server (/server.js) uses the bundle renderer to render the app to a string to put in the html

const fs = require('fs');
const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');
const dotenv = require('dotenv');

dotenv.config();

const bundleRenderer = createBundleRenderer(
  require('./dist/vue-ssr-server-bundle.json'),
  {
  template: fs.readFileSync('./index.html', 'utf-8'),
  },
);

const server = express();
server.use(express.static('public'));

server.get('*', (req, res) => {
  const context = {
  url: req.url,
  clientBundle: `client-bundle.js`,
  };

  bundleRenderer.renderToString(context, (err, html) => {
  if (err) {
    if (err.code === 404) {
    res.status(404).end('Page not found');
    } else {
    res.status(500).end('Internal Server Error');
    }
  } else {
    res.end(html);
  }
  });
});

const port = process.env.PORT || 3000
server.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
like image 370
se22as Avatar asked Nov 18 '20 19:11

se22as


People also ask

Is Vue router server-side?

Vue is primarily client side framework - rendering is done by JS running in the client's browser.

Does Vuex work with Vue 3?

The short answer is: Yes. Vuex is the preferred state management solution for Vue apps, and Vuex 4 is the version compatible with Vue 3. Do take note, however, that using Vuex for state management depends on the complexity of your application.

Does Vue support server-side rendering?

Vite provides built-in support for Vue server-side rendering, but it is intentionally low-level. If you wish to go directly with Vite, check out vite-plugin-ssr, a community plugin that abstracts away many challenging details for you.

Is NUXT faster than Vue?

Nuxt offers better SEO improvement with its server-side rendering feature, faster development with an auto-generic router, public share features, and management with great configuration options and meta tags methods, automatic code splitting with pre-rendered pages — all of this is impossible or extremely complex to ...


2 Answers

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 3 https://v3.vuejs.org/guide/migration/introduction.html

  • migrating from VueRouter 3 to VueRouter 4 https://next.router.vuejs.org/guide/migration/

  • migrating from Vuex 3 to Vuex 4 https://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 App
const 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 application
server.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.
like image 56
se22as Avatar answered Oct 11 '22 16:10

se22as


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')
}
like image 32
brillout Avatar answered Oct 11 '22 16:10

brillout