基於React的SSG靜態站點渲染方案

来源:https://www.cnblogs.com/WindrunnerMax/p/18230087
-Advertisement-
Play Games

基於React的SSG靜態站點渲染方案 靜態站點生成SSG - Static Site Generation是一種在構建時生成靜態HTML等文件資源的方法,其可以完全不需要服務端的運行,通過預先生成靜態文件,實現快速的內容載入和高度的安全性。由於其生成的是純靜態資源,便可以利用CDN等方案以更低的成 ...


基於React的SSG靜態站點渲染方案

靜態站點生成SSG - Static Site Generation是一種在構建時生成靜態HTML等文件資源的方法,其可以完全不需要服務端的運行,通過預先生成靜態文件,實現快速的內容載入和高度的安全性。由於其生成的是純靜態資源,便可以利用CDN等方案以更低的成本和更高的效率來構建和發佈網站,在博客、知識庫、API文檔等場景有著廣泛應用。

描述

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

那麼問題來了,如果我們需要做到各地域訪問的高效性,那麼就必須要在各個地域的主要機房部署服務,而各個地域又存在數據隔離的要求,那麼在這種情況下我們可能需要手動將文檔複製到各個機房部署的服務上去,這必然就是一件很低效的事情,即使某個產品的文檔不會經常更新,但是這種人工處理的方式依然是會耗費大量精力的,顯然是不可取的。而且由於我們的業務是管理各個產品的文檔,在加上在海外業務不斷擴展的情況下,這類的反饋需求必然也會越來越多,那麼解決這個問題就變成了比較重要的事情。

那麼在這種情況下,我就忽然想到了我的博客站點的構建方式,為了方便我會將博客直接通過gh-pages分支部署在GitHub Pages上,而GitHub Pages本身是不支持服務端部署的,也就是說我的博客站全部都是靜態資源。由此可以想到在業務中我們的文檔站也可以用類似的方式來實現,也就是在發佈文檔的時候通過SSG編譯的方式來生成靜態資源,那麼在全部的內容都是靜態資源的情況下,我們就可以很輕鬆地基於CDN來實現跨地域訪問的高效性。此外除了調度CDN的分發方式,我們還可以通過將靜態資源發佈到業務方申請的代碼倉庫中,然後業務方就可以自行部署服務與資源了,通過多機房部署同樣可以解決跨地域訪問的問題。

當然,因為要考慮到各種問題以及現有部署方式的相容,在我們的業務中通過SSG來單獨部署實現跨地域的高效訪問並不太現實,最終大概率還是要走合規的各地域數據同步方案來保證數據的一致性與高效訪問。但是在思考通過SSG來作為這個問題的解決方案時,我還是很好奇如何在React的基礎上來實現SSG渲染的,畢竟我的博客就可以算是基於MdxSSG渲染。最開始我把這個問題想的特別複雜,但是在實現的時候發現只是實現基本原理的話還是很粗暴的解決方案,在渲染的時候並沒有想象中要處理得那麼精細,當然實際上要做完整的方案特別是要實現一個框架也不是那麼容易的事情,對於數據的處理與渲染要做很多方面的考量。

在我們正式開始聊SSG的基本原理前,我們可以先來看一下通過SSG實現靜態站點的特點:

  • 訪問速度快: 靜態網站只是一組預先生成的HTMLCSSJavaScriptImage等靜態文件,沒有運行在伺服器上的動態語言程式,在部署於CDN的情況下,用戶可以直接通過邊緣節點高效獲取資源,可以減少載入時間並增強用戶體驗。
  • 部署簡單: 靜態網站可以在任何托管服務上運行,例如GitHub PagesVercel等,我們只需要傳輸文件即可,無需處理伺服器配置和資料庫管理等,如果藉助Git版本控制和CI/CD工具等,還可以比較輕鬆地實現自動化部署。
  • 資源占用低: 靜態網站只需要非常少的伺服器資源,這使得其可以在低配置的環境中運行,我們可以在較低配置的伺服器上藉助Nginx輕鬆支撐10k+QPS網站訪問。
  • SEO優勢: 靜態網站通常對搜索引擎優化SEO更加友好,預渲染的頁面可以擁有完整的HTML標簽結構,並且通過編譯可以使其儘可能符合語義化結構,這樣使得搜索引擎的機器人更容易抓取和索引。

