preface

Hello, everyone, I am yi, in the last article, we have talked about how to use Vite + VUe3 + TS + Pinia +vueuse to build front-end enterprise-class projects, you can see that many students like, today to bring you a long time how to use Vite to build front-end SSR enterprise-class projects, I hope you enjoy it!

If you’re interested in Vite, check out the column “Vite from Beginner to Master.”

Understand the SSR

What is the SSR

Server-side Rendering refers to the process of stitching HTML structure of a page, sending it to the browser, and binding states and events to make it into a fully interactive page.

Simple understanding is that HTML is written by the server, can dynamically change the page content, the so-called dynamic page. In the early years of PHP, ASP, JSP these Server pages are SSR.

Why use SSR

  • The web page content is rendered on the server side and transferred to the browser once, soThe first screen loads very fast;
  • Is conducive to SEO, because the server returns a complete HTML, you can see the complete DOM in the browser, crawler, Baidu search engine is more friendly;

Quick look

Github repository address

To make a long story short, get straight to work

Recommended package manager priority: PNPM > YARN > NPM > CNPM

Initialize the project

pnpm create vite koa2-ssr-vue3-ts-pinia -- --template vue-ts
Copy the code

Integrated basic configuration

As the focus of this paper is SSR configuration, in order to optimize the reader’s experience, so the basic configuration of the project will not be introduced in detail, in my last article “Hand in hand to teach you to use Vite +vue3+ TS + Pinia + Vueuse to build enterprise front-end project” has been introduced in detail, you can refer to

  1. Modify thetsconfig.json :Look at the code
  2. Modify thevite.config.ts:Look at the code
  3. integrationeslintprettierUniform code quality style:You can view the tutorial
  4. integrationcommitizenhuskySpecification git commit:You can view the tutorial

By now, the basic framework of our project has been built

Modify the client entry

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

// In order to ensure that the data does not interfere with each other, a new instance needs to be exported per request
export const createApp = () = > {
    const app = createSSRApp(App);
    return { app };
}
Copy the code
  1. new~/src/entry-client.ts
import { createApp } from "./main"

const { app } = createApp();

app.mount("#app");
Copy the code
  1. Modify the~/index.htmlThe entrance of the
<! DOCTYPEhtml>
<html lang="en">.<script type="module" src="/src/entry-client.ts"></script>.</html>
Copy the code

At this point you run PNPM run dev and see that the page still works because so far it has just split a file and changed the createSSRApp method.

Creating a development server

Using Koa2

  1. The installationkoa2
pnpm i koa --save && pnpm i @types/koa --save-dev
Copy the code
  1. Installing middlewarekoa-connect
pnpm i koa-connect --save
Copy the code
  1. Use: New~/server.js

Note: because this file is node run entry, so use JS, if you use TS file, need to use TS-Node to run, resulting in complex program

const Koa = require('koa');

