Teach you how to use koa2 + vite + TS + vue3 + Pinia to build front-end SSR enterprise projects

zz_ jesse 2022-05-14 13:50:45 阅读数:180

teachusekoa2koavite

understand SSR

What is? SSR

Server-side rendering (Server-Side Rendering) It refers to the completion of the page by the server HTML Page processing technology of structural splicing , Send to browser , Then bind the state and event , The process of becoming a fully interactive page .

The simple understanding is html It is written by the server , You can dynamically change the page content , The so-called dynamic page . early php[1]、asp[2] 、jsp[3] these Server page All are SSR Of .

Why use SSR

  • The web page content is rendered on the server side , One time transfer to browser , therefore The loading speed of the first screen is very fast ;

  • advantageous to SEO, Because the server returns a complete html, In the browser, you can see the complete dom, For reptiles 、 Baidu search and other engines are more friendly ;

Take a quick look at

github Warehouse address [4]

Cut a long story short , Let's go straight to work ~

It is recommended that the package manager use priority :pnpm > yarn > npm > cnpm

One 、 Initialize project

pnpm create vite koa2-ssr-vue3-ts-pinia -- --template vue-ts

Integrated basic configuration

Because the focus of this paper is SSR To configure , In order to optimize the reader's visual experience , So the Basic configuration Just don't give a detailed introduction , In my last article 《 I'll teach you how to use vite+vue3+ts+pinia+vueuse Build an enterprise level front-end project 》[5] This is described in detail in , You can check it by yourself

  1. modify tsconfig.json : Look at the code [6]

  2. modify vite.config.ts: Look at the code [7]

  3. Integrate eslint and prettier Unified code quality style : You can view the tutorial [8]

  4. Integrate commitizen and husky standard git Submit : You can view the tutorial [9]

So far, the basic framework of our project has been built ~

Two 、 Modify the client entry

  1. modify ~/src/main.ts

import { createSSRApp } from "vue";
import App from "./App.vue";
//  In order to ensure that the data do not interfere with each other , Each request needs to export a new instance
export const createApp = () => {
    const app = createSSRApp(App);
    return { app };
}

2. newly build ~/src/entry-client.ts

import { createApp } from "./main"
const { app } = createApp();
app.mount("#app");

3. modify ~/index.html Entrance

<!DOCTYPE html>
<html lang="en">
    ...
    <script type="module" src="/src/entry-client.ts"></script>
    ...
</html>

Here you run pnpm run dev , It is found that the page can still be displayed normally , Because so far, only one file has been split , And replaced createSSRApp Method ;

3、 ... and 、 Create a development server

Use Koa2

1. install koa2

pnpm i koa --save && pnpm i @types/koa --save-dev

2. Install middleware koa-connect

pnpm i koa-connect --save

3. Use : newly build ~/server.js

remarks : Because the file is node Operation entrance , So use js that will do , If you use ts file , Need to be used alone ts-node Wait to run , Cause the program to become complex

const Koa = require('koa');
(async () => {
    const app = new Koa();
    app.use(async (ctx) => {
        ctx.body = `<!DOCTYPE html>
      <html lang="en">
        <head><title>koa2 + vite + ts + vue3 + vue-router</title></head>
        <body>
          <h1 style="text-align: center;"> Use  koa2 + vite + ts + vue3 + vue-router  Integrated front end  SSR  Enterprise projects </h1>
        </body>
      </html>`;
    });
    app.listen(9000, () => {
        console.log('server is listening in 9000');
    });
})();

4. function node server.js5. result :

c50ab41cb8591da4ec9234b99832053b.png
Untitled.png

Replace the rendering with... In the root directory of the project index.html

1. modify server.js Medium ctx.body The return is index.html

const fs = require('fs');
 const path = require('path');
 
 const Koa = require('koa');
 
 (async () => {
     const app = new Koa();
 
     //  obtain  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');
     });
 })();

2. function node server.js after , We will see that the returned content is blank index.html 了 , But what we need to return is vue Templates , Then we just need to do Regular substitution