那麼同樣的,通過SSG生成的靜態資源站點也有一些局限性:

  • 實時性不強: 由於靜態站點需要提前生成,因此就無法像動態網站一樣根據實時的請求生成對應的內容,例如當我們發佈了新文檔之後,就必須要重新進行增量編譯甚至是全站全量編譯,那麼在編譯期間就無法訪問到最新的內容。
  • 不支持動態交互: 靜態站點通常只是靜態資源的集合,因此在一些動態交互的場景下就無法實現,例如用戶登錄、評論等功能,當然這些功能可以通過客戶端渲染時動態支持,那麼這種情況就不再是純粹的靜態站點,通常是藉助SSG來實現更好的首屏和SEO效果。

綜上所述,SSG更適用於生成內容較為固定、不需要頻繁更新、且對於數據延遲敏感較低的的項目,並且實際上我們可能也只是選取部分能力來優化首屏等場景,最終還是會落到CSR來實現服務能力。因此當我們要選擇渲染方式的時候,還是要充分考慮到業務場景,由此來確定究竟是CSR - Client Side RenderSSR - Server Side RenderSSG - Static Site Generation更適合我們的業務場景,甚至在一些需要額外優化的場景下,ISR - Incremental Static RegenerationDPR - Distributed Persistent RenderingESR - Edge Side Rendering等也可以考慮作為業務上的選擇。

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

那麼接下來我們就從基本原理開始,優化組件編譯的方式,進而基於模版渲染生成SSG,文中相關API的調用基於React17.0.2版本實現,內容相關的DEMO地址為https://github.com/WindrunnerMax/webpack-simple-environment/tree/master/packages/react-render-ssg

基本原理

通常當我們使用React進行客戶端渲染CSR時,只需要在入口的index.html文件中置入<div id="root"></div>的獨立DOM節點,然後在引入的xxx.js文件中通過ReactDOM.render方法將React組件渲染到這個DOM節點上即可。將內容渲染完成之後,我們就會在某些生命周期或者Hooks中發起請求,用以動態請求數據並且渲染到頁面上,此時便完成了組件的渲染流程。

那麼在前邊我們已經聊了比較多的SSG內容,那麼可以明確對於渲染的主要內容而言我們需要將其離線化,因此在這裡就需要先解決第一個問題,如何將數據離線化,而不是在瀏覽器渲染頁面之後再動態獲取。很明顯在前邊我們提到的將數據從資料庫請求出來之後寫入json文件就是個可選的方式,我們可以在代碼構建的時候請求數據,在此時將其寫入文件,在最後一併上傳到CDN即可。

在我們的離線數據請求問題解決後,我們就需要來看渲染問題了,前邊也提到了類似的問題,如果依舊按照之前的渲染思路,而僅僅是將數據請求的地址從服務端介面替換成了靜態資源地址,那麼我們就無法做到SEO以及更快的首屏體驗。其實說到這裡還有一個比較有趣的事情,當我們用SSR的時候,假如我們的組件是dynamic引用的,那麼Next在輸出HTML的時候會將數據打到HTML<script />標簽里,在這種情況下實際上首屏的效率還是不錯的,並且Google進行索引的時候是能夠正常將動態執行Js渲染後的數據抓取,對於我們來說也可以算作一種離線化的渲染方案。

那麼這種方式雖然可行但是並不是很好的方案,我們依然需要繼續解決問題,那麼接下來我們需要正常地來渲染完整的HTML結構。在ReactDOMServer API中存在存在兩個相關的API,分別是renderToStaticMarkuprenderToString,這兩個API都可以將React組件輸出HTML標簽的結構,只是區別是renderToStaticMarkup渲染的是不帶data-reactid的純HTML結構,當客戶端進行React渲染時會完全重建DOM結構,因此可能會存在閃爍的情況,renderToString則渲染了帶標記的HTML結構,React在客戶端不會重新渲染DOM結構,那麼在我們的場景下時需要通過renderToString來輸出HTML結構的。

// packages/react-render-ssg/src/basic/index.ts
import ReactDOMServer from "react-dom/server";

const App = React.createElement(
  React.Fragment,
  null,
  React.createElement("div", null, "React HTML Render"),
  React.createElement(
    "button",
    {
      onClick: () => alert("On Click"),
    },
    "Button"
  )
);

