<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          教你使用 koa2 + vite + ts + vue3 + pinia 構(gòu)建前端 SSR 企業(yè)級(jí)項(xiàng)目

          共 15319字,需瀏覽 31分鐘

           ·

          2022-05-17 13:55


          了解 SSR

          什么是 SSR

          服務(wù)器端渲染(Server-Side Rendering)是指由服務(wù)端完成頁(yè)面的 HTML 結(jié)構(gòu)拼接的頁(yè)面處理技術(shù),發(fā)送到瀏覽器,然后為其綁定狀態(tài)與事件,成為完全可交互頁(yè)面的過程。

          簡(jiǎn)單理解就是html是由服務(wù)端寫出,可以動(dòng)態(tài)改變頁(yè)面內(nèi)容,即所謂的動(dòng)態(tài)頁(yè)面。早年的 php[1]asp[2]jsp[3] 這些 Server page 都是 SSR 的。

          為什么使用 SSR

          • 網(wǎng)頁(yè)內(nèi)容在服務(wù)器端渲染完成,一次性傳輸?shù)綖g覽器,所以 首屏加載速度非常快
          • 有利于SEO,因?yàn)榉?wù)器返回的是一個(gè)完整的 html,在瀏覽器可以看到完整的 dom,對(duì)于爬蟲、百度搜索等引擎就比較友好;

          快速查看

          github 倉(cāng)庫(kù)地址[4]

          長(zhǎng)話短說(shuō),直接開干 ~

          建議包管理器使用優(yōu)先級(jí):pnpm > yarn > npm > cnpm

          一、初始化項(xiàng)目

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

          集成基本配置

          由于本文的重點(diǎn)在于 SSR 配置,為了優(yōu)化讀者的觀感體驗(yàn),所以項(xiàng)目的基本配置就不做詳細(xì)介紹,在我上一篇文章《手把手教你用 vite+vue3+ts+pinia+vueuse 打造企業(yè)級(jí)前端項(xiàng)目》[5]中已詳細(xì)介紹,大家可以自行查閱

          1. 修改 tsconfig.json查看代碼[6]
          2. 修改 vite.config.ts查看代碼[7]
          3. 集成 eslintprettier 統(tǒng)一代碼質(zhì)量風(fēng)格的:查看教程[8]
          4. 集成 commitizenhusky 規(guī)范 git 提交:查看教程[9]

          到這里我們項(xiàng)目的基本框架都搭建完成啦~

          二、修改客戶端入口

          1. 修改 ~/src/main.ts
          import?{?createSSRApp?}?from?"vue";
          import?App?from?"./App.vue";

          //?為了保證數(shù)據(jù)的互不干擾,每次請(qǐng)求需要導(dǎo)出一個(gè)新的實(shí)例
          export?const?createApp?=?()?=>?{
          ????const?app?=?createSSRApp(App);
          ????return?{?app?};
          }

          2.新建 ~/src/entry-client.ts

          import?{?createApp?}?from?"./main"

          const?{?app?}?=?createApp();

          app.mount("#app");

          3.修改 ~/index.html 的入口

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

          到這里你運(yùn)行 pnpm run dev ,發(fā)現(xiàn)頁(yè)面中還是可以正常顯示,因?yàn)榈侥壳爸皇亲隽艘粋€(gè)文件的拆分,以及更換了 createSSRApp 方法;

          三、創(chuàng)建開發(fā)服務(wù)器

          使用 Koa2

          1.安裝 koa2

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

          2.安裝中間件 koa-connect

          pnpm?i?koa-connect?--save

          3.使用:新建 ~/server.js

          備注:因?yàn)樵撐募?node 運(yùn)行入口,所以用 js 即可,如果用 ts 文件,需單獨(dú)使用 ts-node 等去運(yùn)行,導(dǎo)致程序變復(fù)雜

          const?Koa?=?require('koa');

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

          ????app.use(async?(ctx)?=>?{
          ????????ctx.body?=?`
          ??????
          ????????koa2?+?vite?+?ts?+?vue3?+?vue-router
          ????????
          ??????????使用?koa2?+?vite?+?ts?+?vue3?+?vue-router?集成前端?SSR?企業(yè)級(jí)項(xiàng)目
          ????????
          ??????`
          ;
          ????});

          ????app.listen(9000,?()?=>?{
          ????????console.log('server?is?listening?in?9000');
          ????});
          })();

          4.運(yùn)行 node server.js5.結(jié)果:

          Untitled.png

          渲染替換成項(xiàng)目根目錄下的 index.html

          1.修改 server.js 中的 ctx.body 返回的是 index.html

          ?const?fs?=?require('fs');
          ?const?path?=?require('path');
          ?
          ?const?Koa?=?require('koa');
          ?
          ?(async?()?=>?{
          ?????const?app?=?new?Koa();
          ?
          ?????//?獲取?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.運(yùn)行 node server.js后, 我們就會(huì)看到返回的是空白內(nèi)容的 index.html 了,但是我們需要返回的是 vue 模板 ,那么我們只需要做個(gè) 正則的替換

          3.給 index.html 添加 標(biāo)記

          ?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?+?vue3title>
          ???head>
          ???<body>
          ?????<div?id="app">div>
          ?????<script?type="module"?src="/src/entry-client.ts">script>
          ???body>
          ?html>

          4.修改 server.js 中的 ctx.body

          //?other?code?...

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

          ????//?獲取index.html
          ????const?template?=?fs.readFileSync(path.resolve(__dirname,?'index.html'),?'utf-8');

          ????app.use(async?(ctx)?=>?{
          ????????let?vueTemplate?=?'現(xiàn)在假裝這是一個(gè)vue模板';

          ????????//?替換?index.html?中的??標(biāo)記
          ????????let?html?=?template.replace('',?vueTemplate);

          ????????ctx.body?=?html;
          ????});

          ????app.listen(9000,?()?=>?{
          ????????console.log('server?is?listening?in?9000');
          ????});
          })();

          5.運(yùn)行 node server.js后,我們就會(huì)看到返回的 變量 vueTemplate 內(nèi)容

          那么到現(xiàn)在服務(wù)已正常啟動(dòng)了,但是我們?cè)囅胍幌拢覀冺?yè)面模板使用的是 vue,并且 vue 返回的是一個(gè) vue 實(shí)例模板,所以我就要把這個(gè) vue 實(shí)例模板 轉(zhuǎn)換成 可渲染的 html,那么 @vue/server-renderer 就應(yīng)運(yùn)而生了

          四、新增服務(wù)端入口

          因?yàn)?vue 返回的是 vue 實(shí)例模板 而不是 可渲染的 html ,所以我們需要使用 @vue/server-renderer 進(jìn)行轉(zhuǎn)換

          1.安裝 @vue/server-renderer

          pnpm?i?@vue/server-renderer?--save

          2.新建 ~/src/entry-server.ts

          import?{?createApp?}?from?'./main';
          import?{?renderToString?}?from?'@vue/server-renderer';

          export?const?render?=?async?()?=>?{
          ??const?{?app?}?=?createApp();
          ?
          ??//?注入vue?ssr中的上下文對(duì)象
          ??const?renderCtx:?{modules?:?string[]}?=?{}

          ??let?renderedHtml?=?await?renderToString(app,?renderCtx)

          ??return?{?renderedHtml?};
          }

          那么如何去使用 entry-server.ts 呢,到這里就需要 vite

          五、注入 vite

          1.修改 ~/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();

          ????//?創(chuàng)建?vite?服務(wù)
          ????const?viteServer?=?await?vite.createServer({
          ????????root:?process.cwd(),
          ????????logLevel:?'error',
          ????????server:?{
          ????????middlewareMode:?true,
          ????????},
          ????})
          ????
          ????//?注冊(cè)?vite?的?Connect?實(shí)例作為中間件(注意:vite.middlewares?是一個(gè)?Connect?實(shí)例)
          ????app.use(koaConnect(viteServer.middlewares))

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

          ????????????//?2.?應(yīng)用?Vite?HTML?轉(zhuǎn)換。這將會(huì)注入?Vite?HMR?客戶端,
          ????????????template?=?await?viteServer.transformIndexHtml(ctx.path,?template)

          ????????????//?3.?加載服務(wù)器入口,?vite.ssrLoadModule?將自動(dòng)轉(zhuǎn)換
          ????????????const?{?render?}?=?await?viteServer.ssrLoadModule('/src/entry-server.ts')

          ????????????//??4.?渲染應(yīng)用的?HTML
          ????????????const?{?renderedHtml?}?=?await?render(ctx,?{})

          ????????????const?html?=?template.replace('',?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. 運(yùn)行 node server.js 就可以看到返回的 App.vue 模板中的內(nèi)容了,如下圖
          Untitled 1.png

          3.并且我們 右鍵查看顯示網(wǎng)頁(yè)源代碼,也會(huì)看到渲染的正常 html

          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?+?vue3title>
          ??head>
          ??<body>
          ????<div?id="app"><img?alt="Vue?logo"?src="/src/assets/logo.png"><h1?data-v-469af010>Hello?Vue?3?+?TypeScript?+?Viteh1><p?data-v-469af010>?Recommended?IDE?setup:?<a?href=""?target="_blank"?data-v-469af010>VSCodea>?+?<a?href=""?target="_blank"?data-v-469af010>Volara>p><p?data-v-469af010>See?<code?data-v-469af010>README.mdcode>?for?more?information.p><p?data-v-469af010><a?href=""?target="_blank"?data-v-469af010>?Vite?Docs?a>?|?<a?href=""?target="_blank"?data-v-469af010>Vue?3?Docsa>p><button?type="button"?data-v-469af010>count?is:?0button><p?data-v-469af010>?Edit?<code?data-v-469af010>components/HelloWorld.vuecode>?to?test?hot?module?replacement.?p>div>
          ????<script?type="module"?src="/src/entry-client.ts">script>
          ??body>
          html>

          到這里我們就已經(jīng)在 開發(fā)環(huán)境 已經(jīng)正常的渲染了,但我們想一下,在 生產(chǎn)環(huán)境 我們應(yīng)該怎么做呢,因?yàn)樵蹅儾豢赡苤苯釉?生產(chǎn)環(huán)境 運(yùn)行使用 vite 吧!

          所以咱們接下來(lái)處理如何在 生產(chǎn)環(huán)境 運(yùn)行吧

          六、添加開發(fā)環(huán)境

          為了將 SSR 項(xiàng)目可以在生產(chǎn)環(huán)境運(yùn)行,我們需要:

          1.正常構(gòu)建生成一個(gè) 客戶端構(gòu)建包; 2.再生成一個(gè) SSR 構(gòu)建,使其通過?require()?直接加載,這樣便無(wú)需再使用 Vite 的?ssrLoadModule; 3.修改 package.json

          ...
          {
          "scripts":?{
          ????//?開發(fā)環(huán)境
          ????"dev":?"node?server-dev.js",
          ????//?生產(chǎn)環(huán)境
          ????"server":?"node?server-prod.js",
          ????//?構(gòu)建
          ????"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.修改 server.jsserver-dev.js5.運(yùn)行 pnpm run build 構(gòu)建包 6.新增 server-prod.js

          注意:為了處理靜態(tài)資源,需要在此新增 koa-send 中間件: 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)?=>?{
          ????
          ????//?請(qǐng)求的是靜態(tài)資源
          ????????if?(ctx.path.startsWith('/assets'))?{
          ????????????await?sendFile(ctx,?ctx.path,?{?root:?clientRoot?});
          ????????????return;
          ????????}

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

          ????????const?html?=?template
          ????????????.replace('',?appHtml);

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

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

          到這里,我們?cè)?開發(fā)環(huán)境生成環(huán)境 已經(jīng)都可以正常訪問了,那么是不是就萬(wàn)事無(wú)憂了呢?

          為了用戶的更極致的用戶體驗(yàn),那么 預(yù)加載 就必須要安排了

          七、預(yù)加載

          我們知道 vue 組件在 html 中渲染時(shí)都是動(dòng)態(tài)去生成的對(duì)應(yīng)的 jscss 等;

          那么我們要是在用戶獲取 服務(wù)端模板 (也就是執(zhí)行 vite build 后生成的 dist/client 目錄) 的時(shí)候,直接在 html 中把對(duì)應(yīng)的 jscss 文件預(yù)渲染了,這就是 靜態(tài)站點(diǎn)生成(SSG) 的形式。

          閑話少說(shuō),明白道理了之后,直接開干 ~

          1.生成預(yù)加載指令:在 package.json 中的 build:client 添加 --ssrManifest?標(biāo)志,運(yùn)行后生成 ssr-manifest.json

          ...

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

          ...

          2.在 entry-sercer.ts 中添加解析生成的 ssr-manifest.json 方法

          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];
          };

          /**
          ?*?解析需要預(yù)加載的鏈接
          ?*?@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;
          }

          /**
          ?*?預(yù)加載的對(duì)應(yīng)的地址
          ?*?下面的方法只針對(duì)了?js?和?css,如果需要處理其它文件,自行添加即可
          ?*?@param?file
          ?*?@returns?string
          ?*/

          function?renderPreloadLink(file:?string):?string?{
          ????if?(file.endsWith('.js'))?{
          ????????return?`${file}">`;
          ????}?else?if?(file.endsWith('.css'))?{
          ????????return?`${file}">`;
          ????}?else?{
          ????????return?'';
          ????}
          }

          3.給 index.html 添加 標(biāo)記 4.改造 server-prod.js


          ...

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

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

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

          ????????const?html?=?template
          ????????????.replace('',?preloadLinks)
          ????????????.replace('',?appHtml);

          ????????//?do?something
          ????});

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

          5.運(yùn)行 pnpm run build && pnpm run serve 就可正常顯示了

          到這里基本的渲染就完成了,因?yàn)槲覀兪切枰跒g覽器上渲染的,所以 路由 vue-router 就必不可少了

          八、集成 vue-router

          1.安裝 vue-router

          pnpm?i?vue-router?--save

          2.新增對(duì)應(yīng)的路由頁(yè)面 index.vuelogin.vueuser.vue3.新增 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:?'首頁(yè)',
          ????????????????????keepAlive:?true,
          ????????????????????requireAuth:?true
          ????????????????},
          ????????????????component:?()?=>?import('@/pages/index.vue')
          ????????????},
          ????????????{
          ????????????????path:?'/login',
          ????????????????name:?'login',
          ????????????????meta:?{
          ????????????????????title:?'登錄',
          ????????????????????keepAlive:?true,
          ????????????????????requireAuth:?false
          ????????????????},
          ????????????????component:?()?=>?import('@/pages/login.vue')
          ????????????},
          ????????????{
          ????????????????path:?'/user',
          ????????????????name:?'user',
          ????????????????meta:?{
          ????????????????????title:?'用戶中心',
          ????????????????????keepAlive:?true,
          ????????????????????requireAuth:?true
          ????????????????},
          ????????????????component:?()?=>?import('@/pages/user.vue')
          ????????????}
          ????????]
          ????});

          4.修改入口文件 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.修改入口文件 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();

          ????//?路由注冊(cè)
          ????const?router?=?createRouter('server');
          ????app.use(router);
          ????await?router.push(ctx.path);
          ????await?router.isReady();

          ????...
          };

          ...

          6.運(yùn)行 pnpm run build && pnpm run serve 就可正常顯示了

          九、集成 pinia

          1.安裝

          pnpm?i?pinia?--save

          2.新建 src/store/user.ts

          import?{?defineStore?}?from?'pinia';

          export?default?defineStore('user',?{
          ????state:?()?=>?{
          ????????return?{
          ????????????name:?'張三',
          ????????????age:?20
          ????????};
          ????},
          ????actions:?{
          ????????updateName(name:?string)?{
          ????????????this.name?=?name;
          ????????},
          ????????updateAge(age:?number)?{
          ????????????this.age?=?age;
          ????????}
          ????}
          });


          3.新建 src/store/index.ts

          import?{?createPinia?}?from?'pinia';
          import?useUserStore?from?'./user';

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

          ????useUserStore(pinia);

          ????return?pinia;
          };


          4.新建 UsePinia.vue 使用,并且在 pages/index.vue 中引入