3. to index.html add to <!--app-html--> Mark

<!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>koa2 + vite + ts + vue3</title>
   </head>
   <body>
     <div id="app"><!--app-html--></div>
     <script type="module" src="/src/entry-client.ts"></script>
   </body>
 </html>

4. modify server.js Medium ctx.body

// other code ...
(async () => {
    const app = new Koa();
    //  obtain index.html
    const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
    app.use(async (ctx) => {
        let vueTemplate = '<h1 style="text-align:center;"> Now pretend it's a vue Templates </h1>';
        //  Replace  index.html  Medium  <!--app-html-->  Mark
        let html = template.replace('<!--app-html-->', vueTemplate);
        ctx.body = html;
    });
    app.listen(9000, () => {
        console.log('server is listening in 9000');
    });
})();

5. function node server.js after , We'll see the return Variable vueTemplate Content

So now the service has started normally , But let's imagine , Our page template uses vue, also vue Back to a vue Instance template , So I'm going to put this vue Instance template convert to Renderable html, that @vue/server-renderer And that's what happened

Four 、 Add a new server entrance

because vue The return is vue Instance template instead of Renderable html , So we need to use @vue/server-renderer convert

1. install @vue/server-renderer

pnpm i @vue/server-renderer --save

2. newly build ~/src/entry-server.ts

import { createApp } from './main';
import { renderToString } from '@vue/server-renderer';
export const render = async () => {
  const { app } = createApp();
 
  //  Inject vue ssr Context object in
  const renderCtx: {modules?: string[]} = {}
  let renderedHtml = await renderToString(app, renderCtx)
  return { renderedHtml };
}

So how to use entry-server.ts Well , To get here, you need vite

5、 ... and 、 Inject vite

1. modify ~/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();
    //  establish  vite  service
    const viteServer = await vite.createServer({
        root: process.cwd(),
        logLevel: 'error',
        server: {
        middlewareMode: true,
        },
    })
    
    //  register  vite  Of  Connect  Instance as middleware ( Be careful :vite.middlewares  It's a  Connect  example )
    app.use(koaConnect(viteServer.middlewares))
    app.use(async ctx => {
        try {
            // 1.  obtain index.html
            let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
            // 2.  application  Vite HTML  transformation . This will inject  Vite HMR  client ,
            template = await viteServer.transformIndexHtml(ctx.path, template)
            // 3.  Load server entry , vite.ssrLoadModule  Will automatically convert
            const { render } = await viteServer.ssrLoadModule('/src/entry-server.ts')
            //  4.  Render the of the application  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');
    });
})()
  1. function node server.js You can see the return App.vue The content in the template , Here's the picture

90b1da52dece89e6f2ec11f12f8f34dc.png
Untitled 1.png

3. And we Right click to view the source code of the displayed web page , You will also see the normal rendering html

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

We are already here development environment It has been rendered normally , But let's think , stay Production environment What should we do , Because we can't be directly in Production environment Run using vite Well !

So let's deal with how to Production environment Run it

6、 ... and 、 Add a development environment

In order to SSR The project can run in a production environment , We need to :

1. Normal build generates a Client build package ; 2. Regenerate into one SSR structure , Make it pass  require()  Direct load , This eliminates the need to use Vite Of  ssrLoadModule; 3. modify package.json

...
{
"scripts": {
    //  development environment
    "dev": "node server-dev.js",
    //  Production environment
    "server": "node server-prod.js",
    //  structure
    "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",
  },
}
...

4. modify server.js by server-dev.js5. function pnpm run build Build packages 6. newly added server-prod.js

Be careful : To handle static resources , You need to add koa-send middleware : 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) => {
    
    //  Static resources are 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'));
})();

Come here , We are development environment and Generating environment You can access it normally , So is everything safe ?

For the ultimate user experience , that Preloading It has to be arranged

7、 ... and 、 Preloading

We know vue Components stay html When rendering, it is generated dynamically js and css etc. ;

