Adding server-side-rendering to existing vue 3 project
Tags: vue, typescript, serversiderendering
Published: 2021-07-11 12:45:58

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](https://github.com/shubhadip/vuex-typescript).

First we have to add few dependencies && devdependencies so that our project can support ssr
```
yarn add @vue/server-renderer vue@3.1.4
```
```
yarn add -D webpack-manifest-plugin webpack-node-externals express
```
> 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"
```

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)
```
 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
  };
}
```

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
  };
}
```
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);
```
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');
```
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");

}
``` 
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
    })
  )
```
you can read more about this configuration on this [SSRbuildConfig](https://v3.vuejs.org/guide/ssr/build-config.html#example-configuration)

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}`)
})
```
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"]);
```

```
#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",
...
```
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);
```
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();
      }
    })
```
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](https://github.com/shubhadip/vuex-typescript).

Resources which helped me to create this article are 
[https://v3.vuejs.org/guide/ssr/introduction.html#what-is-server-side-rendering-ssr](https://v3.vuejs.org/guide/ssr/introduction.html#what-is-server-side-rendering-ssr)
[youtube](https://www.youtube.com/watch?v=XJfaAkvLXyU&t=1s)