您的位置:首頁 > 軟件教程 > 教程 > 基于React的SSG靜態(tài)站點(diǎn)渲染方案

基于React的SSG靜態(tài)站點(diǎn)渲染方案

來源:好特整理 | 時(shí)間:2024-06-04 09:46:07 | 閱讀:60 |  標(biāo)簽: T S C EA   | 分享到:

基于React的SSG靜態(tài)站點(diǎn)渲染方案 靜態(tài)站點(diǎn)生成SSG - Static Site Generation是一種在構(gòu)建時(shí)生成靜態(tài)HTML等文件資源的方法,其可以完全不需要服務(wù)端的運(yùn)行,通過預(yù)先生成靜態(tài)文件,實(shí)現(xiàn)快速的內(nèi)容加載和高度的安全性。由于其生成的是純靜態(tài)資源,便可以利用CDN等方案以更低的成

基于React的SSG靜態(tài)站點(diǎn)渲染方案

靜態(tài)站點(diǎn)生成 SSG - Static Site Generation 是一種在構(gòu)建時(shí)生成靜態(tài) HTML 等文件資源的方法,其可以完全不需要服務(wù)端的運(yùn)行,通過預(yù)先生成靜態(tài)文件,實(shí)現(xiàn)快速的內(nèi)容加載和高度的安全性。由于其生成的是純靜態(tài)資源,便可以利用 CDN 等方案以更低的成本和更高的效率來構(gòu)建和發(fā)布網(wǎng)站,在博客、知識(shí)庫、 API 文檔等場景有著廣泛應(yīng)用。

描述

在前段時(shí)間遇到了一個(gè)比較麻煩的問題,我們是主要做文檔業(yè)務(wù)的團(tuán)隊(duì),而由于對外的產(chǎn)品文檔涉及到全球很多地域的用戶,因此在 CN 以外地域的網(wǎng)站訪問速度就成了比較大的問題。雖然我們有多區(qū)域部署的機(jī)房,但是每個(gè)地域機(jī)房的數(shù)據(jù)都是相互隔離的,而實(shí)際上很多產(chǎn)品并不會(huì)做很多特異化的定制,因此文檔實(shí)際上是可以通用的,特別是提供了多語言文檔支持的情況下,各地域共用一份文檔也變得合理了起來。而即使對于 CN 和海外地區(qū)有著特異化的定制,但在海外本身的訪問也會(huì)有比較大的局限,例如假設(shè)機(jī)房部署在 US ,那么在 SG 的訪問速度同樣也會(huì)成為一件棘手的事情。

那么問題來了,如果我們需要做到各地域訪問的高效性,那么就必須要在各個(gè)地域的主要機(jī)房部署服務(wù),而各個(gè)地域又存在數(shù)據(jù)隔離的要求,那么在這種情況下我們可能需要手動(dòng)將文檔復(fù)制到各個(gè)機(jī)房部署的服務(wù)上去,這必然就是一件很低效的事情,即使某個(gè)產(chǎn)品的文檔不會(huì)經(jīng)常更新,但是這種人工處理的方式依然是會(huì)耗費(fèi)大量精力的,顯然是不可取的。而且由于我們的業(yè)務(wù)是管理各個(gè)產(chǎn)品的文檔,在加上在海外業(yè)務(wù)不斷擴(kuò)展的情況下,這類的反饋需求必然也會(huì)越來越多,那么解決這個(gè)問題就變成了比較重要的事情。

那么在這種情況下,我就忽然想到了我的博客站點(diǎn)的構(gòu)建方式,為了方便我會(huì)將博客直接通過 gh-pages 分支部署在 GitHub Pages 上,而 GitHub Pages 本身是不支持服務(wù)端部署的,也就是說我的博客站全部都是靜態(tài)資源。由此可以想到在業(yè)務(wù)中我們的文檔站也可以用類似的方式來實(shí)現(xiàn),也就是在發(fā)布文檔的時(shí)候通過 SSG 編譯的方式來生成靜態(tài)資源,那么在全部的內(nèi)容都是靜態(tài)資源的情況下,我們就可以很輕松地基于 CDN 來實(shí)現(xiàn)跨地域訪問的高效性。此外除了調(diào)度 CDN 的分發(fā)方式,我們還可以通過將靜態(tài)資源發(fā)布到業(yè)務(wù)方申請的代碼倉庫中,然后業(yè)務(wù)方就可以自行部署服務(wù)與資源了,通過多機(jī)房部署同樣可以解決跨地域訪問的問題。

