What is server rendering

When implementing requirements, we need to know why server rendering is used, the value of server rendering, and the pros and cons of server rendering. Here don’t do too much introduction, can directly read the official document English document | Chinese document. If you are familiar with server rendering, you can start with # 3, how to set up server rendering.

2. Parse server rendering

If you are still confused by the official parsing, here is a simple diagram of server rendering and customer rendering.

2.1. Server rendering diagram

Note: the dotted red line is done on the server

2.2 Client rendering diagram

Note: the dotted red line is done on the server

2.3. Graphical analysis

As can be seen from the simple rendering flow of 2.1 and 2.2 above:

  1. The server rendering process is simple, the response can render the content, the client renders multiple times, and the data request is influenced by the client network
  2. Server rendering can avoid the white screen problem of client rendering
  3. Server rendering puts a certain amount of stress on the server

2.4 render route switching

If you are using a single page render, the route switching process for both server rendering and client rendering is as shown below. If multi-page rendering is used, the route switching process is the same as in 2.1

3. How to set up server rendering

I don’t know how to set up server render, but you can set up base with Vue SSR guide, Vue – SSR example, server-render.

3.1. Foundation construction

You can use the vite command to quickly set up a foundation project

// Note: I used [email protected] here
yarn create @vitejs/app vue-vite-ssr
Copy the code

3.2 Client entry

Note: Since server rendering is used, you need to understand that each request is a VUE instance. So first we need to modify main.ts

// ~/src/main.ts
import { createSSRApp } from "vue";
import App from "./App.vue";

export function createApp() {
    const app = createSSRApp(App);
    return { app };
}
Copy the code

The client entry becomes much simpler

// ~/src/entry-client.ts
import { createApp } from "./main"

const { app } = createApp();

app.mount("#app");
Copy the code

Of course, since the entry file has changed, the index.html entry needs to be modified accordingly

<! -- ~/index.html -->
<! DOCTYPEhtml>
<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>
  <div id="app"><! --app-html--></div>
  <! -- <script type="module" src="/src/main.ts"></script> -->
  <script type="module" src="/src/entry-client.ts"></script>
</body>

</html>
Copy the code

After the above modification, you will find that the main. Ts file is simply split into a client entry file. Nothing else has changed. Therefore, you can directly run yarn dev or NPM run dev to run the yarn dev project. The ~/ SRC /entry-client.ts file simply replaces main.ts as the entry to the project.

3.3. Create a server

With a brief understanding of server rendering, we can see that we need to build a simple server. This uses the same express setup as the official one, and can be ignored if it has nodeJS base.

// ~/server.js
const express = require("express");
const app = express();

