DEV Community

loading...
Cover image for Adding server-side-rendering to existing vue 3 project

Adding server-side-rendering to existing vue 3 project

shubhadip
・5 min read

In this article we will see how to add server side rendering support to existing vue 3 project.I will be using one of my existing vue3 & vuex project which is available in github.

First we have to add few dependencies && devdependencies so that our project can support ssr

yarn add @vue/server-renderer vue@3.1.4
Enter fullscreen mode Exit fullscreen mode
yarn add -D webpack-manifest-plugin webpack-node-externals express
Enter fullscreen mode Exit fullscreen mode

NOTE: upgrading vue to latest version so that we can use onServerPrefetch lifecycle hook

for server-side-rendering we will have to create two different entry points(files) one, which will be used in server & another in client side also we will need to different build commands for server/client, lets add these two first in package.json scripts section

"build:client": "vue-cli-service build --dest dist/client",
"build:server": "VUE_APP_SSR=true vue-cli-service build --dest dist/server",
"build:ssr": "rm -rf ./dist && npm run build:client && npm run build:server"
Enter fullscreen mode Exit fullscreen mode

we have added a flag VUE_APP_SSR=true which would help us for bundling server side and ignore any window logics as those won't work in server-side.There will be two separate directory within dist folder client && server having separate code.

With build scripts ready lets move to entry files of server side & client side, we will have a common main.ts file which will be included in both entry files entry-client.ts && entry-server.ts

Lets create main.ts, we have to take care of createApp && createSSRApp for respective entry points.we can make use of flag VUE_APP_SSR=true or typeof window check

const isSSR = typeof window === 'undefined';
const app = (isSSR ? createSSRApp : createApp)(rootComponent)
Enter fullscreen mode Exit fullscreen mode

At the end our file would look something like this

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

export default function () {
  const isSSR = typeof window === 'undefined';
  const rootComponent = {
    render: () => h(App),
    components: { App },
  }
const app = (isSSR ? createSSRApp : createApp)(rootComponent)
  app.use(router);
  app.use(store);
  return {
    app,
    router,
    store
  };
}
Enter fullscreen mode Exit fullscreen mode

With the main crux ready lets create entry-client.ts && entry-server.ts

# entry-server.ts
import createApp from './main';

export default function () {

  const {
    router,
    app,
    store
  } = createApp();

  return {
    app,
    router,
    store
  };
}
Enter fullscreen mode Exit fullscreen mode

In server entry file, we are just exporting app,router,store which would be used while serving via express

# entry-client.ts
import createApp from './main'
declare let window: any;

const { app, router, store } = createApp();

(async (r, a, s) => {
  const storeInitialState = window.INITIAL_DATA;

  await r.isReady();

  if (storeInitialState) {
    s.replaceState(storeInitialState);
  }

  a.mount('#app', true);
})(router, app, store);
Enter fullscreen mode Exit fullscreen mode

window.INITIAL_DATA will hold the initialData that would be prefetched in server-side and would be stored in global window object, then in clientSide we will use this data to populate our store on first load.

Now,lets move to webpack config part of SSR, to work with webpack we have to create a vue.config.js file. we would include webpack-manifest-plugin,webpack-node-externals,webpack

const ManifestPlugin = require("webpack-manifest-plugin");
const nodeExternals = require("webpack-node-externals");
const webpack = require('webpack');
const path = require('path');
Enter fullscreen mode Exit fullscreen mode

Lets add config, i will be using export.chainWebpack directly to modify default webpack config provided by vue

exports.chainWebpack = webpackConfig => {
   if (!process.env.VUE_APP_SSR) {
    webpackConfig
      .entry("app")
      .clear()
      .add("./src/entry-client.ts");
    return;
  }

  webpackConfig
    .entry("app")
    .clear()
    .add("./src/entry-server.ts");

}
Enter fullscreen mode Exit fullscreen mode