(async() = > {const app = new Koa();

    app.use(async (ctx) => {
        ctx.body = ` 
        koa2 + vite + ts + vue3 + vue-router  

Integrate front-end SSR enterprise-class projects with KOA2 + Vite + TS + vue3 + VUE-Router '

; }); app.listen(9000.() = > { console.log('server is listening in 9000'); }); }) ();Copy the code
  1. runnode server.js
  2. Results:

Render is replaced with one in the project root directoryindex.html

  1. Modify theserver.jsIn thectx.bodyReturns theindex.html
 const fs = require('fs');
 const path = require('path');
 ​
 const Koa = require('koa');
 ​
 (async() = > {const app = new Koa();
 ​
     / / get the index. HTML
     const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
 ​
     app.use(async (ctx) => {
         ctx.body = template;
     });
 ​
     app.listen(9000.() = > {
         console.log('server is listening in 9000'); }); }) ();Copy the code
2. After running 'node server.js', we will see that the' index. HTML 'returns blank content, but we need to return the' vue template ', so we just need to make a 'regular substitution'Copy the code
  1. toindex.htmladd<! --app-html-->tag
 <! 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>koa2 + vite + ts + vue3</title>
   </head>
   <body>
     <div id="app"><! --app-html--></div>
     <script type="module" src="/src/entry-client.ts"></script>
   </body>
 </html>
Copy the code
  1. Modify theserver.jsIn thectx.body
// other code ...

(async() = > {const app = new Koa();

    / / get the index. HTML
    const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');

    app.use(async (ctx) => {
        let vueTemplate = '

Now pretend this is a vue template

'
; // Replace <! In index.html - app - HTML - > tag let html = template.replace('<! --app-html-->', vueTemplate); ctx.body = html; }); app.listen(9000.() = > { console.log('server is listening in 9000'); }); }) ();Copy the code
  1. runnode server.jsAfter that, we’ll see the returnVariable vueTemplatecontent

So now the service is up and running, but let’s imagine that our page template uses vUE, and VUE returns a VUE instance template, so I’m going to convert that VUE instance template into renderable HTML, So @vue/server-renderer was born

Added the server entrance

Since Vue returns vue instance templates instead of renderable HTML, we need to use @vue/server-renderer for the conversion

  1. The installation@vue/server-renderer
pnpm i @vue/server-renderer --save
Copy the code
  1. new~/src/entry-server.ts
import { createApp } from './main';
import { renderToString } from '@vue/server-renderer';

export const render = async() = > {const { app } = createApp();
	
  // Inject context objects in vue SSR
  constrenderCtx: {modules? :string[]} = {}

  let renderedHtml = await renderToString(app, renderCtx)

  return { renderedHtml };
}
Copy the code

So how do you use entry-server.ts when you need vite

injectionvite

  1. Modify the~/server.js
const fs = require('fs')
const path = require('path')

const Koa = require('koa')
const koaConnect = require('koa-connect')

const vite = require('vite')

;(async() = > {const app = new Koa();

    // Create a vite service
    const viteServer = await vite.createServer({
        root: process.cwd(),
        logLevel: 'error'.server: {
        middlewareMode: true,}})// Register vite's Connect instance as middleware (note: Vite. Middlewares is a Connect instance)
    app.use(koaConnect(viteServer.middlewares))

    app.use(async ctx => {
        try {
            // 1. Get index.html
            let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');

            // 2. Apply Vite HTML conversion. This will inject the Vite HMR client,
            template = await viteServer.transformIndexHtml(ctx.path, template)

            // 3. Load the server entry, vite. SsrLoadModule will automatically convert
            const { render } = await viteServer.ssrLoadModule('/src/entry-server.ts')

            // 4. Render the application's HTML
            const { renderedHtml } = await render(ctx, {})

            const html = template.replace('<! --app-html-->', renderedHtml)

            ctx.type = 'text/html'
            ctx.body = html
        } catch (e) {
            viteServer && viteServer.ssrFixStacktrace(e)
            console.log(e.stack)
            ctx.throw(500, e.stack)
        }
    })

    app.listen(9000.() = > {
        console.log('server is listening in 9000'); }); }) ()Copy the code
  1. runnode server.jsYou can see the contents of the returned app.vue template, as shown below

  1. And weRight click to view display page source codeYou will also see the rendered normal HTML
<! DOCTYPEhtml>
<html lang="en">
  <head>
    <script type="module" src="/@vite/client"></script>

    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>koa2 + vite + ts + vue3</title>
  </head>
  <body>
    <div id="app"><! - [-- -- ><img alt="Vue logo" src="/src/assets/logo.png"><! - [-- -- ><h1 data-v-469af010>Hello Vue 3 + TypeScript + Vite</h1><p data-v-469af010> Recommended IDE setup: <a href="<https://code.visualstudio.com/>" target="_blank" data-v-469af010>VSCode</a> + <a href="<https://github.com/johnsoncodehk/volar>" target="_blank" data-v-469af010>Volar</a></p><p data-v-469af010>See <code data-v-469af010>README.md</code> for more information.</p><p data-v-469af010><a href="<https://vitejs.dev/guide/features.html>" target="_blank" data-v-469af010> Vite Docs </a> | <a href="<https://v3.vuejs.org/>" target="_blank" data-v-469af010>Vue 3 Docs</a></p><button type="button" data-v-469af010>count is: 0</button><p data-v-469af010> Edit <code data-v-469af010>components/HelloWorld.vue</code> to test hot module replacement. </p><! -] -- ><! -] -- ></div>
    <script type="module" src="/src/entry-client.ts"></script>
  </body>
</html>
Copy the code

At this point we have rendered normally in the development environment, but let’s think about what we should do in the production environment, because we can’t run vite directly in the production environment.

So let’s deal with how to run in production

Adding a development environment

In order for the SSR project to be able to run in a production environment, we need:

  1. A normal build generates oneClient build package;
  2. Regenerate into an SSR build and make it throughrequire()Load directly, so you don’t need to use Vite anymoressrLoadModule;
  3. Modify thepackage.json
. {"scripts": {
    // Development environment
    "dev": "node server-dev.js".// Production environment
    "server": "node server-prod.js"./ / build
    "build": "pnpm build:client && pnpm build:server"."build:client": "vite build --outDir dist/client"."build:server": "vite build --ssr src/entry-server.js --outDir dist/server",}}...Copy the code
  1. Modify theserver.jsserver-dev.js
  2. runpnpm run buildBuild a package
  3. newserver-prod.js

Note: To handle static resources, we need to add koA-send middleware here: PNPM I koa-send –save

const Koa = require('koa');
const sendFile = require('koa-send');

const path = require('path');
const fs = require('fs');

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

const clientRoot = resolve('dist/client');
const template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8');
const render = require('./dist/server/entry-server.js').render;
const manifest = require('./dist/client/ssr-manifest.json');

(async() = > {const app = new Koa();

    app.use(async (ctx) => {
				
				// A static resource is requested
        if (ctx.path.startsWith('/assets')) {
            await sendFile(ctx, ctx.path, { root: clientRoot });
            return;
        }

        const [ appHtml ] = await render(ctx, manifest);

        const html = template
            .replace('<! --app-html-->', appHtml);

        ctx.type = 'text/html';
        ctx.body = html;
    });

    app.listen(8080.() = > console.log('started server on http://localhost:8080')); }) ();Copy the code

At this point, we have normal access to both the development environment and the build environment, so is everything ok?

For the user’s ultimate user experience, preloading must be arranged

preload

We know that vUE components are dynamically generated when rendering in HTML corresponding JS and CSS, etc.

Static site generation (SSG) takes the form of pre-rendering the JS and CSS files directly in HTML when the user obtains the server template (i.e. the dist/client directory generated after vite build).

Gossip less say, understand the reason, direct stem ~

  1. Generate preload instructions: in package.jsonbuild:clientadd--ssrManifestFlag, generated after runssr-manifest.json
. {"scripts": {..."build:client": "vite build --ssrManifest --outDir dist/client". }}...Copy the code
  1. inentry-sercer.tsAdd parser generated inssr-manifest.jsonmethods
export const render = async (
    ctx: ParameterizedContext,
    manifest: Record<string.string[] >) :Promise"[string.string] > = > {const { app } = createApp();
    console.log(ctx, manifest, ' ');

    constrenderCtx: { modules? :string[]} = {};const renderedHtml = await renderToString(app, renderCtx);

    const preloadLinks = renderPreloadLinks(renderCtx.modules, manifest);

    return [renderedHtml, preloadLinks];
};

/** * Parse links that need to be preloaded *@param modules
 * @param manifest
 * @returns string* /
function renderPreloadLinks(
    modules: undefined | string[],
    manifest: Record<string.string[] >) :string {
    let links = ' ';
    const seen = new Set(a);if (modules === undefined) throw new Error(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;
}

The following methods only apply to JS and CSS. If you need to handle other files, you can add them yourself@param file
 * @returns string* /
function renderPreloadLink(file: string) :string {
    if (file.endsWith('.js')) {
        return `<link rel="modulepreload" crossorigin href="${file}"> `;
    } else if (file.endsWith('.css')) {
        return `<link rel="stylesheet" href="${file}"> `;
    } else {
        return ' '; }}Copy the code
  1. toindex.htmladd<! --preload-links-->tag
  2. transformserver-prod.js
. (async() = > {const app = new Koa();

    app.use(async (ctx) => {
				
	...

        const [appHtml, preloadLinks] = await render(ctx, manifest);

        const html = template
            .replace('<! --preload-links-->', preloadLinks)
            .replace('<! --app-html-->', appHtml);

        // do something
    });

    app.listen(8080.() = > console.log('started server on http://localhost:8080')); }) ();Copy the code
  1. runpnpm run build && pnpm run serveCan display normally

At this point the basic rendering is complete, because we need to render on the browser, so the vue-Router route is essential

Integrated vue – the router

  1. Install the vue – the router
pnpm i vue-router --save
Copy the code
  1. The route page is addedindex.vuelogin.vueuser.vue
  2. newsrc/router/index.ts
import {
    createRouter as createVueRouter,
    createMemoryHistory,
    createWebHistory,
    Router
} from 'vue-router';

export const createRouter = (type: 'client' | 'server') :Router= >
    createVueRouter({
        history: type= = ='client' ? createWebHistory() : createMemoryHistory(),

        routes: [{path: '/'.name: 'index'.meta: {
                    title: 'home'.keepAlive: true.requireAuth: true
                },
                component: () = > import('@/pages/index.vue')}, {path: '/login'.name: 'login'.meta: {
                    title: 'login'.keepAlive: true.requireAuth: false
                },
                component: () = > import('@/pages/login.vue')}, {path: '/user'.name: 'user'.meta: {
                    title: 'User center'.keepAlive: true.requireAuth: true
                },
                component: () = > import('@/pages/user.vue')}});Copy the code
  1. Modify import filesrc/enter-client.ts
import { createApp } from './main';

import { createRouter } from './router';
const router = createRouter('client');

const { app } = createApp();

app.use(router);

router.isReady().then(() = > {
    app.mount('#app'.true);
});
Copy the code
  1. Modify import filesrc/enter-server.ts
.import { createRouter } from './router'
const router = createRouter('client');

export const render = async (
    ctx: ParameterizedContext,
    manifest: Record<string.string[] >) :Promise"[string.string] > = > {const { app } = createApp();

    // Route registration
    const router = createRouter('server');
    app.use(router);
    await router.push(ctx.path);
    awaitrouter.isReady(); . }; .Copy the code
  1. runpnpm run build && pnpm run serveCan display normally

Integrated pinia

  1. The installation
pnpm i pinia --save
Copy the code
  1. newsrc/store/user.ts
import { defineStore } from 'pinia';

export default defineStore('user', {
    state: () = > {
        return {
            name: 'Joe'.age: 20
        };
    },
    actions: {
        updateName(name: string) {
            this.name = name;
        },
        updateAge(age: number) {
            this.age = age; }}});Copy the code
  1. newsrc/store/index.ts
import { createPinia } from 'pinia';
import useUserStore from './user';

export default() = > {const pinia = createPinia();

    useUserStore(pinia);

    return pinia;
};

Copy the code
  1. newUsePinia.vueUse, and inpages/index.vueThe introduction of
<template>
    <h2>Welcome to vite+vue3+ TS + Pinia + Vue-Router4</h2>
    <div>{{userstore. name}} age: {{userstore. age}}</div ><br />
    <button @click="addAge">Click to add one year to the age of {{userstore. name}}</button>
    <br />
</template>

<script lang="ts">
    import { defineComponent } from 'vue';
    import useUserStore from '@/store/user';
    export default defineComponent({
        name: 'UsePinia'.setup() {
            const userStore = useUserStore();

            const addAge = () = > {
                userStore.updateAge(++userStore.age);
            };
            return{ userStore, addAge }; }});</script>

Copy the code
  1. injectionpinia: modifysrc/entry-client.ts
.import createStore from '@/store';
const pinia = createStore();

const { app } = createApp();

app.use(router);
app.use(pinia);

// Initialize pini
// Note: __INITIAL_STATE__ needs to be defined in SRC /types/shims-global.d.ts
if (window.__INITIAL_STATE__) {
    pinia.state.value = JSON.parse(window.__INITIAL_STATE__); }...Copy the code
  1. Modify thesrc/entry-server.ts
.import createStore from '@/store';

export const render = () = >{...// pinia
    const pinia = createStore();
    app.use(pinia);
    const state = JSON.stringify(pinia.state.value); .return[renderedHtml, state, preloadLinks]; }...Copy the code
  1. Modify theserver-dev.jsserver-prod.js
.const [renderedHtml, state, preloadLinks] = await render(ctx, {});

const html = template
     .replace('<! --app-html-->', renderedHtml)
     .replace('<! --pinia-state-->', state);
    // server-prod.js
    .replace('<! --preload-links-->', preloadLinks)

...
Copy the code
  1. toindex.htmladd<! --pinia-state-->tag
<script>
    window.__INITIAL_STATE__ = '<! --pinia-state-->';
</script>
Copy the code
  1. runpnpm run devCan display normally

Note: The integration of Pinia due to the injection of more complex and different methods, temporarily do not explain in detail, if you need, the following will be a detailed analysis!

other

  • vueuseIntegration of: reference”How to use Vite + VUe3 + TS + Pinia + Vueuse to build enterprise level front End Project of DJIA”
  • CSS integration:Refer to the above
    • You can use:New features of native CSS variable,scssorless
  • CSS UI library:Refer to the same
    • One thing to noteAccording to the need to introduce
  • Of course, there’s a lot to consider, likePressure test.concurrent , Load balancingEtc., but these are not within the scope of the article theme, here will not do a detailed introduction, interested can leave a message, there will be time to open the corresponding column
  • Among themLoad balancingThis front end can be used by studentspm2Or just throw it over to operationsdocker

Project template address

portal

The last

Note: Vite SSR support is still in the experimental stage, may encounter some unknown bugs, so please use caution in the company production environment, personal projects can be abused

This series will be a continuous update series, about the whole “Vite from entry to master” column, I will mainly from the following several aspects, please wait and see!!

Pretty boy pretty girls, all see here, don’t click a “like” and then go 🌹🌹🌹