app.use("*".async (req, res) => {
    let html = ` 
         
       
       
       Document   

From server render

'
; res.status(200).set({ "Content-Type": "text/html" }).end(html); }); app.listen(5000.() = > { console.log("[express] run http://localhost:5000"); }); Copy the code

3.4. Server entry

If you replace the HTML of the server response with the HTML of the vUE execution, isn’t that the VUE-SRR we need? So we need to get the vue executed HTML, which is what ~/ SRC /entry-server.js needs to do. Of course, if you still don’t know where to start or how to implement entry-server.js you can refer to Vue SSR guide, VUe-SSR, server-render. But you need to understand the role and function of each step.

RenderToString = renderToString = renderToString = renderToString = renderToString = renderToString = renderToString = renderToString = renderToString

// ~/src/entry-server.js
import { createApp } from "./main"
import { renderToString } from "@vue/server-renderer"

export async function render(){
    const { app } = createApp();

    const ctx = {};
    const html = await renderToString(app, ctx);

    return { html }
}
Copy the code

Note: Render uses async await because renderToString(app, CTX) is an asynchronous function. Some people may ask why synchronization, it has to interface with 2.1 points. The server generates HTML from the data and template before returning it to the client, so synchronization is required.

With entry-server.js, modify server.js again

// ~/server.js
const fs = require("fs");
const path = require("path");
const express = require("express");

const resolve = (p) = > path.resolve(__dirname, p);

async function createServer(root = process.cwd(), isProd = process.env.NODE_ENV === "production") {
    const app = express();
    let vite;
    let { createServer: _createServer } = require("vite");
    vite = await _createServer({
        root,
        server: {
            middlewareMode: true.watch: {
                usePolling: true.interval: 100,}}}); app.use(vite.middlewares); app.use("*".async (req, res) => {
        const { originalUrl: url } = req;
        try {
            let template, render;
            // Read the template
            template = fs.readFileSync(resolve("index.html"), "utf-8");
            template = await vite.transformIndexHtml(url, template);
            render = (await vite.ssrLoadModule("/src/entry-server.js")).render;

            let { html } = await render();
            // Replace the tags in the template
            html = template.replace(` <! --app-html-->`, html);
            / / response
            res.status(200).set({ "Content-Type": "text/html" }).end(html);
        } catch (e) {
            isProd || vite.ssrFixStacktrace(e);
            console.error(`[error]`, e.stack);
            res.status(500).end(e.stack); }});return { app };
}

// Create a service
createServer().then(({ app }) = > {
    app.listen(5000.() = > {
        console.log("[server] http://localhost:5000");
    });
});
Copy the code

Ok, now that the basic development works are done, run Node./server.js to access renderings from the server

3.5. Add the operating environment

With the previous development environment set up, the production environment is much easier. Since it is a build environment, it is time to introduce packaged files, so add packaged scripts first

// ~/package.json
{
"scripts": {
    "dev": "vite"."dev:server": "node ./server.js"."build": "yarn build:client && yarn build:server"."build:client": "vite build --ssrManifest --outDir dist/client"."build:server": "vite build --ssr src/entry-server.js --outDir dist/server"."serve": "cross-env NODE_ENV=production node ./server.js"}},Copy the code

~/server.js is updated again, the full code can be seen below

// ~/server.js.async function createServer(root = process.cwd(), isProd = process.env.NODE_ENV === "production") {
    const app = express();
    let vite;
    if (isProd) {
        // Production environment
        app.use(require("compression") ()); app.use(require("serve-static")(resolve("dist/client"), {
                index: false,})); }else {
        / / development
        let { createServer: _createServer } = require("vite");
        vite = await _createServer({
            root,
            server: {
                middlewareMode: true.watch: {
                    usePolling: true.interval: 100,}}}); app.use(vite.middlewares); }/ / template
    const indexHtml = isProd ? fs.readFileSync(resolve("dist/client/index.html"), "utf-8") : "";
    // Map files
    const manifest = isProd ? require("./dist/client/ssr-manifest.json") : {};

    app.use("*".async (req, res) => {
        const { originalUrl: url } = req;
        console.log(`[server] The ${new Date()} - ${url}`);
        try {
            let template, render;
            if (isProd) {
                / / production
                template = indexHtml;
                render = require("./dist/server/entry-server.js").render;
            } else {
                / / development
                template = fs.readFileSync(resolve("index.html"), "utf-8");
                template = await vite.transformIndexHtml(url, template);
                render = (await vite.ssrLoadModule("/src/entry-server.js")).render;
            }

            let { html } = await render(url, manifest);
            // Replace the tag
            html = template.replace(` <! --app-html-->`, html);
            / / response
            res.status(200).set({ "Content-Type": "text/html" }).end(html);
        } catch (e) {
            isProd || vite.ssrFixStacktrace(e);
            console.error(`[error]`, e.stack);
            res.status(500).end(e.stack); }});return{ app }; }...Copy the code

3.6 basic complete code

Note: this only implements the base build and does not integrate vuE-Router and VUex. If you desperately need to integrate the complete code, you can access vue-viet-SSR

main.ts

// ~/src/main.ts
import { createSSRApp } from "vue";
import App from "./App.vue";

export function createApp() {
    const app = createSSRApp(App);

    return { app };
}
Copy the code

entry-client.ts

// ~/src/entry-client.ts
import { createApp } from "./main"
const { app } = createApp();
app.mount("#app");

// ~/src/entry-server.js
import { createApp } from "./main"
import { renderToString } from "@vue/server-renderer"

export async function render(url, manifest){
    const { app } = createApp();

    const ctx = {};
    const html = await renderToString(app, ctx);
    
    const preloadLinks = renderPreloadLinks(ctx.modules, manifest);

    return { html, preloadLinks }
}


function renderPreloadLinks(modules, manifest) {
    let links = "";
    const seen = new Set(a); modules.forEach((id) = > {
        const files = manifest[id];
        if (files) {
            files.forEach((file) = > {
                if(! seen.has(file)) { seen.add(file); links += renderPreloadLink(file); }}); }});return links;
}

function renderPreloadLink(file) {
    if (file.endsWith(".js")) {
        return `<link rel="modulepreload" crossorigin href="${file}"> `;
    } else if (file.endsWith(".css")) {
        return `<link rel="stylesheet" href="${file}"> `;
    } else {
        // TODO
        return ""; }}Copy the code

server.js

// ~/server.js
const fs = require("fs");
const path = require("path");
const express = require("express");
const resolve = (p) = > path.resolve(__dirname, p);

async function createServer(root = process.cwd(), isProd = process.env.NODE_ENV === "production") {
    const app = express();
    let vite;
    if (isProd) {
        // Production environment
        app.use(require("compression") ()); app.use(require("serve-static")(resolve("dist/client"), {
                index: false,})); }else {
        / / development
        let { createServer: _createServer } = require("vite");
        vite = await _createServer({
            root,
            server: {
                middlewareMode: true.watch: {
                    usePolling: true.interval: 100,}}}); app.use(vite.middlewares); }/ / template
    const indexHtml = isProd ? fs.readFileSync(resolve("dist/client/index.html"), "utf-8") : "";
    // Map files
    const manifest = isProd ? require("./dist/client/ssr-manifest.json") : {};

    app.use("*".async (req, res) => {
        const { originalUrl: url } = req;
        console.log(`[server] The ${new Date()} - ${url}`);
        try {
            let template, render;
            if (isProd) {
                / / production
                template = indexHtml;
                render = require("./dist/server/entry-server.js").render;
            } else {
                / / development
                template = fs.readFileSync(resolve("index.html"), "utf-8");
                template = await vite.transformIndexHtml(url, template);
                render = (await vite.ssrLoadModule("/src/entry-server.js")).render;
            }

            let { html, preloadLinks } = await render(url, manifest);
            // Replace the tag
            html = template
                .replace(` <! -- app-preload-links -->`, preloadLinks)
                // For client tag server rendering
                .replace(` <! --app-html-->`, html);
            / / response
            res.status(200).set({ "Content-Type": "text/html" }).end(html);
        } catch (e) {
            isProd || vite.ssrFixStacktrace(e);
            console.error(`[error]`, e.stack);
            res.status(500).end(e.stack); }});return { app };
}

// Create a service
createServer().then(({ app }) = > {
    app.listen(5000.() = > {
        console.log("[server] http://localhost:5000");
    });
});

Copy the code

index.html

// ~/index.html
<! DOCTYPEhtml>
<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 ssr</title>
  <! -- app-preload-links -->
</head>

<body>
  <div id="app"><! --app-html--></div>
  <! -- app-script -->
  <script type="module" src="/src/entry-client.ts"></script>
</body>

</html>
Copy the code

4. Import the vue-router route

In order to reduce the compact class capacity, the following repeated content will “…” Omitted. If you urgently need to integrate the complete code you can access vue-viet-SSR

// ~/src/router.ts

import {
    createWebHistory,
    createRouter as _createRouter,
    createMemoryHistory,
    RouteRecordRaw,
} from "vue-router";

const routes: Array<RouteRecordRaw> = [
    {
        path: "/".alias: "/index".component: () = > import("./views/index.vue"),}, {path: "/".component: () = > import("./views/index.vue"),
        children: [{path: "client".component: () = > import("./views/client.vue"),}, {path: "server".component: () = > import("./views/server.vue"),},],},];export function createRouter() {
    return _createRouter({
        // history: import.meta.env.SSR ? createMemoryHistory("/ssr") : createWebHistory("/ssr"),
        history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
        routes,
    });
}
Copy the code

Modify the mian. Ts

import { createRouter } from "./router"
// ...
const router = createRouter();
app.use(router);
return { app, router };
// ...

Copy the code

Modify the entry – client. Ts

// ...
const { app, router, store } = createApp();
router.isReady().then(() = > {
    app.mount("#app");
});
Copy the code

Modify the entry – server. Js

// ...
const { app, router } = createApp();

// The access can be normal only after the base route is removed
router.push(url.replace(router.options.history.base, ""));
/ / need to manually trigger, see: https://next.router.vuejs.org/zh/guide/migration/#%E5%B0%86-onready-%E6%94%B9%E4%B8%BA-isready
await router.isReady();
// ...
return { html, preloadLinks };
// ...
Copy the code

So the route transformation is simply completed.

Inject your soul

I don’t know if you’ve noticed that everything we’ve done above is all about rendering without data (no soul), there’s no template + data as in 2.1. If you notice this, you already understand why vuE-SSR is used.

Due to the composition-API changes in vue3. X it is possible to define parameters without a single data. So to inject soul, here’s my idea:

5.1. Define Store.ts

Between you and me, I finally gave up the method. You can customize the package if you are interested

// ~/src/store.ts
export interface State {
    count: number;
}

export interface Store {
    state: State;
    setState: (key: keyof State, data: any) = > void;
    getState: (key: keyof State) = > any;
}

let state: State;
const setState = function (key: keyof State, data: any) {
    state[key] = data;
};

const getState = function (key: keyof State) {
    return state[key];
};

export function createStore(data? : Record<string.any>) {
    console.log(">>> data", data);
    
    // @ts-ignore
    state = data || {
        count: 0};return { state, setState, getState };
}

export function useStore() {
    return { state, setState, getState };
}
Copy the code

Modify the ~ / SRC/main. Ts

// ...
// @ts-ignore
const store = !import.meta.env.SSR && window && window.__INITIAL_STATE__ ? createStore(window.__INITIAL_STATE__) : createStore();
// ...
return { app, router, store };
Copy the code

Modify the ~ / SRC/entry – server. Js

// ...
export async function render(url, manifest) {
    / / execution asyncData (); Notice the order in relation to renderToString
    await invokeAsyncData({ store, route: router.currentRoute.value });
}

function invokeAsyncData({ store, route }) {
    console.log("[invokeAsyncData]", route.matched);
    return Promise.allSettled(
        route.matched.map(({ components }) = > {
            let asyncData = components.default.asyncData || false;
            returnasyncData && asyncData({ store, route }); })); }Copy the code

Components use

// ~/src/views/index.vue
<template>
    <h2>Home page</h2>
    <router-view></router-view>
    count : {{ count }}
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
import { useStore, Store } from ".. /store";

export default defineComponent({
    asyncData({ store }: { store: Store }) {
        return new Promise((resolve) = > {
            store.setState("count".4);
            // Note that the new version of TS requires a parameter, otherwise an error is reported
            resolve(true);
        });
    },
    setup(props, context) {
        let store = useStore();
        console.log("> > >", store );
        
        let state = reactive({
            count: store.getState("count")});return{... toRefs(state), }; }});</script>
<style lang="scss"></style>
Copy the code

Modify the above code to see the template + data rendering.

5.2 Route switching processing

Although the home page rendering did what we wanted, asyncData was not called when the route was switched

// ~/src/main.ts

// Add by routing the hook function.
// Note that vue-router can omit the third next parameter, as described in the vue-router@4 documentation
router.beforeResolve(async (to, from) = > {// Since the getMatchedComponents API is officially removed, it is customized
    let toMatchedComponents = getMatchedComponents(to.matched);

    toMatchedComponents.length &&
        (await Promise.allSettled(
            toMatchedComponents.map((component) = > {
                // @ts-ignore
                if (component.asyncData) {
                    // @ts-ignore
                    return component.asyncData({ store, route: to }); }}))); });function getMatchedComponents(list: RouteRecordNormalized[]) {
    return list.map(({ components }) = > {
        return components.default;
    });
}

Copy the code

I thought it was so simple, but I found it in the nested routine. Switch routing routing will repeat the same call asyncData function, then secretly to bi li bi li borrow some optimization method.

// ~/src/main.ts
router.beforeResolve(async (to, from) = > {// Since the getMatchedComponents API is officially removed, it is customized
    let toMatchedComponents = getMatchedComponents(to.matched);
    let fromMatchedComponents = getMatchedComponents(from.matched);
    // Optimize filtering
    let isSameCompoent = false;
    let components = toMatchedComponents.filter((compnent, index) = > {
        returnisSameCompoent || (isSameCompoent = fromMatchedComponents[index] ! == compnent); }); components.length && (await Promise.allSettled(
            components.map((component) = > {
                // @ts-ignore
                if (component.asyncData) {
                    // @ts-ignore
                    return component.asyncData({ store, route: to }); }}))); });Copy the code

5.3 Bilibili

Bilibili is a good VUE-SSR case, before read a bilibili front road, a comprehensive analysis of the bilibili optimization program and implementation program, similar to this, of course, just similar. May be due to some reasons can not find, if you know friends can stay, thank you!

Above is the bilibili front page (you can search __INITIAL_STATE__ in the file according to the directory) to find the code. Although Bilibili uses VUE2, the functional modules can still be seen. The server returns window.__initial_state__ with the initial state of the replaceState API in VUEX; Finally, it is activated through the VUE client.

Switch client route data processing, you can parameter mantra back to fight PS: 5T5 YYds, also find the corresponding file search __INITIAL_STATE__ can be found. It can be seen that the same filtering optimization scheme is implemented during route switchover to prevent asyncData from being invoked repeatedly under the same component.

Of course Bilibili can learn a lot, and I’m not just talking about the front end; Also include bi li bi li content.

6. Introduce Vuex

Note: Although adding custom store can achieve the purpose, the encapsulation degree is not enough and the function is not perfect; Merely as a means to an end. You can do this if you are a personal project. If you are a company project, please take vuex seriously unless you are a big shot. Chinese failed the kind of) self-encapsulation. For reference, visit vue-viet-SSR

With the knowledge of the store defined earlier, you can simply replace it with vuex.

// ~/src/store.ts
import { InjectionKey } from "vue";
import { RouteLocationNormalized } from "vue-router";
import { createStore as _createStore, Store } from "vuex";

// Declare type for store state
export interface State {
    client: string[];
    server: string[];
}

export interface AsyncDataParam {
    store: Store<State>;
    route: RouteLocationNormalized;
}

// // defines the injection key
export const key: InjectionKey<Store<State>> = Symbol(a);export function createStore() {
    const store = _createStore<State>({
        state: {
            client: [].server: []},mutations: {
            setClient(state, data) {
                state.client = data;
            },
            setServer(state, data){ state.server = data; }},actions: {
            AYSNC_CLIENT({ commit }) {
                return new Promise((resolve, reject) = > {
                    setTimeout(() = > {
                        commit("setClient"["vue3"."vue-router"."vuex"]);
                        resolve(true);
                    }, 20);
                });
            },
            ASYNC_SERVER({ commit }) {
                return new Promise((resolve, reject) = > {
                    setTimeout(() = > {
                        commit("setServer"["vite"."express"."serialize-javascript"]);
                        resolve(true);
                    }, 30); }); ,}}});/ / replace state
    // @ts-ignore
    if (!import.meta.env.SSR && window && window.__INITIAL_STATE__) {
        // @ts-ignore
        store.replaceState(window.__INITIAL_STATE__);
    }

    return { store };
}
Copy the code

Modify the ~ / SRC/main. Ts

// ...
router.beforeResolve(async (to, from) = > {let toMatchedComponents = getMatchedComponents(to.matched);
    let fromMatchedComponents = getMatchedComponents(from.matched);
    // Optimize filtering
    let isSameCompoent = false;
    let components = toMatchedComponents.filter((compnent, index) = > {
        returnisSameCompoent || (isSameCompoent = fromMatchedComponents[index] ! == compnent); });console.log("[components]", components, toMatchedComponents, fromMatchedComponents);

    // The component that needs to perform async
    components.length &&
        (await Promise.allSettled(
            components.map((component) = > {
                // @ts-ignore
                if (component.asyncData) {
                    // @ts-ignore
                    return component.asyncData({ store, route: to }); }}))); });// ...
Copy the code

Use the sample

// ~/src/views/client.vue
<template>
    <h3>The client</h3>
    client:{{ client }}
</template>
<script lang="ts">
import { defineComponent, isReactive, toRefs } from "vue";
import { Store, useStore } from "vuex";
import { AsyncDataParam, key, State } from ".. /store";
export default defineComponent({
    asyncData({ store }: AsyncDataParam) {
        console.log("[AYSNC_CLIENT]");
        
        return store.dispatch("AYSNC_CLIENT");
    },
    setup(props, context) {
        const { client } = useStore<State>(key).state;

        console.log("> > >", client, isReactive(client));

        return {
            client,
            / /... toRefs()}; }});</script>
<style lang="scss"></style>
Copy the code

7,

Vue-ssr front-end basic functions were established. Think carefully if you are working on a corporate project. Because server rendering is not only front-end development, but also needs a powerful background service support. For large traffic sites; You should also consider caching, server resources, stress, monitoring, and a number of other issues. Whether it is a first access or a route switch, if the request data is delayed too long, the page will be in a state of suspended animation. In this case, it is better to completely separate the front and back ends, at least to let the user know the status of the current route.

For complete code, go to VUe-viet-SSR

If there are mistakes, welcome to correct them. Learn together and make progress together