基於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
渲染的,畢竟我的博客就可以算是基於Mdx
的SSG
渲染。最開始我把這個問題想的特別複雜,但是在實現的時候發現只是實現基本原理的話還是很粗暴的解決方案,在渲染的時候並沒有想象中要處理得那麼精細,當然實際上要做完整的方案特別是要實現一個框架也不是那麼容易的事情,對於數據的處理與渲染要做很多方面的考量。
在我們正式開始聊SSG
的基本原理前,我們可以先來看一下通過SSG
實現靜態站點的特點:
- 訪問速度快: 靜態網站只是一組預先生成的
HTML
、CSS
、JavaScript
、Image
等靜態文件,沒有運行在伺服器上的動態語言程式,在部署於CDN
的情況下,用戶可以直接通過邊緣節點高效獲取資源,可以減少載入時間並增強用戶體驗。 - 部署簡單: 靜態網站可以在任何托管服務上運行,例如
GitHub Pages
、Vercel
等,我們只需要傳輸文件即可,無需處理伺服器配置和資料庫管理等,如果藉助Git
版本控制和CI/CD
工具等,還可以比較輕鬆地實現自動化部署。 - 資源占用低: 靜態網站只需要非常少的伺服器資源,這使得其可以在低配置的環境中運行,我們可以在較低配置的伺服器上藉助
Nginx
輕鬆支撐10k+
的QPS
網站訪問。 SEO
優勢: 靜態網站通常對搜索引擎優化SEO
更加友好,預渲染的頁面可以擁有完整的HTML
標簽結構,並且通過編譯可以使其儘可能符合語義化結構,這樣使得搜索引擎的機器人更容易抓取和索引。
那麼同樣的,通過SSG
生成的靜態資源站點也有一些局限性:
- 實時性不強: 由於靜態站點需要提前生成,因此就無法像動態網站一樣根據實時的請求生成對應的內容,例如當我們發佈了新文檔之後,就必須要重新進行增量編譯甚至是全站全量編譯,那麼在編譯期間就無法訪問到最新的內容。
- 不支持動態交互: 靜態站點通常只是靜態資源的集合,因此在一些動態交互的場景下就無法實現,例如用戶登錄、評論等功能,當然這些功能可以通過客戶端渲染時動態支持,那麼這種情況就不再是純粹的靜態站點,通常是藉助
SSG
來實現更好的首屏和SEO
效果。
綜上所述,SSG
更適用於生成內容較為固定、不需要頻繁更新、且對於數據延遲敏感較低的的項目,並且實際上我們可能也只是選取部分能力來優化首屏等場景,最終還是會落到CSR
來實現服務能力。因此當我們要選擇渲染方式的時候,還是要充分考慮到業務場景,由此來確定究竟是CSR - Client Side Render
、SSR - Server Side Render
、SSG - Static Site Generation
更適合我們的業務場景,甚至在一些需要額外優化的場景下,ISR - Incremental Static Regeneration
、DPR - Distributed Persistent Rendering
、ESR - Edge Side Rendering
等也可以考慮作為業務上的選擇。
當然,回到最初我們提到的問題上,假如我們只是為了靜態資源的同步,通過CDN
來解決全球跨地域訪問的問題,那麼實際上並不是一定需要完全的SSG
來解決問題。將CSR
完全轉變為SSR
畢竟是一件改造範圍比較大的事情,而我們的目標僅僅是一處生產、多處消費,因此我們可以轉過來想一想實際上JSON
文件也是屬於靜態資源的一種類型,我們可以直接在前端發起請求將JSON
文件作為靜態資源請求到瀏覽器並且藉助SDK
渲染即可,至於一些交互行為例如點贊等功能的速度問題我們也是可以接受的,文檔站最的主要行為還是閱讀文檔。此外對於md
文件我們同樣可以如此處理,例如docsify
就是通過動態請求,但是同樣的對於搜索引擎來說這些需要執行Js
來動態請求的內容並沒有那麼容易抓取,所以如果想比較好地實現這部分能力還是需要不斷優化迭代。
那麼接下來我們就從基本原理開始,優化組件編譯的方式,進而基於模版渲染生成SSG
,文中相關API
的調用基於React
的17.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
結構。在ReactDOM
的Server API
中存在存在兩個相關的API
,分別是renderToStaticMarkup
與renderToString
,這兩個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
預覽靜態資源地址。實際上當前很多開源的靜態站點搭建框架例如VitePress
、RsPress
等等都是採用類似的原理,都是在服務端生成HTML
、Js
、CSS
等等靜態文件,然後在客戶端由各自的框架重新接管DOM
的行為,當然這些框架的集成度很高,對於相關庫的復用程度也更高。而針對於更複雜的應用場景,還可以考慮Next
、Gatsby
等框架實現,這些框架在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-plugin
將HTML
文件輸出,這部分調度我們會在最後統一處理。
// 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