const HTML = ReactDOMServer.renderToString(App);
// <div data-reactroot="">React HTML Render</div><button data-reactroot="">Button</button>

當前我們已經得到組件渲染過後的完整HTML結構,緊接著從輸出的內容我們可以看出來一個問題,我們定義的onClick函數並沒有在渲染過後的HTML結構中體現出來,此時在我們的HTML結構中只是一些完整的標簽,並沒有任何事件的處理。當然這也是很合理的情況,我們是用React框架實現的事件處理,其並不太可能直接完整地映射到輸出的HTML中,特別是在複雜應用中我們還是需要通過React來做後續事件交互處理的,那麼很顯然我們依舊需要在客戶端處理相關的事件。

那麼在React中我們常用的處理客戶端渲染函數就是ReactDOM.render,那麼當前我們實際上已經處理好了HTML結構,而並不需要再次將內容完整地渲染出來,或者換句話說我們現在需要的是將事件掛在相關DOM上來處理交互行為,將React附加到在服務端環境中已經由React渲染的現有HTML上,由React來接管有關的DOM的處理。那麼對於我們來說,我們需要將同樣的React組件在客戶端一併定義,然後將其輸出到頁面的Js中,也就是說這部分內容是需要在客戶端中執行的。

// packages/react-render-ssg/src/basic/index.ts
const PRESET = `
const App = React.createElement(
  React.Fragment,
  null,
  React.createElement("div", null, "React HTML Render"),
  React.createElement(
    "button",
    {
      onClick: () => alert("On Click"),
    },
    "Button"
  )
);
const _default = App;
ReactDOM.hydrate(_default, document.getElementById("root"));
`;

await fs.writeFile(`dist/${jsPathName}`, PRESET);

實際上這部分代碼都是在服務端生成的,我們此時並沒有在客戶端運行的內容,或者說這是我們的編譯過程,還沒有到達運行時,所以我們生成的一系列內容都是在服務端執行的,那麼很明顯我們是需要拼裝HTML等靜態資源文件的。因此在這裡我們可以通過預先定義一個HTML文件的模版,然後將構建過程中產生的內容放到模版以及新生成的文件里,產生的所有內容都將隨著構建一併上傳到CDN上並分發。

<!-- packages/react-render-ssg/public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... Meta -->
    <title>Template</title>
    <!-- INJECT STYLE -->
  </head>
  <body>
    <div id="root">
      <!-- INJECT HTML -->
    </div>
    <!-- ... React Library  -->
    <!-- INJECT SCRIPT -->
  </body>
</html>
// packages/react-render-ssg/src/basic/index.ts
const template = await fs.readFile("./public/index.html", "utf-8");
await fs.mkdir("dist", { recursive: true });
const random = Math.random().toString(16).substring(7);
const jsPathName = `${random}.js`;
const html = template
.replace(/<!-- INJECT HTML -->/, HTML)
.replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsPathName}"></script>`);
await fs.writeFile(`dist/${jsPathName}`, PRESET);
await fs.writeFile(`dist/index.html`, html);

至此我們完成了最基本的SSG構建流程,接下來就可以通過靜態伺服器訪問資源了,在這部分DEMO可以直接通過ts-node構建以及anywhere預覽靜態資源地址。實際上當前很多開源的靜態站點搭建框架例如VitePressRsPress等等都是採用類似的原理,都是在服務端生成HTMLJsCSS等等靜態文件,然後在客戶端由各自的框架重新接管DOM的行為,當然這些框架的集成度很高,對於相關庫的復用程度也更高。而針對於更複雜的應用場景,還可以考慮NextGatsby等框架實現,這些框架在SSG的基礎上還提供了更多的能力,對於更複雜的應用場景也有著更好的支持。

組件編譯

雖然在前邊我們已經實現了最基本的SSG原理,但是很明顯我們為了最簡化地實現原理人工處理了很多方面的內容,例如在上述我們輸出到Js文件的代碼中是通過PRESET變數定義的純字元串實現的代碼,而且我們對於同一個組件定義了兩遍,相當於在服務端和客戶端分開定義了運行的代碼,那麼很明顯這樣的方式並不太合理,接下來我們就需要解決這個問題。