當(dāng)然,因?yàn)橐紤]到各種問題以及現(xiàn)有部署方式的兼容,在我們的業(yè)務(wù)中通過 SSG 來單獨(dú)部署實(shí)現(xiàn)跨地域的高效訪問并不太現(xiàn)實(shí),最終大概率還是要走合規(guī)的各地域數(shù)據(jù)同步方案來保證數(shù)據(jù)的一致性與高效訪問。但是在思考通過 SSG 來作為這個(gè)問題的解決方案時(shí),我還是很好奇如何在 React 的基礎(chǔ)上來實(shí)現(xiàn) SSG 渲染的,畢竟我的博客就可以算是基于 Mdx SSG 渲染。最開始我把這個(gè)問題想的特別復(fù)雜,但是在實(shí)現(xiàn)的時(shí)候發(fā)現(xiàn)只是實(shí)現(xiàn)基本原理的話還是很粗暴的解決方案,在渲染的時(shí)候并沒有想象中要處理得那么精細(xì),當(dāng)然實(shí)際上要做完整的方案特別是要實(shí)現(xiàn)一個(gè)框架也不是那么容易的事情,對于數(shù)據(jù)的處理與渲染要做很多方面的考量。

在我們正式開始聊 SSG 的基本原理前,我們可以先來看一下通過 SSG 實(shí)現(xiàn)靜態(tài)站點(diǎn)的特點(diǎn):

  • 訪問速度快: 靜態(tài)網(wǎng)站只是一組預(yù)先生成的 HTML 、 CSS 、 JavaScript Image 等靜態(tài)文件,沒有運(yùn)行在服務(wù)器上的動(dòng)態(tài)語言程序,在部署于 CDN 的情況下,用戶可以直接通過邊緣節(jié)點(diǎn)高效獲取資源,可以減少加載時(shí)間并增強(qiáng)用戶體驗(yàn)。
  • 部署簡單: 靜態(tài)網(wǎng)站可以在任何托管服務(wù)上運(yùn)行,例如 GitHub Pages Vercel 等,我們只需要傳輸文件即可,無需處理服務(wù)器配置和數(shù)據(jù)庫管理等,如果借助 Git 版本控制和 CI/CD 工具等,還可以比較輕松地實(shí)現(xiàn)自動(dòng)化部署。
  • 資源占用低: 靜態(tài)網(wǎng)站只需要非常少的服務(wù)器資源,這使得其可以在低配置的環(huán)境中運(yùn)行,我們可以在較低配置的服務(wù)器上借助 Nginx 輕松支撐 10k+ QPS 網(wǎng)站訪問。
  • SEO 優(yōu)勢: 靜態(tài)網(wǎng)站通常對搜索引擎優(yōu)化 SEO 更加友好,預(yù)渲染的頁面可以擁有完整的 HTML 標(biāo)簽結(jié)構(gòu),并且通過編譯可以使其盡可能符合語義化結(jié)構(gòu),這樣使得搜索引擎的機(jī)器人更容易抓取和索引。

那么同樣的,通過 SSG 生成的靜態(tài)資源站點(diǎn)也有一些局限性:

  • 實(shí)時(shí)性不強(qiáng): 由于靜態(tài)站點(diǎn)需要提前生成,因此就無法像動(dòng)態(tài)網(wǎng)站一樣根據(jù)實(shí)時(shí)的請求生成對應(yīng)的內(nèi)容,例如當(dāng)我們發(fā)布了新文檔之后,就必須要重新進(jìn)行增量編譯甚至是全站全量編譯,那么在編譯期間就無法訪問到最新的內(nèi)容。
  • 不支持動(dòng)態(tài)交互: 靜態(tài)站點(diǎn)通常只是靜態(tài)資源的集合,因此在一些動(dòng)態(tài)交互的場景下就無法實(shí)現(xiàn),例如用戶登錄、評(píng)論等功能,當(dāng)然這些功能可以通過客戶端渲染時(shí)動(dòng)態(tài)支持,那么這種情況就不再是純粹的靜態(tài)站點(diǎn),通常是借助 SSG 來實(shí)現(xiàn)更好的首屏和 SEO 效果。

