Server-side Rendering (SSR) is one of the methods to present web content to the user. Even though this method is quite old, I never had a chance to learn it by a code, only by the concept. In this article, I will try to provide an overview of SSR, with the goal of demonstrating its implementation through simple examples.
What is SSR
SSR refers to generating content in the browser while the fetching and rendering are done on the server. Since the content is rendered on the server, it becomes available to the user once the loading is complete. However, for any interaction process, we need to handle it by doing the hydration process first. The HTML that the user receives is completely static, and there is a waiting time for the hydration process. This is called Time to Interactive. You can read the explanation from the web.dev team for more information here.
SSR in Vue
Nuxt is a popular framework for handling SSR project. However, in this article, we won’t use Nuxt to implement the SSR. Instead, we will use:
- Vue 3 as the base client library
- Express for the back end
- Vite for the bundler
How to implement
For this article, we will use Stackblitz to implement our small project. You can check the full code here
Initiate the Project
Let’s initiate the project by using Vite vue-ts
. Here is our directory, we will remove some files that we do not need.
After that, we will add these files
- index.html
- server.js # Main application server
- src/
- main.ts # we will store the function to init our app here
- entry-client.ts # This function mounts the app to a DOM element and will be used in the hydration process.
- entry-server.ts # renders the app using the framework's SSR API, we will use it in server.js
Add Client Code
For our client-side code, we add main.js
. The createSSRApp
is a function that was introduced in Vue 3 to create a server-side rendering application. This function can be used to render the Vue application on the server.
import { createSSRApp } from 'vue';
import App from './App.vue';
export const createApp = () => {
/**
* use createSSRApp to render the Vue App on the server
* and send it to the user to do the hydration process
*/
const app = createSSRApp(App);
return {
app,
};
};
Then we add entry-client.ts
to initiate our app on the client-side
import { createApp } from './main.js';
/**
* initiate the Vue App for a client-side application
*/
const { app } = createApp();
app.mount('#app');
Also, let’s update the App.vue
. Here we display a counter inside the button, which increments with every click.
<template>
<div>
<button @click="handleIncrement">{{ count }}</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(1);
const handleIncrement = () => {
count.value += 1;
};
</script>
Add Server Code
The next step is to handle the server-side code. Add entry-server.ts
and use the app
from createSSRApp
. Then, render the app to HTML using renderToString
, which can then be sent to the client.
import { renderToString } from 'vue/server-renderer';
import { createApp } from './main';
/**
* initiate the Vue App for a server-side application,
* we use renderToString to render the app to HTML
*/
export const render = async () => {
const { app } = createApp();
const html = await renderToString(app);
return {
html,
};
};
Combine Client and Server
Now let’s handle the server. We will use express for our Node.js app
npm install express
Next, add server.js
. You can find this code guide in Vite SSR guide.
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createServer } = require('vite');
async function initServer() {
const app = express();
// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
// can take control
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
});
// Use vite's connect instance as middleware. If you use your own
// express router (express.Router()), you should use router.use
app.use(vite.middlewares);
app.use('*', async (req, res) => {
// 1. Read index.html
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
);
// 2. Apply Vite HTML transforms. This injects the Vite HMR client,
// and also applies HTML transforms from Vite plugins, e.g. global
// preambles from @vitejs/plugin-react
template = await vite.transformIndexHtml(req.originalUrl, template);
// 3. Load the server entry. ssrLoadModule automatically transforms
// ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
const render = (await vite.ssrLoadModule('/src/entry-server.ts')).render;
// 4. render the app HTML. This assumes entry-server.js's exported
// `render` function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const { html: appHtml } = await render();
// 5. Inject the app-rendered HTML into the template.
const html = template.replace('<!--main-app-->', appHtml);
// 6. Send the rendered HTML back.
res.set({ 'Content-Type': 'text/html' }).end(html);
});
return app;
}
initServer().then((app) =>
app.listen(3000, () => {
console.log('ready');
})
);
Then, let’s update our index.html
. Add the placeholder <!--main-app-->
and update the script source file to /src/entry-client.ts
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<!-- 1. Add "main-app", it will be replaced by our App -->
<div id="app"><!--main-app--></div>
<!-- 2. Update the source to entry-client.ts -->
<script type="module" src="/src/entry-client.ts"></script>
</body>
</html>
Finally, update the dev
scripts in package.json
to node server.js
{
"scripts": {
"dev": "node server.js"
},
}
Conclusion
In this article, we already learn how to create simple SSR applications by using Vue, Vite, and Express. There are a lot of things that we can improve, some of them are:
- Writing SSR-friendly Code
- Building for production
- Generate Preload asset
- Generate Meta tag
- Handle routing and middleware
Top comments (2)
Hi,
In section "Add Client Code" do I have to create file main.js or it is mistake and should I add data to existing file main.ts