那麼我們首先需要定義一個公共的App組件,在該組件的代碼實現中與前邊的基本原理中一致,這個組件會共用在服務端的HTML生成和客戶端的React Hydrate,而且為了方便外部的模塊導入組件,我們通常都是通過export default的方式預設導出整個組件。

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

const App = () => (
  <React.Fragment>
    <div>React Render SSG</div>
    <button onClick={() => alert("On Click")}>Button</button>
  </React.Fragment>
);

export default App;

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

// 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"],
  };
};

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

// 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(/<!-- INJECT HTML -->/, HTML)
    .replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
  await fs.writeFile(`${path}index.html`, html);
})();

模版渲染

當前我們已經復用了組件的定義,並且通過Rollup打包了需要在客戶端運行的Js文件,不需要再人工維護輸出到客戶端的內容。那麼場景再複雜一些,假如此時我們的組件有著更加複雜的內容,例如引用了組件庫來構建視圖,以及引用了一些CSS樣式預處理器來構建樣式,那麼我們的服務端輸出HTML的程式就會變得更加複雜。

繼續沿著前邊的處理思路,我們在服務端的處理程式僅僅是需要將App組件的HTML內容渲染出來,那麼假設此時我們的組件引用了@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";

那麼需要關註的是,當前我們運行組件的時候是在服務端環境中,那麼在Node環境中顯然我們是不認識.less文件以及.css文件的,實際上先不說這些樣式文件,import語法本身在Node環境中也是不支持的,只不過我們通常是使用ts-node來執行整個運行程式,暫時這點不需要關註,那麼對於樣式文件我們在這裡實際上是不需要的,所以我們就需要配置Node環境來處理這些樣式文件的引用。

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

但是即使這樣問題顯然沒有結束,熟悉arco-design的打包同學可能會清楚,當我們引入的樣式文件是Button/style/index時,實際上是引入了一個js文件而不是.less文件,如果需要明確引入.less文件的話是需要明確Button/style/index.less文件指向的。那麼此時如果我們是引入的.less文件,那麼並不會出現什麼問題,但是此時我們引用的是.js文件,而這個.js文件中內部的引用方式是import,因為此時我們是通過es而不是lib部分明確引用的,即使在tsconfig中配置了相關解析方式為commonjs也是沒有用的。

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

因此我們可以看到,如果僅僅用ts-node來解析或者說執行服務端的數據生成是不夠的,會導致我們平時實現組件的時候有著諸多限制,例如我們不能隨便引用es的實現而需要藉助包本身的package.json聲明的內容來引入內容,如果包不能處理commonjs的引用那麼還會束手無策。那麼在這種情況下我們還是需要引入打包工具來打包commonjs的代碼,然後再通過Node來執行輸出HTML。通過打包工具,我們能夠做的事情就很多了,在這裡我們將資源文件例如.less.svg都通過null-loader載入,且相關的配置輸出都以commonjs為基準,此時我們輸出的文件為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",
  },
};

當前我們已經得到了可以在Node環境中運行的組件,那麼緊接著,考慮到輸出SSG時我們通常都需要預置靜態數據,例如我們要渲染文檔的話就需要首先在資料庫中將相關數據表達查詢出來,然後作為靜態數據傳入到組件中,然後在預輸出的HTML中將內容直接渲染出來,那麼此時我們的App組件的定義就需要多一個getStaticProps函數聲明,並且我們還引用了一些樣式文件。

// 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.Fragment>
    <div>React Render SSG With {props.name}</div>
    <Button style={{ marginTop: 10 }} type="primary" onClick={() => alert("On Click")}>
      Button
    </Button>
  </React.Fragment>
);

export const getStaticProps = () => {
  return Promise.resolve({
    name: "Static Props",
  });
};

export default App;
/* packages/react-render-ssg/src/rspack/app.less */
body {
  padding: 20px;
}

同樣的,我們也需要為客戶端運行的Js文件打包,只不過在這裡由於我們需要處理預置的靜態數據,我們在打包的時候同樣就需要預先生成模版代碼,當我們在服務端執行打包功能的時候,就需要將從資料庫查詢或者從文件讀取的數據放置於生成的模版文件中,然後以該文件為入口去再打包客戶端執行的React Hydrate能力。在這裡因為希望將模版文件看起來更加清晰,我們使用了JSON.parse來處理預置數據,實際上這裡只需要將占位預留好,數據在編譯的時候經過stringify直接寫入到模版文件中即可。

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

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

