用React Router 4構(gòu)建通用JavaScript應(yīng)用
React Router 是一個(gè)在 React 領(lǐng)域非常流行的庫。它依靠位置欄上的請(qǐng)求 URL 和 瀏覽器的操作歷史來渲染不同的頁面內(nèi)容來保持顯示,進(jìn)而將你的 app 同步到用戶接口的頁面上。
新的閃閃發(fā)亮
最近,版本4的 React Router 已經(jīng)進(jìn)入 beta 發(fā)布階段。損譽(yù)各半,React Router 的這一次發(fā)布是對(duì)上一版本的完整重寫,這導(dǎo)致了很多破壞性的 API 變更。
在版本 4 的核心理念是 “聲明式可組合性(declarative composability)”?—— 它包含 React 之所以優(yōu)秀的組件概念,并將其應(yīng)用于 routing。React Router 4 的每一個(gè)部分都是 React 的組件: Router , Route , Link 等等。
React Router 的一位開發(fā)者, Ryan Florence ,親手制作了一個(gè)簡短的視頻來介紹***的 React Router,這段視頻獲得了很多人的推薦:
后臺(tái)如何?
新版本的 React Router 奉上了一個(gè)新的 web 頁面,上面有 許多實(shí)用的代碼示例 ,但沒有提供實(shí)例介紹如何在服務(wù)端使用 React Router 來進(jìn)行基于 React 的頁面的渲染。
對(duì)于我近期正在進(jìn)行的項(xiàng)目,對(duì)搜索引擎友好且具備***的網(wǎng)站運(yùn)行速度是重中之重,難道這樣就要在客戶端渲染整個(gè)頁面?難道就用示例頁面上所有實(shí)例所采取的辦法?那是不可取的。我們要使用一個(gè) Express 服務(wù)器來在后臺(tái)對(duì) React 頁面進(jìn)行渲染。
在其介紹視屏中, Ryan 有一個(gè)可以從某些 API 獲取數(shù)據(jù)來初始化其狀態(tài)的 App 組件, 使用的是 componentDidMount 生命周期方法。但異步數(shù)據(jù)的獲取操作完畢,組件就會(huì)被更新以顯示數(shù)據(jù)。
但是當(dāng)要在服務(wù)端對(duì) App 組件進(jìn)行渲染的時(shí)候這樣做不會(huì)有效果: 在你使用 renderToString 的時(shí)候, 帶有 HTML 代碼的字符串在調(diào)用了組件的渲染方法之后就會(huì)被同步地創(chuàng)建出來。 componentDidMount 從未被調(diào)用到。
因此如果我們使用 Ryan 視頻里的示例在后臺(tái)渲染出 App 組件,它只會(huì)生成一條 “Loading…” 消息。
解決方案
作為對(duì)概念方案的驗(yàn)證,我創(chuàng)建了一個(gè) demo 應(yīng)用,基本上就是對(duì)視頻中 Ryan 的示例進(jìn)行重造,但采取的是服務(wù)器端的渲染。
應(yīng)用使用了 GitHub API 去獲取有關(guān)于 Gist 代碼片段的數(shù)據(jù):
代碼展示
你可以在 Github 上找到 demo 應(yīng)用的源代碼:
https://github.com/technology-ebay-de/universal-react-router4
簡而言之,下面就是我所做的事情…
server/index.js
這就是每次有 HTTP 請(qǐng)求發(fā)到 Express 服務(wù)器的時(shí)候都會(huì)跑一次的代碼:
const routes = [
'/',
'/g/:gistId'
];
app.get('*', (req, res) => {
const match = routes.reduce((acc, route) => matchPath(req.url, route, { exact: true }) || acc, null);
if (!match) {
res.status(404).send(render(<NoMatch />));
return;
}
fetch('https://api.github.com/gists')
.then(r => r.json())
.then(gists =>
res.status(200).send(render(
(
<StaticRouter context={{}} location={req.url}>
<App gists={gists} />
</StaticRouter>
), gists
))
).catch(err => res.status(500).send(render(<Error />));
});
app.listen(3000, () => console.log('Demo app listening on port 3000'));
(注意:這里只是摘錄,你可以在 GitHub 找到完整的源代碼 )
在第 1–4行 , 我為 app 定義了一個(gè)路由數(shù)組。數(shù)組的***個(gè)元素就是針對(duì)主頁的初始請(qǐng)求, 沒有 Gists 被選上。第二個(gè)路由就是用來展示一個(gè)被選上的 Gist 的。
在第 6行 , 我的 Express app 被告知要處理任何可以使用星號(hào)匹配上的請(qǐng)求。
在第 7行 , 我使用了來自于 React Router 的 matchPath 函數(shù)對(duì)路由數(shù)組進(jìn)行簡省; 結(jié)果就是一個(gè)匹配對(duì)象,其擁有關(guān)于能匹配到的路由以及任何可以從 URL 路徑轉(zhuǎn)換過來的參數(shù),這些信息。
在第 8–11行 , 如果有不能匹配的路由,我就渲染出一個(gè)錯(cuò)誤頁面,上面會(huì)說 : “頁面沒找到(Page not found)”。
這里的 render 函數(shù)只是圍繞 React 的 renderToString 的一個(gè)封裝而已,添加了包圍 React 組件的 HTML 的基礎(chǔ)頁面的 HTML 代碼 ( <html> , <head> , <body> , 等等)。
在第 12–22行 , 我會(huì)從 GitHub API 獲取到可以填充 App 狀態(tài)的數(shù)據(jù),并且對(duì) App 組件進(jìn)行渲染。
最顯而易見是第 17 行,我使用了 StaticRouter 組件來初始化 React Router。該 Router 組件類型是采用服務(wù)端渲染方案的***選擇。它永遠(yuǎn)不會(huì)改變位置, 這是我們?cè)诒緢?chǎng)景下所需要的, 因?yàn)槭窃诤笈_(tái)上, 我們只會(huì)渲染一次,而且不會(huì)直接地對(duì)用戶的交互操作做出反應(yīng)。
而第 23 行 會(huì)捕獲在處理期間產(chǎn)生的錯(cuò)誤信息來渲染出一個(gè)錯(cuò)誤頁面。
我的 App 組件看起來像下面這樣:
export default ({ gists }) => (
<div>
<Sidebar>
{
gists ? gists.map(gist => (
<SidebarItem key={gist.id}>
<Link to={`/g/${gist.id}`}>
{gist.description || '[no description]'}
</Link>
</SidebarItem>
)) : (<p>Loading…</p>)
}
</Sidebar>
<Main>
<Route path="/" exact component={Home} />
{
gists && (
<Route path="/g/:gistId" render={ ({ match }) => (
<Gist gist={gists.find(g => g.id === match.params.gistId)} />
)
} />
)
}
</Main>
</div>
);
(→ GitHub上有完整的源代碼 )
在第 1 行 , 組件接收到作為一個(gè)屬性的 Gist 數(shù)據(jù)對(duì)象。
第 3–13行 渲染了一個(gè) Sidebar 組件,里面是連接到不同 Gist 鏈接。 SidebarItem 組件里面所包含的是只有在存在實(shí)際的 Gist 數(shù)據(jù)時(shí)才會(huì)被渲染的數(shù)據(jù)。在服務(wù)端,總會(huì)有這樣的情況發(fā)生。而我們?cè)诜?wù)端和客戶端渲染中都會(huì)用到該組件。如果組件是在客戶端被渲染的, 我們可能處在獲取新的 Gist 數(shù)據(jù)這一過程之中,所以會(huì)展示出一條 “Loading…(加載中)” 的消息。
第 15行 使用了一個(gè)來自于 React Router 庫的 Route 組件,用以在路由匹配到“/”這個(gè)路徑的時(shí)候展示出 Home 組件。這里我們使用的是精確匹配, 不然的話任何以斜線開頭的路徑都會(huì)匹配到。
如果有 Gist 數(shù)據(jù)要展示的話, 那么在第 18 行 , 另外一個(gè) Route 組件就會(huì)被用來展示一個(gè) Gist 組件,上面是被選擇的 Gist 的詳細(xì)信息。
client/index.js
如前所述,這是一個(gè) 通用 JavaScript 應(yīng)用(大家都知道的“同構(gòu)”),意思是相同的代碼即可用于渲染服務(wù)器頁面,又可以用于渲染客戶端頁面。這里摘錄了一段在客戶端初始化頁面的代碼:
render((
<BrowserRouter>
<App gists={window.__gists__} />
</BrowserRouter>
), document.getElementById('app'));
(→ GitHub 上的完整代碼 )
這比服務(wù)端代碼簡單多了! 第 1 行 的 render 函數(shù)就是 ReactDOM 的 render 函數(shù) 。它把我的 React 組件渲染出來的布局附加到 DOM 節(jié)點(diǎn)上。
第 2 行 使用了 BrowserRouter (代替了我在服務(wù)端進(jìn)行渲染使用的 StaticRouter)。
第 3 行 我使用 gist 數(shù)據(jù)對(duì) App 組件進(jìn)行實(shí)例化,以此代替通過 GitHub API 獲取數(shù)據(jù)。gist 數(shù)據(jù)來自瀏覽器 DOM 的全局變量,它是后端通過 <script> 標(biāo)簽放在那里的。
基本上就這些了!
當(dāng)我需要在瀏覽器中打開應(yīng)用程序時(shí),我可以點(diǎn)擊側(cè)邊欄中的任何 Gist。客戶端的 Reactor 路由確保每次點(diǎn)擊鏈接時(shí),頁面的網(wǎng)址都會(huì)更新,并且依賴于新網(wǎng)址的網(wǎng)頁部分會(huì)刷新。 當(dāng)我點(diǎn)擊瀏覽器的重載按鈕時(shí),后端的靜態(tài)路由確保顯示與之對(duì)應(yīng)的數(shù)據(jù)頁面。