<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 構建前端 SSR 企業(yè)級項目

          共 13845字,需瀏覽 28分鐘

           ·

          2022-05-11 09:24

          大廠技術??高級前端??Node進階

          點擊上方?程序員成長指北,關注公眾號

          回復1,加入高級Node交流群

          前言

          大家好,我是 [1],在上一篇文章中,我們有講到《如何使用 vite+vue3+ts+pinia+vueuse 打造前端企業(yè)級項目》[2],能看的出來很多同學喜歡,今天給大家?guī)肀卧S久的 如何使用vite 打造前端 SSR 企業(yè)級項目,希望大家能喜歡!

          如果大家對 Vite 感興趣可以去看看專欄:《Vite 從入門到精通》[3]

          了解 SSR

          什么是 SSR

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

          簡單理解就是html是由服務端寫出,可以動態(tài)改變頁面內容,即所謂的動態(tài)頁面。早年的 php[4]asp[5]jsp[6] 這些 Server page 都是 SSR 的。

          為什么使用 SSR

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

          快速查看

          github 倉庫地址[7]

          長話短說,直接開干 ~

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

          一、初始化項目

          pnpm?create?vite?koa2-ssr-vue3-ts-pinia?--?--template?vue-ts
          復制代碼

          集成基本配置

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

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

          到這里我們項目的基本框架都搭建完成啦~

          二、修改客戶端入口

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

          //?為了保證數(shù)據(jù)的互不干擾,每次請求需要導出一個新的實例
          export?const?createApp?=?()?=>?{
          ????const?app?=?createSSRApp(App);
          ????return?{?app?};
          }
          復制代碼
          1. 新建 `~/src/entry-client.ts`
          import?{?createApp?}?from?"./main"

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

          app.mount("#app");
          復制代碼
          1. 修改 `~/index.html` 的入口
          html>
          <html?lang="en">

          ????...

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

          ????...

          html>
          復制代碼

          到這里你運行 pnpm run dev ,發(fā)現(xiàn)頁面中還是可以正常顯示,因為到目前只是做了一個文件的拆分,以及更換了 createSSRApp 方法;

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

          使用 Koa2

          1. 安裝 `koa2`
          pnpm?i?koa?--save?&&?pnpm?i?@types/koa?--save-dev
          復制代碼
          1. 安裝中間件 `koa-connect`
          pnpm?i?koa-connect?--save
          復制代碼
          1. 使用:新建 ~/server.js

          備注:因為該文件為 node 運行入口,所以用 js 即可,如果用 ts 文件,需單獨使用 ts-node 等去運行,導致程序變復雜

          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è)級項目
          ????????
          ??????`
          ;
          ????});

          ????app.listen(9000,?()?=>?{
          ????????console.log('server?is?listening?in?9000');
          ????});
          })();
          復制代碼
          1. 運行 node server.js
          2. 結果:
          Untitled.png

          渲染替換成項目根目錄下的 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');
          ?????});
          ?})();
          復制代碼
          1. 運行 `node server.js`后, 我們就會看到返回的是空白內容的 `index.html` 了,但是我們需要返回的是 `vue 模板` ,那么我們只需要做個 `正則的替換`
          2. 給 `index.html` 添加 `` 標記
          ?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>
          復制代碼
          1. 修改 `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)在假裝這是一個vue模板';

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

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

          ????app.listen(9000,?()?=>?{
          ????????console.log('server?is?listening?in?9000');
          ????});
          })();
          復制代碼
          1. 運行 node server.js后,我們就會看到返回的 變量 vueTemplate 內容

          那么到現(xiàn)在服務已正常啟動了,但是我們試想一下,我們頁面模板使用的是 vue,并且 vue 返回的是一個 vue 實例模板,所以我就要把這個 vue 實例模板 轉換成 可渲染的 html,那么 @vue/server-renderer 就應運而生了

          四、新增服務端入口

          因為 vue 返回的是 vue 實例模板 而不是 可渲染的 html ,所以我們需要使用 @vue/server-renderer 進行轉換

          1. 安裝 `@vue/server-renderer`
          pnpm?i?@vue/server-renderer?--save
          復制代碼
          1. 新建 `~/src/entry-server.ts`
          import?{?createApp?}?from?'./main';
          import?{?renderToString?}?from?'@vue/server-renderer';

          export?const?render?=?async?()?=>?{
          ??const?{?app?}?=?createApp();
          ?
          ??//?注入vue?ssr中的上下文對象
          ??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?服務
          ????const?viteServer?=?await?vite.createServer({
          ????????root:?process.cwd(),
          ????????logLevel:?'error',
          ????????server:?{
          ????????middlewareMode:?true,
          ????????},
          ????})
          ????
          ????//?注冊 vite 的 Connect 實例作為中間件(注意:vite.middlewares 是一個 Connect 實例)
          ????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. 應用 Vite HTML 轉換。這將會注入 Vite HMR 客戶端,
          ????????????template?=?await?viteServer.transformIndexHtml(ctx.path,?template)

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

          ????????????//??4.?渲染應用的?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. 運行 node server.js 就可以看到返回的 App.vue 模板中的內容了,如下圖
          Untitled 1.png
          1. 并且我們 `右鍵查看顯示網(wǎng)頁源代碼`,也會看到渲染的正常 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)境 我們應該怎么做呢,因為咱們不可能直接在 生產(chǎn)環(huán)境 運行使用 vite 吧!

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

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

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

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

          {
          "scripts":?{
          ????//?開發(fā)環(huán)境
          ????"dev":?"node?server-dev.js",
          ????//?生產(chǎn)環(huán)境
          ????"server":?"node?server-prod.js",
          ????//?構建
          ????"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",
          ??},
          }

          ...

          復制代碼
          1. 修改 server.jsserver-dev.js
          2. 運行 pnpm run build 構建包
          3. 新增 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)?=>?{
          ????
          ????//?請求的是靜態(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'));
          })();
          復制代碼

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

          為了用戶的更極致的用戶體驗,那么 預加載 就必須要安排了

          七、預加載

          我們知道 vue 組件在 html 中渲染時都是動態(tài)去生成的對應的 jscss 等;

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

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

          1. `生成預加載指令`:在 package.json 中的 `build:client` 添加 `--ssrManifest`?標志,運行后生成 `ssr-manifest.json`
          ...

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

          ...
          復制代碼
          1. 在 `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];
          };

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

          /**
          ?*?預加載的對應的地址
          ?*?下面的方法只針對了?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?'';
          ????}
          }
          復制代碼
          1. 給 `index.html` 添加 `` 標記
          2. 改造 `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'));
          })();
          復制代碼
          1. 運行 pnpm run build && pnpm run serve 就可正常顯示了

          到這里基本的渲染就完成了,因為我們是需要在瀏覽器上渲染的,所以 路由 vue-router 就必不可少了

          八、集成 vue-router

          1. 安裝 vue-router
          pnpm?i?vue-router?--save
          復制代碼
          1. 新增對應的路由頁面 `index.vue` 、 `login.vue` 、 `user.vue`
          2. 新增 `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:?'首頁',
          ????????????????????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')
          ????????????}
          ????????]
          ????});
          復制代碼
          1. 修改入口文件 `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);
          });
          復制代碼
          1. 修改入口文件 `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();

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

          ????...
          };

          ...
          復制代碼
          1. 運行 pnpm run build && pnpm run serve 就可正常顯示了

          九、集成 pinia

          1. 安裝
          pnpm?i?pinia?--save
          復制代碼
          1. 新建 `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;
          ????????}
          ????}
          });

          復制代碼
          1. 新建 `src/store/index.ts`
          import?{?createPinia?}?from?'pinia';
          import?useUserStore?from?'./user';

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

          ????useUserStore(pinia);

          ????return?pinia;
          };

          復制代碼
          1. 新建 `UsePinia.vue` 使用,并且在 `pages/index.vue` 中引入