綜上所述, SSG 更適用于生成內(nèi)容較為固定、不需要頻繁更新、且對于數(shù)據(jù)延遲敏感較低的的項(xiàng)目,并且實(shí)際上我們可能也只是選取部分能力來優(yōu)化首屏等場景,最終還是會(huì)落到 CSR 來實(shí)現(xiàn)服務(wù)能力。因此當(dāng)我們要選擇渲染方式的時(shí)候,還是要充分考慮到業(yè)務(wù)場景,由此來確定究竟是 CSR - Client Side Render 、 SSR - Server Side Render SSG - Static Site Generation 更適合我們的業(yè)務(wù)場景,甚至在一些需要額外優(yōu)化的場景下, ISR - Incremental Static Regeneration 、 DPR - Distributed Persistent Rendering 、 ESR - Edge Side Rendering 等也可以考慮作為業(yè)務(wù)上的選擇。

當(dāng)然,回到最初我們提到的問題上,假如我們只是為了靜態(tài)資源的同步,通過 CDN 來解決全球跨地域訪問的問題,那么實(shí)際上并不是一定需要完全的 SSG 來解決問題。將 CSR 完全轉(zhuǎn)變?yōu)? SSR 畢竟是一件改造范圍比較大的事情,而我們的目標(biāo)僅僅是一處生產(chǎn)、多處消費(fèi),因此我們可以轉(zhuǎn)過來想一想實(shí)際上 JSON 文件也是屬于靜態(tài)資源的一種類型,我們可以直接在前端發(fā)起請求將 JSON 文件作為靜態(tài)資源請求到瀏覽器并且借助 SDK 渲染即可,至于一些交互行為例如點(diǎn)贊等功能的速度問題我們也是可以接受的,文檔站最的主要行為還是閱讀文檔。此外對于 md 文件我們同樣可以如此處理,例如 docsify 就是通過動(dòng)態(tài)請求,但是同樣的對于搜索引擎來說這些需要執(zhí)行 Js 來動(dòng)態(tài)請求的內(nèi)容并沒有那么容易抓取,所以如果想比較好地實(shí)現(xiàn)這部分能力還是需要不斷優(yōu)化迭代。

那么接下來我們就從基本原理開始,優(yōu)化組件編譯的方式,進(jìn)而基于模版渲染生成 SSG ,文中相關(guān) API 的調(diào)用基于 React 17.0.2 版本實(shí)現(xiàn),內(nèi)容相關(guān)的 DEMO 地址為 https://github.com/WindrunnerMax/webpack-simple-environment/tree/master/packages/react-render-ssg 。

基本原理

通常當(dāng)我們使用 React 進(jìn)行客戶端渲染 CSR 時(shí),只需要在入口的 index.html 文件中置入

的獨(dú)立 DOM 節(jié)點(diǎn),然后在引入的 xxx.js 文件中通過 ReactDOM.render 方法將 React 組件渲染到這個(gè) DOM 節(jié)點(diǎn)上即可。將內(nèi)容渲染完成之后,我們就會(huì)在某些生命周期或者 Hooks 中發(fā)起請求,用以動(dòng)態(tài)請求數(shù)據(jù)并且渲染到頁面上,此時(shí)便完成了組件的渲染流程。

那么在前邊我們已經(jīng)聊了比較多的 SSG 內(nèi)容,那么可以明確對于渲染的主要內(nèi)容而言我們需要將其離線化,因此在這里就需要先解決第一個(gè)問題,如何將數(shù)據(jù)離線化,而不是在瀏覽器渲染頁面之后再動(dòng)態(tài)獲取。很明顯在前邊我們提到的將數(shù)據(jù)從數(shù)據(jù)庫請求出來之后寫入 json 文件就是個(gè)可選的方式,我們可以在代碼構(gòu)建的時(shí)候請求數(shù)據(jù),在此時(shí)將其寫入文件,在最后一并上傳到 CDN 即可。