So if we get from users Server template ( Which is execution vite build Post generated dist/client Catalog ) When , Directly in html Put the corresponding js and css The file is pre rendered , This is it. Static site generation (SSG) In the form of .

Gossip , After understanding the truth , Let's go straight to work ~

1. Generate preload instructions : stay package.json Medium build:client add to --ssrManifest  sign , Build after run ssr-manifest.json

...
{
"scripts": {
    ...
    "build:client": "vite build --ssrManifest --outDir dist/client",
    ...
  },
}
...

2. stay entry-sercer.ts Add parsing generated ssr-manifest.json Method

export const render = async (
    ctx: ParameterizedContext,
    manifest: Record<string, string[]>
): Promise<[string, string]> => {
    const { app } = createApp();
    console.log(ctx, manifest, '');
    const renderCtx: { modules?: string[] } = {};
    const renderedHtml = await renderToString(app, renderCtx);
    const preloadLinks = renderPreloadLinks(renderCtx.modules, manifest);
    return [renderedHtml, preloadLinks];
};
/**
 *  Resolve 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();
    if (modules === undefined) throw new Error();
    modules.forEach((id) => {
        const files = manifest[id];
        if (files) {
            files.forEach((file) => {
                if (!seen.has(file)) {
                    seen.add(file);
                    links += renderPreloadLink(file);
                }
            });
        }
    });
    return links;
}
/**
 *  Preload the corresponding address
 *  The following method is only for  js  and  css, If other files need to be processed , Add it 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 '';
    }
}

3. to index.html add to <!--preload-links--> Mark 4. reform server-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'));
})();

5. function pnpm run build && pnpm run serve It can be displayed normally

Here, the basic rendering is completed , Because we need to render on the browser , therefore route vue-router It's necessary

8、 ... and 、 Integrate vue-router

1. install vue-router

pnpm i vue-router --save

2. Add the corresponding routing page index.vuelogin.vueuser.vue3. newly added src/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 page ',
                    keepAlive: true,
                    requireAuth: true
                },
                component: () => import('@/pages/index.vue')
            },
            {
                path: '/login',
                name: 'login',
                meta: {
                    title: ' Sign in ',
                    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')
            }
        ]
    });

4. Modify entry file src/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);
});

5. Modify entry file src/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);
    await router.isReady();
    ...
};
...

6. function pnpm run build && pnpm run serve It can be displayed normally

Nine 、 Integrate pinia

1. install

pnpm i pinia --save

2. newly build src/store/user.ts

import { defineStore } from 'pinia';
export default defineStore('user', {
    state: () => {
        return {
            name: ' Zhang San ',
            age: 20
        };
    },
    actions: {
        updateName(name: string) {
            this.name = name;
        },
        updateAge(age: number) {
            this.age = age;
        }
    }
});

3. newly build src/store/index.ts

import { createPinia } from 'pinia';
import useUserStore from './user';
export default () => {
    const pinia = createPinia();
    useUserStore(pinia);
    return pinia;
};

4. newly build UsePinia.vue Use , And in pages/index.vue Introduction in

<template>
    <h2> Welcome to use vite+vue3+ts+pinia+vue-router4</h2>
    <div>{
{ userStore.name }} In the age of : {
{ userStore.age }}</div
    ><br />
    <button @click="addAge"> Click to {
{ userStore.name }} Increase your age by one year </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>

5. Inject pinia : modify src/entry-client.ts

...
import createStore from '@/store';
const pinia = createStore();
const { app } = createApp();
app.use(router);
app.use(pinia);
//  initialization  pini
//  Be careful :__INITIAL_STATE__ Need to be in  src/types/shims-global.d.ts In the definition of
if (window.__INITIAL_STATE__) {
    pinia.state.value = JSON.parse(window.__INITIAL_STATE__);
}
...

6. modify src/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];
}
...

7. modify server-dev.js and server-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)
...

8. to index.html add to <!--pinia-state--> Mark

<script>
    window.__INITIAL_STATE__ = '<!--pinia-state-->';
</script>

9. function pnpm run dev It can be displayed normally

remarks : Integrate pinia This piece is relatively Complex and different methods , No detailed explanation for the time being , If you need to , A detailed analysis will follow !

Ten 、 Other

  • vueuse Integration of : May refer to 《 I'll teach you how to use vite+vue3+ts+pinia+vueuse Build a large enterprise level front-end project 》[10]

  • CSS Integrate : See above [11]

    • You can use : Native css variable New characteristics scss perhaps less

  • CSS Of UI library : See ibid [12]

    • It should be noted that Introduce on demand

  • Of course, there are many things to consider , such as Pressure measurement , Concurrent , Load balancing etc. , But these are not within the scope of the topic of the article , I won't go into details here , Interested can leave a message , There will be a new corresponding column later

  • among Load balancing This front-end can be used by students pm2, Or leave it directly to the operation and maintenance department to get docker

Project template address

Portal [13]

Last

Friendship tips : at present Vite Of SSR Support is still in the experimental stage , You may encounter some unknown bug , Therefore, in the company's production environment, please use with caution , It can be abused in personal projects ~

The series will be a continuous update series , On the whole 《Vite From entry to mastery 》 special column [14], I will mainly explain from the following aspects , Please wait and see !!!

c564592f7a727e707fd0c91ac388d8c5.png
Untitled.png

Reference material

[1]

https://baike.baidu.com/item/php/9337: https://link.juejin.cn?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2Fphp%2F9337

[2]

https://baike.baidu.com/item/asp/128906: https://link.juejin.cn?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2Fasp%2F128906

[3]

https://baike.baidu.com/item/jsp/141543: https://link.juejin.cn?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2Fjsp%2F141543

[4]

https://github.com/jeddygong/vite-templates/tree/master/koa2-ssr-vite-vue3-ts-pinia: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjeddygong%2Fvite-templates%2Ftree%2Fmaster%2Fkoa2-ssr-vite-vue3-ts-pinia

[5]

https://juejin.cn/post/7079785777692934174: https://juejin.cn/post/7079785777692934174

[6]

https://github.com/jeddygong/vite-templates/blob/master/koa2-ssr-vite-vue3-ts-pinia/tsconfig.json: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjeddygong%2Fvite-templates%2Fblob%2Fmaster%2Fkoa2-ssr-vite-vue3-ts-pinia%2Ftsconfig.json

[7]

https://github.com/jeddygong/vite-templates/blob/master/koa2-ssr-vite-vue3-ts-pinia/vite.config.ts: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjeddygong%2Fvite-templates%2Fblob%2Fmaster%2Fkoa2-ssr-vite-vue3-ts-pinia%2Fvite.config.ts

[8]

https://juejin.cn/post/7079785777692934174#heading-8: https://juejin.cn/post/7079785777692934174#heading-8

[9]

https://juejin.cn/post/7079785777692934174#heading-28: https://juejin.cn/post/7079785777692934174#heading-28

[10]

https://juejin.cn/post/7079785777692934174#heading-17: https://juejin.cn/post/7079785777692934174#heading-17

[11]

https://juejin.cn/post/7079785777692934174#heading-20: https://juejin.cn/post/7079785777692934174#heading-20

[12]

https://juejin.cn/post/7079785777692934174#heading-27: https://juejin.cn/post/7079785777692934174#heading-27

[13]

https://github.com/jeddygong/vite-templates/tree/master/koa2-ssr-vite-vue3-ts-pinia: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjeddygong%2Fvite-templates%2Ftree%2Fmaster%2Fkoa2-ssr-vite-vue3-ts-pinia

[14]

https://juejin.cn/column/7074954144817086472: https://juejin.cn/column/7074954144817086472

About this article

author : Master Yi

https://juejin.cn/post/7086467466703929358

The End

版权声明:本文为[zz_ jesse]所创,转载请带上原文链接,感谢。 https://qdmana.com/2022/134/202205141326564391.html