based on which build is going to run we have added different entry points, for this we will use VUE_APP_SSR flag.

Now we have to add few more code so that webpack can build server-side bundle properly.we have to set target to node && libraryFormat to commonjs2 since this file is going to run via express

  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");
  webpackConfig.plugin('limit').use(
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  )
Enter fullscreen mode Exit fullscreen mode

you can read more about this configuration on this SSRbuildConfig

the last part is to create an server.js file which we will run on server via express.

const path = require('path');
const fs = require('fs');
const serialize = require('serialize-javascript');
const express = require('express');
const { renderToString } = require("@vue/server-renderer");
const  PORT = process.env.PORT || 4455
const manifest = require("../dist/server/ssr-manifest.json");
const appPath = path.join(__dirname, "../dist",'server', manifest["app.js"]);
const App = require(appPath).default;

const server = express();

server.use("/img", express.static(path.join(__dirname, "../dist/client", "img")));
server.use("/js", express.static(path.join(__dirname, "../dist/client", "js")));
server.use("/manifest.json", express.static(path.join(__dirname, "../dist/client", "manifest.json")));
server.use("/css", express.static(path.join(__dirname, "../dist/client", "css")));
server.use(
  "/favicon.ico",
  express.static(path.join(__dirname, "../dist/client", "favicon.ico"))
);

server.get('*', async (req, res) => {
  const { app, router, store } = await App(req);

  await router.push(req.url);
  await router.isReady();

  let appContent = await renderToString(app);

  const renderState = `
    <script>
      window.INITIAL_DATA = ${serialize(store.state)}
    </script>`;

  fs.readFile(path.join(__dirname, '../dist/client/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);
  });
});

server.listen(PORT, ()=>{
  console.log(`server listening at port ${PORT}`)
})
Enter fullscreen mode Exit fullscreen mode

we will be using above code which will intercept all request to our server.

const manifest = require("../dist/server/ssr-manifest.json");
const appPath = path.join(__dirname, "../dist",'server', manifest["app.js"]);
Enter fullscreen mode Exit fullscreen mode
#ssr-manifest.json
  "app.css": "/css/app.aaa5a7e8.css",
  "app.js": "/js/app.b8f9c779.js",
  "app.css.map": "/css/app.aaa5a7e8.css.map",
  "app.js.map": "/js/app.b8f9c779.js.map",
...
Enter fullscreen mode Exit fullscreen mode

this is where we use manifest.json file to select appropriate server file that would be served from express, contents of this json file is an object which has mapping for specific bundles

await router.push(req.url);
await router.isReady();
let appContent = await renderToString(app);
Enter fullscreen mode Exit fullscreen mode

above mentioned code will be used to match url-page properly with router.push, then renderToString will output everything as string which would be served from express.

In the above server.js you can see html variable holds the entire content that will be served from express to browser, next step would be to add support for meta-tags.

After all these configuration, now our pages can be rendered from server, now we will use axios to fetch data from endpoint which can rendered from server

# vue file
    const fetchInitialData = async () => {
      const response = await axios('https://jsonplaceholder.typicode.com/posts')
      store.dispatch(AllActionTypes.USER_LISTS, response.data || [])
    }

    onServerPrefetch(async () => {
     await fetchInitialData()
    })

    const listData = computed(() => {
      return store.getters.getUserList || []
    });

    onMounted(async () => {
      if(!listData.value.length){
        await fetchInitialData();
      }
    })
Enter fullscreen mode Exit fullscreen mode

The above code is an example of how can we fetch data for server-side rendering, we have used onServerPrefetch lifecycle method to fetch data && for client side we are using onMounted hook incase data is not available in window from server.

Note: I have skipped few steps while explaining, all code regarding this article is present at Repository.

Resources which helped me to create this article are
https://v3.vuejs.org/guide/ssr/introduction.html#what-is-server-side-rendering-ssr
youtube

Discussion (0)