基于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等方案以更低的成
靜態(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):
HTML
、
CSS
、
JavaScript
、
Image
等靜態(tài)文件,沒有運(yùn)行在服務(wù)器上的動(dòng)態(tài)語言程序,在部署于
CDN
的情況下,用戶可以直接通過邊緣節(jié)點(diǎn)高效獲取資源,可以減少加載時(shí)間并增強(qiáng)用戶體驗(yàn)。
GitHub Pages
、
Vercel
等,我們只需要傳輸文件即可,無需處理服務(wù)器配置和數(shù)據(jù)庫管理等,如果借助
Git
版本控制和
CI/CD
工具等,還可以比較輕松地實(shí)現(xiàn)自動(dòng)化部署。
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)也有一些局限性:
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
的
標(biāo)簽里,在這種情況下實(shí)際上首屏的效率還是不錯(cuò)的,并且
Google
進(jìn)行索引的時(shí)候是能夠正常將動(dòng)態(tài)執(zhí)行
Js
渲染后的數(shù)據(jù)抓取,對于我們來說也可以算作一種離線化的渲染方案。
那么這種方式雖然可行但是并不是很好的方案,我們依然需要繼續(xù)解決問題,那么接下來我們需要正常地來渲染完整的
HTML
結(jié)構(gòu)。在
ReactDOM
的
Server API
中存在存在兩個(gè)相關(guān)的
API
,分別是
renderToStaticMarkup
與
renderToString
,這兩個(gè)
API
都可以將
React
組件輸出
HTML
標(biāo)簽的結(jié)構(gòu),只是區(qū)別是
renderToStaticMarkup
渲染的是不帶
data-reactid
的純
HTML
結(jié)構(gòu),當(dāng)客戶端進(jìn)行
React
渲染時(shí)會(huì)完全重建
DOM
結(jié)構(gòu),因此可能會(huì)存在閃爍的情況,
renderToString
則渲染了帶標(biāo)記的
HTML
結(jié)構(gòu),
React
在客戶端不會(huì)重新渲染
DOM
結(jié)構(gòu),那么在我們的場景下時(shí)需要通過
renderToString
來輸出
HTML
結(jié)構(gòu)的。
// 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);
// React HTML Render
當(dāng)前我們已經(jīng)得到組件渲染過后的完整
HTML
結(jié)構(gòu),緊接著從輸出的內(nèi)容我們可以看出來一個(gè)問題,我們定義的
onClick
函數(shù)并沒有在渲染過后的
HTML
結(jié)構(gòu)中體現(xiàn)出來,此時(shí)在我們的
HTML
結(jié)構(gòu)中只是一些完整的標(biāo)簽,并沒有任何事件的處理。當(dāng)然這也是很合理的情況,我們是用
React
框架實(shí)現(xiàn)的事件處理,其并不太可能直接完整地映射到輸出的
HTML
中,特別是在復(fù)雜應(yīng)用中我們還是需要通過
React
來做后續(xù)事件交互處理的,那么很顯然我們依舊需要在客戶端處理相關(guān)的事件。
那么在
React
中我們常用的處理客戶端渲染函數(shù)就是
ReactDOM.render
,那么當(dāng)前我們實(shí)際上已經(jīng)處理好了
HTML
結(jié)構(gòu),而并不需要再次將內(nèi)容完整地渲染出來,或者換句話說我們現(xiàn)在需要的是將事件掛在相關(guān)
DOM
上來處理交互行為,將
React
附加到在服務(wù)端環(huán)境中已經(jīng)由
React
渲染的現(xiàn)有
HTML
上,由
React
來接管有關(guān)的
DOM
的處理。那么對于我們來說,我們需要將同樣的
React
組件在客戶端一并定義,然后將其輸出到頁面的
Js
中,也就是說這部分內(nèi)容是需要在客戶端中執(zhí)行的。
// 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);
實(shí)際上這部分代碼都是在服務(wù)端生成的,我們此時(shí)并沒有在客戶端運(yùn)行的內(nèi)容,或者說這是我們的編譯過程,還沒有到達(dá)運(yùn)行時(shí),所以我們生成的一系列內(nèi)容都是在服務(wù)端執(zhí)行的,那么很明顯我們是需要拼裝
HTML
等靜態(tài)資源文件的。因此在這里我們可以通過預(yù)先定義一個(gè)
HTML
文件的模版,然后將構(gòu)建過程中產(chǎn)生的內(nèi)容放到模版以及新生成的文件里,產(chǎn)生的所有內(nèi)容都將隨著構(gòu)建一并上傳到
CDN
上并分發(fā)。
Template
// 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(//, HTML)
.replace(//, ``);
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
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實(shí)現(xiàn)對象集合與DataTable的相互轉(zhuǎn)換
閱讀鴻蒙NEXT元服務(wù):論如何免費(fèi)快速上架作品
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動(dòng)態(tài)代理的對比分析
閱讀Win11筆記本“自動(dòng)管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)