在模版文件生成好之後,我們就需要以這個文件作為入口調度客戶端資源文件的打包了,這裡由於我們還引用了組件庫,輸出的內容自然不光是Js文件,還需要將CSS文件一併輸出,並且我們還需要配置一些通過參數名可以控制的文件名生成、externals等等。這裡需要註意的是,此處我們不需要使用html-pluginHTML文件輸出,這部分調度我們會在最後統一處理。

// 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<string, string>);
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",
    // ...
  },
};

那麼此時我們就需要調度所有文件的打包過程了,首先我們需要創建需要的輸出和臨時文件夾,然後啟動服務端commonjs打包的流程,輸出node-side-entry.js文件,並且讀取其中定義的App組件以及預設數據讀取方法,緊接著我們需要創建客戶端入口的模版文件,並且通過調度預設數據讀取方法將數據寫入到入口模版文件中,此時我們就可以通過打包的commonjs組件執行並且輸出HTML了,並且客戶端運行的React Hydrate代碼也可以在這裡一併打包出來,最後將各類資源文件的引入一併在HTML中替換並且寫入到輸出文件中就可以了。至此當我們打包完成輸出文件後,就可以使用靜態資源伺服器啟動SSG的頁面預覽了。

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("<props placeholder>", JSON.stringify(defaultProps))
    .replace("<index placeholder>", 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(/<!-- INJECT HTML -->/, HTML)
    .replace(/<!-- INJECT STYLE -->/, `<link rel="stylesheet" href="${random}.css">`)
    .replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
  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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • title: Vue 3 Teleport:掌控渲染的藝術 date: 2024/6/5 updated: 2024/6/5 description: 這篇文章介紹了Vue3框架中的一個創新特性——Teleport,它允許開發者將組件內容投送到文檔對象模型(DOM)中的任意位置,即使這個位置在組件的 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 首先明確一點,localStorage是同步的 一、首先為什麼會有這樣的問題 localStorage 是 Web Storage API 的一部分,它提供了一種存儲鍵值對的機制。localStorage 的數據是持久存儲在用戶的硬碟上的 ...
  • 前端跨域問題的解決方案通常涉及幾種不同的方法,每種方法都有其特定的應用場景和優缺點。以下是一些常見的前端跨域解決方案: JSONP(JSON with Padding) 原理:利用<script>標簽沒有跨域限制的特性,通過動態創建<script>標簽並設置其src屬性為跨域請求的URL,來實現跨域 ...
  • Web 性能是 Web 開發的一個重要方面,側重於網頁載入速度以及對用戶輸入的響應速度 通過優化網站來改善性能,可以在為用戶提供更好的體驗 網頁性能既廣泛又非常深入 1. 為什麼性能這麼重要? 1. 性能關乎留住用戶 性能對於任何線上業務都至關重要 與載入速度緩慢、讓人感覺運行緩慢的網站相比,載入速... ...
  • Don't Talk, code is here: 重點是startRecord 方法 <template> <div> <el-tooltip class="item" effect="dark" content="再次點擊 【開始錄音】 即為重新錄製,之前錄製的將被作廢" placement=" ...
  • title: Vue.js 動畫與過渡效果實戰 date: 2024/6/4 updated: 2024/6/4 description: 這篇文章介紹瞭如何在網頁設計中使用過渡動畫和組件效果,以及如何利用模式和列表展示信息。還提到了使用鉤子實現組件間通信的方法。 categories: 前端開發 ...
  • 以用戶為中心的性能指標是理解和改進站點體驗的關鍵點 一、以用戶為中心的性能指標 1. 指標是用來幹啥的? 指標是用來衡量性能和用戶體驗的 2. 指標類型 感知載入速度:網頁可以多快地載入網頁中的所有視覺元素並將其渲染到屏幕上 載入響應速度:頁面載入和執行組件快速響應用戶互動所需的 JavaScrip... ...
  • 隨著Web應用變得越來越複雜,而jQuery的功能卻顯得過於簡單,難以應對這些複雜的需求。比如,對於一些需要大量動態交互的應用程式,jQuery的功能並不足夠強大。此外,由於jQuery所寫應用的代碼結構較為混亂,其中包含了大量的全局變數和函數,例如,全局變數"$"和"jQuery"都指向了jQue... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...