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
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
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.
Resources which helped me to create this article are
https://v3.vuejs.org/guide/ssr/introduction.html#what-is-server-side-rendering-ssr
youtube
Top comments (3)
Hello Can I add ssr by doing this in vue 2?
Hello sir. This is a great tutorial. Meanwhile, may you explain how to do it withour TypeScript (Vue 3, Vuex4 with JavaScript)?
i think you will just have to convert ts file to js, remove typescript dependencies & typings