我們聊聊從頭學(xué)服務(wù)器組件:在導(dǎo)航間保留狀態(tài)
存在的問題
到目前為止,訪問每個(gè)頁面,服務(wù)器返回的都是一個(gè) HTML 字符串:
async function sendHTML(res, jsx) {
const html = await renderJSXToHTML(jsx);
res.setHeader("Content-Type", "text/html");
res.end(html);
}
這對(duì)瀏覽器首次加載是非常友好的——瀏覽器有針對(duì) HTML 渲染做針對(duì)性優(yōu)化,會(huì)盡可能快地展示——但對(duì)于頁面導(dǎo)航就不太理想了。我們希望頁面支持布局刷新,即只更新 "發(fā)生變化的部分",頁面其他部分不受影響,也讓交互變得流暢了。
為了說明這個(gè)問題,我們向 BlogLayout 組件的 <nav> 中添加一個(gè) <input />:
<nav>
<a href="/">Home</a>
<hr />
<input />
<hr />
</nav>
請(qǐng)注意,每次瀏覽博客時(shí),輸入框里的內(nèi)容(或叫“狀態(tài)”)都會(huì)消失:
圖片
這對(duì)一個(gè)簡(jiǎn)單的博客來說可能沒有問題,但如果要構(gòu)建具有復(fù)雜交互的應(yīng)用程序,這種行為在某些時(shí)候就會(huì)讓人頭疼。因此,你需要讓用戶在應(yīng)用程序中隨意瀏覽時(shí),不會(huì)丟失頁面狀態(tài)。
接下來,我們將分 3 步驟來解決這個(gè)問題:
- 添加一些客戶端 JS 邏輯攔截導(dǎo)航,修改默認(rèn)行為(這樣我們就可以手動(dòng)重新獲取內(nèi)容,而無需重新加載整個(gè)頁面)
- 修改服務(wù)端代碼,支持在隨后的導(dǎo)航中需要提供返回 JSX 響應(yīng)(而不是 HTML)的支持
- 修改客戶端代碼,在不破壞 DOM 的情況下執(zhí)行 JSX 更新(提示:這部分將使用 React)
局部更新支持
步驟 1:攔截導(dǎo)航
我們需要一些客戶端邏輯,因此增加一個(gè) client.js 文件。先修改服務(wù)端代碼(即 server.js),增加靜態(tài)資源的路由請(qǐng)求支持(即"/client.js")。
createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
// 增加靜態(tài)資源 "/client.js" 的路由支持
if (url.pathname === "/client.js") {
await sendScript(res, "./client.js"); // 返回服務(wù)端本地的 client.js 文件
} else {
await sendHTML(res, <Router url={url} />);
}
} catch (err) {
// ...
}
}).listen(8080)
async function sendScript(res, filename) {
const content = await readFile(filename, "utf8");
res.setHeader("Content-Type", "text/javascript");
res.end(content);
}
另外,在 sendHTML() 中為響應(yīng)的 HTML 文本增加 <script> 標(biāo)簽,引用 client.js 文件。
async function sendHTML(res, jsx) {
let html = await renderJSXToHTML(jsx);
html += `<script type="module" src="/client.js"></script>`;
res.setHeader("Content-Type", "text/html");
res.end(html);
}
現(xiàn)在說回 client.js。在這個(gè)文件中,我們會(huì)覆蓋網(wǎng)站內(nèi)導(dǎo)航的默認(rèn)行為(譯注:此處指<a> 標(biāo)簽鏈接能力),改調(diào)我們自己的 navigate() 函數(shù):
async function navigate(pathname) {
// TODO
}
window.addEventListener("click", (e) => {
// 只處理 <a> 標(biāo)簽的點(diǎn)擊事件
if (e.target.tagName !== "A") {
return;
}
// Ignore "open in a new tab".
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
// Ignore external URLs.
const href = e.target.getAttribute("href");
if (!href.startsWith("/")) {
return;
}
// 這里是核心代碼。首先,阻止 <a> 標(biāo)簽?zāi)J(rèn)的跳轉(zhuǎn)行為
e.preventDefault();
// 然后,將跳轉(zhuǎn)地址推入瀏覽歷史,瀏覽器地址欄的地址也同步更新
window.history.pushState(null, null, href);
// 最后,執(zhí)行自定義的導(dǎo)航行為
navigate(href);
}, true);
window.addEventListener("popstate", () => {
// When the user presses Back/Forward, call our custom logic too.
navigate(window.location.pathname);
});
navigate() 函數(shù)就是編寫自定義導(dǎo)航行為的地方。在 navigate() 函數(shù)中,我們使用 fetch API 請(qǐng)求下一個(gè)路由的 HTML 響應(yīng),并使用這個(gè)響應(yīng)更新頁面:
let currentPathname = window.location.pathname;
async function navigate(pathname) {
currentPathname = pathname;
// 獲取當(dāng)前導(dǎo)航路由下的 HTML 數(shù)據(jù)
const response = await fetch(pathname);
const html = await response.text();
if (pathname === currentPathname) {
// 獲取 HTML 響應(yīng)數(shù)據(jù) <body> 標(biāo)簽里的內(nèi)容
const bodyStartIndex = html.indexOf("<body>") + "<body>".length;
const bodyEndIndex = html.lastIndexOf("</body>");
const bodyHTML = html.slice(bodyStartIndex, bodyEndIndex);
// 將拿到的 HTML 數(shù)據(jù)替換掉當(dāng)前 <body> 里的內(nèi)容
document.body.innerHTML = bodyHTML;
}
}
點(diǎn)擊這里的 線上 demo[7] 查看效果。
可以看到,在頁面不刷新的情況下,頁面內(nèi)容被替換了,表示我們已經(jīng)成功覆蓋了瀏覽器默認(rèn)導(dǎo)航行為。不過,由于目前是針對(duì) <body> 元素的整體覆蓋,所以<input> 里的數(shù)據(jù)仍然會(huì)丟失。接下來,我們會(huì)修改服務(wù)器部分,增加返回 JSX 響應(yīng)的支持。
步驟 2:增加返回 JSX 響應(yīng)的支持
還記得我們之前使用 JSX 組成的對(duì)象樹結(jié)構(gòu)吧?
{
$$typeof: Symbol.for("react.element"),
type: 'html',
props: {
children: [
{
$$typeof: Symbol.for("react.element"),
type: 'head',
props: {
// ... And so on ...
接下來,我們將為服務(wù)器增加以 ?jsx 結(jié)尾的請(qǐng)求支持。?jsx 請(qǐng)求返回的是一個(gè) JSX 樹結(jié)構(gòu),而不是 HTML,這更容易讓客戶端確定哪些部分發(fā)生了變化,并只在必要時(shí)更新 DOM,這將直接解決 <input> 狀態(tài)在每次導(dǎo)航時(shí)丟失的問題,但這并不是我們這樣做的唯一原因。接下來,你將看到我們?nèi)绾螌⑿滦畔ⅲú粌H僅是 HTML)從服務(wù)器傳遞到客戶端。
首先修改服務(wù)器代碼,支持請(qǐng)求中攜帶 ?jsx 參數(shù)時(shí)調(diào)用一個(gè)新的 sendJSX() 函數(shù):
createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === "/client.js") {
// ...
} else if (url.searchParams.has("jsx")) {
url.searchParams.delete("jsx"); // 確保傳遞給 <Router> 的 url 是干凈的
await sendJSX(res, <Router url={url} />); // 返回 JSX 數(shù)據(jù)
} else {
await sendHTML(res, <Router url={url} />);
}
// ...
在 sendJSX() 中,我們使用 JSON.stringify(jsx) 把的 JSX 樹轉(zhuǎn)換成一個(gè) JSON 字符串,并通過網(wǎng)絡(luò)返回:
async function sendJSX(res, jsx) {
const jsxString = JSON.stringify(jsx, null, 2); // Indent with two spaces.
res.setHeader("Content-Type", "application/json");
res.end(jsxString);
}
要注意的是,我們并不是發(fā)送 JSX 語法本身(如 "<Foo />" ),只是將 JSX 生成的樹結(jié)構(gòu)轉(zhuǎn)換成 JSON 字符串。當(dāng)然,真正的 RSC 實(shí)現(xiàn)使用的是并不是 JSON 格式,這里只是為了方便理解。真正的實(shí)現(xiàn)方式我們將在下一個(gè)系列進(jìn)行探討。
修改客戶端代碼,看看目前返回的數(shù)據(jù):
async function navigate(pathname) {
currentPathname = pathname;
const response = await fetch(pathname + "?jsx");
const jsonString = await response.text();
if (pathname === currentPathname) {
alert(jsonString);
}
}
點(diǎn)擊這里[8],加載首頁,然后點(diǎn)擊一個(gè)鏈接會(huì)看到一個(gè)提示,類似下面這樣:
{
"key": null,
"ref": null,
"props": {
"url": "http://localhost:3000/hello-world"
},
// ...
}
這并不是我們想要的結(jié)構(gòu),我們希望看到類似 <html>...</html> 的 JSX 樹。那是哪里出問題了呢?
我們分析一下,現(xiàn)在的 JSX 看起來是這樣的:
<Router url="http://localhost:3000/hello-world" />
// {
// $$typeof: Symbol.for('react.element'),
// type: Router,
// props: { url: "http://localhost:3000/hello-world" } },
// ...
// }
這種結(jié)構(gòu)發(fā)送給客戶端還“為時(shí)過早”,我們不知道 Router 具體的 JSX 內(nèi)容,因?yàn)?nbsp;Router 只存在服務(wù)器上。我們需要在服務(wù)端調(diào)用 Router 組件,才能得到需要發(fā)送給客戶端的 JSX 內(nèi)容。
如果,我們使用 { url: "http://localhost:3000/hello-world" } } 作為 prop 調(diào)用 Router 函數(shù),會(huì)得到這樣一段 JSX:
<BlogLayout>
<BlogIndexPage />
</BlogLayout>
一樣的,把這個(gè)結(jié)果發(fā)送回客戶端還為時(shí)過早,因?yàn)槲覀儾恢?nbsp;BlogLayout 的內(nèi)容,而且它只存在于服務(wù)器上,因此我們還必須調(diào)用 BlogLayout,得到最終傳遞給客戶端的 JSX。
先想一下我們的結(jié)果——調(diào)用結(jié)束后,我們希望得到一個(gè)不引用任何服務(wù)器代碼的 JSX 樹。類似:
<html>
<head>...</head>
<body>
<nav>
<a href="/">Home</a>
<hr />
</nav>
<main>
<section>
<h1>Welcome to my blog</h1>
<div>
...
</div>
</main>
<footer>
<hr />
<p>
<i>
(c) Jae Doe 2003
</i>
</p>
</footer>
</body>
</html>
這個(gè)結(jié)果可以直接傳遞給 JSON.stringify() 并發(fā)送給客戶端。
首先,編寫一個(gè) renderJSXToClientJSX() 函數(shù)。接收一段 JSX 作為參數(shù),"解析 "服務(wù)器專有部分(通過調(diào)用相應(yīng)組件獲得),直到只剩下客戶端可以理解的 JSX。
結(jié)構(gòu)上看,這個(gè)函數(shù)類似于 renderJSXToHTML(),但它遞歸遍歷返回的是對(duì)象,而不是 HTML:
async function renderJSXToClientJSX(jsx) {
if (
typeof jsx === "string" ||
typeof jsx === "number" ||
typeof jsx === "boolean" ||
jsx == null
) {
// Don't need to do anything special with these types.
return jsx;
} else if (Array.isArray(jsx)) {
// Process each item in an array.
return Promise.all(jsx.map((child) => renderJSXToClientJSX(child)));
} else if (jsx != null && typeof jsx === "object") {
if (jsx.$$typeof === Symbol.for("react.element")) {
if (typeof jsx.type === "string") {
// This is a component like <div />.
// Go over its props to make sure they can be turned into JSON.
return {
...jsx,
props: await renderJSXToClientJSX(jsx.props),
};
} else if (typeof jsx.type === "function") {
// This is a custom React component (like <Footer />).
// Call its function, and repeat the procedure for the JSX it returns.
const Component = jsx.type;
const props = jsx.props;
const returnedJsx = await Component(props);
return renderJSXToClientJSX(returnedJsx);
} else throw new Error("Not implemented.");
} else {
// This is an arbitrary object (for example, props, or something inside of them).
// Go over every value inside, and process it too in case there's some JSX in it.
return Object.fromEntries(
await Promise.all(
Object.entries(jsx).map(async ([propName, value]) => [
propName,
await renderJSXToClientJSX(value),
])
)
);
}
} else throw new Error("Not implemented");
}
接下來編輯 sendJSX(),先將類似 <Router /> 的 JSX 轉(zhuǎn)換為 "客戶端 JSX",然后再對(duì)其字符串化:
async function sendJSX(res, jsx) {
const clientJSX = await renderJSXToClientJSX(jsx);
const clientJSXString = JSON.stringify(clientJSX, null, 2); // Indent with two spaces
res.setHeader("Content-Type", "application/json");
res.end(clientJSXString);
}
點(diǎn)擊這里的 線上 demo[9] 查看效果。
現(xiàn)在點(diǎn)擊鏈接就會(huì)顯示一個(gè)與 HTML 很類似的樹狀結(jié)構(gòu)提示,這表示我們可以對(duì)它進(jìn)行差異化處理了。
注意:目前我們的目標(biāo)是能最低要求的工作起來,但在實(shí)現(xiàn)過程中還有很多不盡如人意的地方。例如,格式本身非常冗長(zhǎng)和重復(fù),真正的 RSC 使用的是更簡(jiǎn)潔的格式。就像前面生成 HTML 一樣,整個(gè)響應(yīng)被 await 是很糟糕的體驗(yàn)。理想情況下,我們希望將 JSX 分塊進(jìn)行流式傳輸,并在客戶端進(jìn)行拼接。同樣,共享布局的部分內(nèi)容(如 <html> 和 <nav> )沒有發(fā)生變化,現(xiàn)在也要重新發(fā)送這些內(nèi)容。生產(chǎn)就緒的 RSC 實(shí)現(xiàn)并不存在這些缺陷,不過我們暫時(shí)接受這些缺陷,讓代碼看起來更容易理解。
步驟 3:在客戶端執(zhí)行 JSX 更新
嚴(yán)格來說,我們不必使用 React 來擴(kuò)展 JSX。
到目前為止,我們的 JSX 節(jié)點(diǎn)只包含瀏覽器內(nèi)置組件(例如:<nav>、<footer>)。為了實(shí)現(xiàn)對(duì)服務(wù)端返回的 JSX 數(shù)據(jù),執(zhí)行差異化處理和更新,我們會(huì)直接使用 React。
我們?yōu)?React 提供的 JSX 數(shù)據(jù),會(huì)幫助它知道要?jiǎng)?chuàng)建的 DOM 節(jié)點(diǎn)信息,也方便將來安全地進(jìn)行更改。同樣,React 處理過程·中,也會(huì)遍歷 DOM,查看每個(gè) DOM 節(jié)點(diǎn)對(duì)應(yīng)的 JSX 信息。這樣,React 就能將事件處理程序附加到 DOM 節(jié)點(diǎn),讓節(jié)點(diǎn)具備交互性,我們把這個(gè)過程叫水合(hydration)。
傳統(tǒng)上,要將服務(wù)器渲染的標(biāo)記水合,需要調(diào)用 hydrateRoot(),需要為 React 提供一個(gè)掛載 DOM 節(jié)點(diǎn),還有服務(wù)器上創(chuàng)建的初始 JSX。類似這樣:
// Traditionally, you would hydrate like this
hydrateRoot(document, <App />);
問題是我們?cè)诳蛻舳烁緵]有像 <App /> 這樣的根組件!從客戶端的角度來看,目前我們的整個(gè)應(yīng)用程序就是一大塊 JSX,沒有任何 React 組件。然而,React 真正需要的只是與初始 HTML 相對(duì)應(yīng)的 JSX 樹。這個(gè)樹類似 <html>...</html> 這樣的:
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(document, getInitialClientJSX());
function getInitialClientJSX() {
// TODO: return the <html>...</html> client JSX tree mathching the initial HTML
}
這個(gè)過程會(huì)非常快,因?yàn)榭蛻舳朔祷氐?JSX 樹中沒有任何自定義組件。React 將會(huì)以近乎瞬時(shí)的速度遍歷 DOM 樹和 JSX 樹,并構(gòu)建稍后更新樹時(shí)所需要的一些內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
然后,在每次用戶導(dǎo)航時(shí),我們獲取下一頁的 JSX,并通過 root.render[10] 更新 DOM:
async function navigate(pathname) {
currentPathname = pathname;
const clientJSX = await fetchClientJSX(pathname);
if (pathname === currentPathname) {
root.render(clientJSX);
}
}
async function fetchClientJSX(pathname) {
// TODO: fetch and return the <html>...</html> client JSX tree for the next route
}
補(bǔ)充完 getInitialClientJSX() 和 fetchClientJS() 的內(nèi)容后。React 就會(huì)按照我們預(yù)期的方式,創(chuàng)建并托管我們的項(xiàng)目,同時(shí)也不會(huì)丟失狀態(tài)。
現(xiàn)在,就讓我們來看看如何實(shí)現(xiàn)這兩個(gè)功能。
從服務(wù)器獲取 JSX
我們先從實(shí)現(xiàn) fetchClientJSX() 開始,因?yàn)樗菀讓?shí)現(xiàn)。
首先,讓我們回顧一下 ?jsx 服務(wù)器端的實(shí)現(xiàn)。
async function sendJSX(res, jsx) {
const clientJSX = await renderJSXToClientJSX(jsx);
const clientJSXString = JSON.stringify(clientJSX);
res.setHeader("Content-Type", "application/json");
res.end(clientJSXString);
}
我們?cè)诳蛻舳讼蚍?wù)端發(fā)送 JSX 請(qǐng)求,然后將響應(yīng)代入 JSON.parse(),再將其轉(zhuǎn)換為 JSX:
async function fetchClientJSX(pathname) {
const response = await fetch(pathname + "?jsx");
const clientJSXString = await response.text();
const clientJSX = JSON.parse(clientJSXString);
return clientJSX;
}
這樣實(shí)現(xiàn)之后[11],點(diǎn)擊鏈接渲染 JSX 時(shí)就會(huì)報(bào)錯(cuò):
Objects are not valid as a React child (found: object with keys {type, key, ref, props, _owner, _store}).
原因是我們傳遞給 JSON.stringify() 的對(duì)象是這樣的:
{
$$typeof: Symbol.for("react.element"),
type: 'html',
props: {
// ...
但是查看客戶端 JSON.parse() 結(jié)果,$$typeof 屬性在傳輸過程中丟失了:
{
type: 'html',
props: {
// ...
對(duì) React 來說,如果 JSX 節(jié)點(diǎn)中沒有 $$typeof: Symbol.for("react.element") 屬性,就不會(huì)被看作是有效節(jié)點(diǎn)。這是一種有意為之的安全機(jī)制——默認(rèn)情況下,React 不會(huì)隨意將一個(gè)任意 JSON 對(duì)象視為 JSX 標(biāo)記。因此,就利用了 Symbol 值無法在 JSON 序列化過程中存在的特性,為 JSX 增加了一個(gè)Symbol.for('react.element') 屬性。
不過,我們確實(shí)在服務(wù)器上創(chuàng)建了這些 JSX 節(jié)點(diǎn),而且確實(shí)希望在客戶端上呈現(xiàn)它們。因此,我們需要調(diào)整邏輯,以 "保留" $$typeof: Symbol.for("react.element") 屬性。
幸運(yùn)的是,這個(gè)問題并不難解決。JSON.stringify() 接受一個(gè)替換函數(shù)[12],可以讓我們自定義 JSON 的生成行為。在服務(wù)器上,我們將用 "$RE" 這樣的特殊字符串替換 Symbol.for('react.element'):
async function sendJSX(res, jsx) {
// ...
const clientJSXString = JSON.stringify(clientJSX, stringifyJSX); // Notice the second argument
// ...
}
function stringifyJSX(key, value) {
if (value === Symbol.for("react.element")) {
// We can't pass a symbol, so pass our magic string instead.
return "$RE"; // Could be arbitrary. I picked RE for React Element.
} else if (typeof value === "string" && value.startsWith("$")) {
// To avoid clashes, prepend an extra $ to any string already starting with $.
return "$" + value;
} else {
return value;
}
}
在客戶端,我們將向 JSON.parse() 傳遞一個(gè) reviver 函數(shù),將 "$RE" 替換為 Symbol.for('react.element') :
async function fetchClientJSX(pathname) {
// ...
const clientJSX = JSON.parse(clientJSXString, parseJSX); // Notice the second argument
// ...
}
function parseJSX(key, value) {
if (value === "$RE") {
// This is our special marker we added on the server.
// Restore the Symbol to tell React that this is valid JSX.
return Symbol.for("react.element");
} else if (typeof value === "string" && value.startsWith("$$")) {
// This is a string starting with $. Remove the extra $ added by the server.
return value.slice(1);
} else {
return value;
}
}
點(diǎn)擊這里的 線上 demo[13] 查看效果。
現(xiàn)在,在頁面間進(jìn)行導(dǎo)航,不過更新全部是以 JSX 形式獲取并在客戶端應(yīng)用的!
如果你在輸入框中輸入內(nèi)容,然后點(diǎn)擊鏈接,會(huì)發(fā)現(xiàn) <input> 狀態(tài)在所有導(dǎo)航中都得到了保留,除第一個(gè)導(dǎo)航除外。這是因?yàn)槲覀冞€沒有告訴 React 頁面的初始 JSX 是什么,所以它無法正確關(guān)聯(lián)服務(wù)器返回的 HTML。
將初始 JSX 內(nèi)聯(lián)到 HTML 中
還有這段代碼:
const root = hydrateRoot(document, getInitialClientJSX());
function getInitialClientJSX() {
return null; // TODO
}
我們需要將根節(jié)點(diǎn)與初始客戶端 JSX 相結(jié)合,但客戶端上的 JSX 怎么來?
為了解決這個(gè)問題,我們可以把初始 JSX 字符串作為客戶端的全局變量:
const root = hydrateRoot(document, getInitialClientJSX());
function getInitialClientJSX() {
const clientJSX = JSON.parse(window.__INITIAL_CLIENT_JSX_STRING__, reviveJSX);
return clientJSX;
}
在服務(wù)器上,我們修改 sendHTML() 函數(shù),以便將應(yīng)用程序渲染為客戶端 JSX,并將其內(nèi)聯(lián)到 HTML 末尾:
async function sendHTML(res, jsx) {
let html = await renderJSXToHTML(jsx);
// Serialize the JSX payload after the HTML to avoid blocking paint:
const clientJSX = await renderJSXToClientJSX(jsx);
const clientJSXString = JSON.stringify(clientJSX, stringifyJSX);
html += `<script>window.__INITIAL_CLIENT_JSX_STRING__ = `;
html += JSON.stringify(clientJSXString).replace(/</g, "\\u003c");
html += `</script>`;
// ...
最后,我們需要對(duì)文本節(jié)點(diǎn)生成 HTML 的方式進(jìn)行一些小調(diào)整[14],以便 React 可以將它們水合。
點(diǎn)擊這里的 線上 demo[15] 查看效果。
現(xiàn)在你可以輸入一些內(nèi)容,其數(shù)據(jù)不會(huì)在導(dǎo)航間丟失:
圖片
這就是我們最初設(shè)定的目標(biāo)!當(dāng)然,保存這個(gè)輸入框數(shù)據(jù)并不是重點(diǎn),重要的是,我們的應(yīng)用程序?qū)崿F(xiàn)了“就地 ”刷新和導(dǎo)航,而不必丟失任何狀態(tài)。
注意:雖然真正的 RSC 實(shí)現(xiàn)確實(shí)會(huì)在 HTML payload 對(duì) JSX 進(jìn)行編碼,但仍存在一些重要的差異。生產(chǎn)就緒的 RSC 設(shè)置會(huì)在生成 JSX 塊時(shí)發(fā)送它們,而不是在最后發(fā)送單個(gè)大 Blob 數(shù)據(jù)。當(dāng) React 加載時(shí),水合作用可以立即開始——React 使用已經(jīng)可用的 JSX 塊遍歷樹,而不是非要等到它們?nèi)康竭_(dá)。 RSC 還允許將某些組件標(biāo)記為客戶端組件,這時(shí)候它們?nèi)匀粫?huì) SSR 到 HTML 中,但它們的代碼會(huì)包含在捆綁包中。對(duì)于客戶端組件,只有其 props 的 JSON 被序列化。將來,React 可能會(huì)添加額外的機(jī)制來刪除 HTML 和嵌入的 payload之間的重復(fù)內(nèi)容。
總結(jié)
本文,我們?yōu)榉?wù)端增加了返回 JSX 數(shù)據(jù)的支持,并使用 React 在客戶端進(jìn)行消費(fèi),實(shí)現(xiàn)基于 JSX 結(jié)構(gòu)的頁面初始化和頁面局部更新。出于安全考慮,React 要求 JSX 節(jié)點(diǎn)中需要包含一個(gè) $$typeof: Symbol.for("react.element") 屬性。為此,我們?cè)谛蛄谢徒馕龅臅r(shí)候,對(duì) $$typeof 做了特殊的轉(zhuǎn)換處理。
至此,現(xiàn)在我們的代碼實(shí)際上可以工作了,而且架構(gòu)已經(jīng)很接近真正的 RSC 了(雖然沒有引入流式傳輸這樣的復(fù)雜機(jī)制)。在下一篇,也就是本系列的終篇,我們將對(duì)現(xiàn)在的代碼做一些清理工作,修復(fù)一些缺陷,并為下一波功能做好準(zhǔn)備。