在我們的離線數(shù)據(jù)請求問題解決后,我們就需要來看渲染問題了,前邊也提到了類似的問題,如果依舊按照之前的渲染思路,而僅僅是將數(shù)據(jù)請求的地址從服務(wù)端接口替換成了靜態(tài)資源地址,那么我們就無法做到 SEO 以及更快的首屏體驗(yàn)。其實(shí)說到這里還有一個(gè)比較有趣的事情,當(dāng)我們用 SSR 的時(shí)候,假如我們的組件是 dynamic 引用的,那么 Next 在輸出 HTML 的時(shí)候會(huì)將數(shù)據(jù)打到 HTML `); await fs.writeFile(`dist/${jsPathName}`, PRESET); await fs.writeFile(`dist/index.html`, html);

至此我們完成了最基本的 SSG 構(gòu)建流程,接下來就可以通過靜態(tài)服務(wù)器訪問資源了,在這部分 DEMO 可以直接通過 ts-node 構(gòu)建以及 anywhere 預(yù)覽靜態(tài)資源地址。實(shí)際上當(dāng)前很多開源的靜態(tài)站點(diǎn)搭建框架例如 VitePress 、 RsPress 等等都是采用類似的原理,都是在服務(wù)端生成 HTML 、 Js 、 CSS 等等靜態(tài)文件,然后在客戶端由各自的框架重新接管 DOM 的行為,當(dāng)然這些框架的集成度很高,對于相關(guān)庫的復(fù)用程度也更高。而針對于更復(fù)雜的應(yīng)用場景,還可以考慮 Next 、 Gatsby 等框架實(shí)現(xiàn),這些框架在 SSG 的基礎(chǔ)上還提供了更多的能力,對于更復(fù)雜的應(yīng)用場景也有著更好的支持。

組件編譯

雖然在前邊我們已經(jīng)實(shí)現(xiàn)了最基本的 SSG 原理,但是很明顯我們?yōu)榱俗詈喕貙?shí)現(xiàn)原理人工處理了很多方面的內(nèi)容,例如在上述我們輸出到 Js 文件的代碼中是通過 PRESET 變量定義的純字符串實(shí)現(xiàn)的代碼,而且我們對于同一個(gè)組件定義了兩遍,相當(dāng)于在服務(wù)端和客戶端分開定義了運(yùn)行的代碼,那么很明顯這樣的方式并不太合理,接下來我們就需要解決這個(gè)問題。

那么我們首先需要定義一個(gè)公共的 App 組件,在該組件的代碼實(shí)現(xiàn)中與前邊的基本原理中一致,這個(gè)組件會(huì)共享在服務(wù)端的 HTML 生成和客戶端的 React Hydrate ,而且為了方便外部的模塊導(dǎo)入組件,我們通常都是通過 export default 的方式默認(rèn)導(dǎo)出整個(gè)組件。

// packages/react-render-ssg/src/rollup/app.tsx
import React from "react";

const App = () => (
  
    
React Render SSG
); export default App;

緊接著我們先來處理客戶端的 React Hydrate ,在先前我們是通過人工維護(hù)的編輯的字符串來定義的,而實(shí)際上我們同樣可以打包工具在 Node 端將組建編譯出來,以此來輸出 Js 代碼文件。在這里我們選擇使用 Rollup 來打包 Hydrate 內(nèi)容,我們以 app.tsx 作為入口,將整個(gè)組件作為 iife 打包,然后將輸出的內(nèi)容寫入 APP_NAME ,然后將實(shí)際的 hydrate 置入 footer ,就可以完成在客戶端的 React 接管 DOM 執(zhí)行了。

// packages/react-render-ssg/rollup.config.js
const APP_NAME = "ReactSSG";
const random = Math.random().toString(16).substring(7);

export default async () => {
  return {
    input: "./src/rollup/app.tsx",
    output: {
      name: APP_NAME,
      file: `./dist/${random}.js`,
      format: "iife",
      globals: {
        "react": "React",
        "react-dom": "ReactDOM",
      },
      footer: `ReactDOM.hydrate(React.createElement(${APP_NAME}), document.getElementById("root"));`,
    },
    plugins: [
      // ...
    ],
    external: ["react", "react-dom"],
  };
};

接下來我們來處理服務(wù)端的 HTML 文件生成與資源的引用,這里的邏輯與先前的基本原理中服務(wù)端生成邏輯差別并不大,只是多了通過終端調(diào)用 Rollup 打包的邏輯,同樣也是將 HTML 輸出,并且將 Js 文件引入到 HTML 中,這里需要特殊關(guān)注的是我們的 Rollup 打包時(shí)的輸出文件路徑是在這里由 --file 參數(shù)覆蓋原本的 rollup.config.js 內(nèi)置的配置。

// packages/react-render-ssg/src/rollup/index.ts
const exec = promisify(child.exec);

(async () => {
  const HTML = ReactDOMServer.renderToString(React.createElement(App));
  const template = await fs.readFile("./public/index.html", "utf-8");

  const random = Math.random().toString(16).substring(7);
  const path = "./dist/";
  const { stdout } = await exec(`npx rollup -c --file=${path + random}.js`);
  console.log("Client Compile Complete", stdout);

  const jsFileName = `${random}.js`;
  const html = template
    .replace(//, HTML)
    .replace(//, ``);
  await fs.writeFile(`${path}index.html`, html);
})();

模版渲染

當(dāng)前我們已經(jīng)復(fù)用了組件的定義,并且通過 Rollup 打包了需要在客戶端運(yùn)行的 Js 文件,不需要再人工維護(hù)輸出到客戶端的內(nèi)容。那么場景再復(fù)雜一些,假如此時(shí)我們的組件有著更加復(fù)雜的內(nèi)容,例如引用了組件庫來構(gòu)建視圖,以及引用了一些 CSS 樣式預(yù)處理器來構(gòu)建樣式,那么我們的服務(wù)端輸出 HTML 的程序就會(huì)變得更加復(fù)雜。

繼續(xù)沿著前邊的處理思路,我們在服務(wù)端的處理程序僅僅是需要將 App 組件的 HTML 內(nèi)容渲染出來,那么假設(shè)此時(shí)我們的組件引用了 @arco-design 組件庫,并且通常我們還需要引用其中的 less 文件或者 css 文件。

import "@arco-design/web-react/dist/css/arco.css";
import { Button } from "@arco-design/web-react";
// OR
import "@arco-design/web-react/es/Button/style/index";
import { Button } from "@arco-design/web-react/es/Button";

那么需要關(guān)注的是,當(dāng)前我們運(yùn)行組件的時(shí)候是在服務(wù)端環(huán)境中,那么在 Node 環(huán)境中顯然我們是不認(rèn)識(shí) .less 文件以及 .css 文件的,實(shí)際上先不說這些樣式文件, import 語法本身在 Node 環(huán)境中也是不支持的,只不過我們通常是使用 ts-node 來執(zhí)行整個(gè)運(yùn)行程序,暫時(shí)這點(diǎn)不需要關(guān)注,那么對于樣式文件我們在這里實(shí)際上是不需要的,所以我們就需要配置 Node 環(huán)境來處理這些樣式文件的引用。

require.extensions[".css"] = () => undefined;
require.extensions[".less"] = () => undefined;

但是即使這樣問題顯然沒有結(jié)束,熟悉 arco-design 的打包同學(xué)可能會(huì)清楚,當(dāng)我們引入的樣式文件是 Button/style/index 時(shí),實(shí)際上是引入了一個(gè) js 文件而不是 .less 文件,如果需要明確引入 .less 文件的話是需要明確 Button/style/index.less 文件指向的。那么此時(shí)如果我們是引入的 .less 文件,那么并不會(huì)出現(xiàn)什么問題,但是此時(shí)我們引用的是 .js 文件,而這個(gè) .js 文件中內(nèi)部的引用方式是 import ,因?yàn)榇藭r(shí)我們是通過 es 而不是 lib 部分明確引用的,即使在 tsconfig 中配置了相關(guān)解析方式為 commonjs 也是沒有用的。

{
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "esModuleInterop": true
    }
  }
}

因此我們可以看到,如果僅僅用 ts-node 來解析或者說執(zhí)行服務(wù)端的數(shù)據(jù)生成是不夠的,會(huì)導(dǎo)致我們平時(shí)實(shí)現(xiàn)組件的時(shí)候有著諸多限制,例如我們不能隨便引用 es 的實(shí)現(xiàn)而需要借助包本身的 package.json 聲明的內(nèi)容來引入內(nèi)容,如果包不能處理 commonjs 的引用那么還會(huì)束手無策。那么在這種情況下我們還是需要引入打包工具來打包 commonjs 的代碼,然后再通過 Node 來執(zhí)行輸出 HTML 。通過打包工具,我們能夠做的事情就很多了,在這里我們將資源文件例如 .less 、 .svg 都通過 null-loader 加載,且相關(guān)的配置輸出都以 commonjs 為基準(zhǔn),此時(shí)我們輸出的文件為 node-side-entry.js

// packages/react-render-ssg/rspack.server.ts
const config: Configuration = {
  context: __dirname,
  entry: {
    index: "./src/rspack/app.tsx",
  },
  externals: externals,
  externalsType: "commonjs",
  externalsPresets: {
    node: true,
  },
  // ...
  module: {
    rules: [
      { test: /\.svg$/, use: "null-loader" },
      { test: /\.less$/, use: "null-loader" },
    ],
  },
  devtool: false,
  output: {
    iife: false,
    libraryTarget: "commonjs",
    publicPath: isDev ? "" : "./",
    path: path.resolve(__dirname, ".temp"),
    filename: "node-side-entry.js",
  },
};

當(dāng)前我們已經(jīng)得到了可以在 Node 環(huán)境中運(yùn)行的組件,那么緊接著,考慮到輸出 SSG 時(shí)我們通常都需要預(yù)置靜態(tài)數(shù)據(jù),例如我們要渲染文檔的話就需要首先在數(shù)據(jù)庫中將相關(guān)數(shù)據(jù)表達(dá)查詢出來,然后作為靜態(tài)數(shù)據(jù)傳入到組件中,然后在預(yù)輸出的 HTML 中將內(nèi)容直接渲染出來,那么此時(shí)我們的 App 組件的定義就需要多一個(gè) getStaticProps 函數(shù)聲明,并且我們還引用了一些樣式文件。

// packages/react-render-ssg/src/rspack/app.tsx
import "./app.less";
import { Button } from "@arco-design/web-react";
import React from "react";

const App: React.FC<{ name: string }> = props => (
  
    
React Render SSG With {props.name}
); export const getStaticProps = () => { return Promise.resolve({ name: "Static Props", }); }; export default App;
/* packages/react-render-ssg/src/rspack/app.less */
body {
  padding: 20px;
}

同樣的,我們也需要為客戶端運(yùn)行的 Js 文件打包,只不過在這里由于我們需要處理預(yù)置的靜態(tài)數(shù)據(jù),我們在打包的時(shí)候同樣就需要預(yù)先生成模版代碼,當(dāng)我們在服務(wù)端執(zhí)行打包功能的時(shí)候,就需要將從數(shù)據(jù)庫查詢或者從文件讀取的數(shù)據(jù)放置于生成的模版文件中,然后以該文件為入口去再打包客戶端執(zhí)行的 React Hydrate 能力。在這里因?yàn)橄M麑⒛0嫖募雌饋砀忧逦,我們使用? JSON.parse 來處理預(yù)置數(shù)據(jù),實(shí)際上這里只需要將占位預(yù)留好,數(shù)據(jù)在編譯的時(shí)候經(jīng)過 stringify 直接寫入到模版文件中即可。

// packages/react-render-ssg/src/rspack/entry.tsx
/* eslint-disable @typescript-eslint/no-var-requires */

const Index = require(``);
const props = JSON.parse(``);
ReactDOM.hydrate(React.createElement(Index.default, { ...props }), document.getElementById("root"));

在模版文件生成好之后,我們就需要以這個(gè)文件作為入口調(diào)度客戶端資源文件的打包了,這里由于我們還引用了組件庫,輸出的內(nèi)容自然不光是 Js 文件,還需要將 CSS 文件一并輸出,并且我們還需要配置一些通過參數(shù)名可以控制的文件名生成、 externals 等等。這里需要注意的是,此處我們不需要使用 html-plugin HTML 文件輸出,這部分調(diào)度我們會(huì)在最后統(tǒng)一處理。

// packages/react-render-ssg/rspack.config.ts

const args = process.argv.slice(2);
const map = args.reduce((acc, arg) => {
  const [key, value] = arg.split("=");
  acc[key] = value || "";
  return acc;
}, {} as Record);
const outputFileName = map["--output-filename"];

const config: Configuration = {
  context: __dirname,
  entry: {
    index: "./.temp/client-side-entry.tsx",
  },
  externals: {
    "react": "React",
    "react-dom": "ReactDOM",
  },
  // ...
  builtins: {
    // ...
    pluginImport: [
      {
        libraryName: "@arco-design/web-react",
        customName: "@arco-design/web-react/es/{{ member }}",
        style: true,
      },
      {
        libraryName: "@arco-design/web-react/icon",
        customName: "@arco-design/web-react/icon/react-icon/{{ member }}",
        style: false,
      },
    ],
  },
  // ...
  output: {
    chunkLoading: "jsonp",
    chunkFormat: "array-push",
    publicPath: isDev ? "" : "./",
    path: path.resolve(__dirname, "dist"),
    filename: isDev
      ? "[name].bundle.js"
      : outputFileName
      ? outputFileName + ".js"
      : "[name].[contenthash].js",
    // ...
  },
};

那么此時(shí)我們就需要調(diào)度所有文件的打包過程了,首先我們需要?jiǎng)?chuàng)建需要的輸出和臨時(shí)文件夾,然后啟動(dòng)服務(wù)端 commonjs 打包的流程,輸出 node-side-entry.js 文件,并且讀取其中定義的 App 組件以及預(yù)設(shè)數(shù)據(jù)讀取方法,緊接著我們需要?jiǎng)?chuàng)建客戶端入口的模版文件,并且通過調(diào)度預(yù)設(shè)數(shù)據(jù)讀取方法將數(shù)據(jù)寫入到入口模版文件中,此時(shí)我們就可以通過打包的 commonjs 組件執(zhí)行并且輸出 HTML 了,并且客戶端運(yùn)行的 React Hydrate 代碼也可以在這里一并打包出來,最后將各類資源文件的引入一并在 HTML 中替換并且寫入到輸出文件中就可以了。至此當(dāng)我們打包完成輸出文件后,就可以使用靜態(tài)資源服務(wù)器啟動(dòng) SSG 的頁面預(yù)覽了。

const appPath = path.resolve(__dirname, "./app.tsx");
const entryPath = path.resolve(__dirname, "./entry.tsx");
require.extensions[".less"] = () => undefined;

(async () => {
  const distPath = path.resolve("./dist");
  const tempPath = path.resolve("./.temp");
  await fs.mkdir(distPath, { recursive: true });
  await fs.mkdir(tempPath, { recursive: true });

  const { stdout: serverStdout } = await exec(`npx rspack -c ./rspack.server.ts`);
  console.log("Server Compile", serverStdout);
  const nodeSideAppPath = path.resolve(tempPath, "node-side-entry.js");
  const nodeSideApp = require(nodeSideAppPath);
  const App = nodeSideApp.default;
  const getStaticProps = nodeSideApp.getStaticProps;
  let defaultProps = {};
  if (getStaticProps) {
    defaultProps = await getStaticProps();
  }

  const entry = await fs.readFile(entryPath, "utf-8");
  const tempEntry = entry
    .replace("", JSON.stringify(defaultProps))
    .replace("", appPath);
  await fs.writeFile(path.resolve(tempPath, "client-side-entry.tsx"), tempEntry);

  const HTML = ReactDOMServer.renderToString(React.createElement(App, defaultProps));
  const template = await fs.readFile("./public/index.html", "utf-8");
  const random = Math.random().toString(16).substring(7);
  const { stdout: clientStdout } = await exec(`npx rspack build -- --output-filename=${random}`);
  console.log("Client Compile", clientStdout);

  const jsFileName = `${random}.js`;
  const html = template
    .replace(//, HTML)
    .replace(//, ``)
    .replace(//, ``);
  await fs.writeFile(path.resolve(distPath, "index.html"), html);
})();

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://www.sanity.io/ssr-vs-ssg-guide
https://react.docschina.org/reference/react-dom
https://www.theanshuman.dev/articles/what-the-heck-is-ssg-static-site-generation-explained-with-nextjs-5cja
小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)