My first time implementing SSR using Vue 3 and Vite.
Tags: vue, webdev, javascript, vite
Published: 2023-03-16 03:41:12

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](https://web.dev/tti/).

# 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](https://stackblitz.com/edit/vitejs-vite-ycb4qu?file=README.md)

## 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dxf08ukii6gdujhuuop5.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0gnjcfjhfszkenk8iue.png)

After that, we will add these files

```bash
- 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.

```jsx
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

```jsx
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.

```html
<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.

```jsx
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

```bash
npm install express
```

Next, add `server.js`. You can find this code guide in [Vite SSR guide](https://vitejs.dev/guide/ssr.html#setting-up-the-dev-server).

```jsx
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`

```html
<!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`

```jsx
{
 "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](https://vuejs.org/guide/scaling-up/ssr.html#writing-ssr-friendly-code)
- [Building for production](https://vuejs.org/guide/scaling-up/ssr.html#writing-ssr-friendly-code)
- [Generate Preload asset](https://vuejs.org/guide/scaling-up/ssr.html#writing-ssr-friendly-code)
- Generate Meta tag
- Handle routing and middleware