React Router 是一个在 React 领域非常流行的库。它依靠位置栏上的请求 URL 和 浏览器的操作历史来渲染不同的页面内容来保持显示,进而将你的 app 同步到用户接口的页面上。
新的闪闪发亮
最近,版本4的 React Router 已经进入 beta 发布阶段。损誉各半,React Router 的这一次发布是对上一版本的完整重写,这导致了很多破坏性的 API 变更。 在版本 4 的核心理念是 “声明式可组合性(declarative composability)”?—— 它包含 React 之所以优秀的组件概念,并将其应用于 routing。React Router 4 的每一个部分都是 React 的组件:Router, Route, Link 等等。 React Router 的一位开发者,Ryan Florence,亲手制作了一个简短的视频来介绍最新的 React Router,这段视频获得了很多人的推荐: 视频:https://youtu.be/a4kqMQorcnE
后台如何?
新版本的 React Router 奉上了一个新的 web 页面,上面有许多实用的代码示例,但没有提供实例介绍如何在服务端使用 React Router 来进行基于 React 的页面的渲染。 对于我近期正在进行的项目,对搜索引擎友好且具备最佳的网站运行速度是重中之重,难道这样就要在客户端渲染整个页面?难道就用示例页面上所有实例所采取的办法?那是不可取的。我们要使用一个 Express 服务器来在后台对 React 页面进行渲染。 在其介绍视屏中, Ryan 有一个可以从某些 API 获取数据来初始化其状态的 App 组件, 使用的是 componentDidMount 生命周期方法。但异步数据的获取操作完毕,组件就会被更新以显示数据。 但是当要在服务端对 App 组件进行渲染的时候这样做不会有效果: 在你使用 renderToString 的时候, 带有 HTML 代码的字符串在调用了组件的渲染方法之后就会被同步地创建出来。componentDidMount 从未被调用到。 因此如果我们使用 Ryan 视频里的示例在后台渲染出 App 组件,它只会生成一条 “Loading…” 消息。
解决方案
作为对概念方案的验证,我创建了一个 demo 应用,基本上就是对视频中 Ryan 的示例进行重造,但采取的是服务器端的渲染。 应用使用了 GitHub API 去获取有关于 Gist 代码片段的数据:
代码展示
你可以在 Github 上找到 demo 应用的源代码: https://github.com/technology-ebay-de/universal-react-router4 简而言之,下面就是我所做的事情…
server/index.js
这就是每次有 HTTP 请求发到 Express 服务器的时候都会跑一次的代码:
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 找到完整的源代码)
App.js
我的 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 行, 组件接收到作为一个属性的 Gist 数据对象。 第 3–13行 渲染了一个 Sidebar 组件,里面是连接到不同 Gist 链接。 SidebarItem 组件里面所包含的是只有在存在实际的 Gist 数据时才会被渲染的数据。在服务端,总会有这样的情况发生。而我们在服务端和客户端渲染中都会用到该组件。如果组件是在客户端被渲染的, 我们可能处在获取新的 Gist 数据这一过程之中,所以会展示出一条 “Loading…(加载中)” 的消息。 第 15行 使用了一个来自于 React Router 库的 Route 组件,用以在路由匹配到“/”这个路径的时候展示出 Home 组件。这里我们使用的是精确匹配, 不然的话任何以斜线开头的路径都会匹配到。 如果有 Gist 数据要展示的话, 那么在第 18 行, 另外一个 Route 组件就会被用来展示一个 Gist 组件,上面是被选择的 Gist 的详细信息。
client/index.js
如前所述,这是一个通用 JavaScript 应用(大家都知道的“同构”),意思是相同的代码即可用于渲染服务器页面,又可以用于渲染客户端页面。这里摘录了一段在客户端初始化页面的代码:
render((
<BrowserRouter>
<App gists={window.__gists__} />
</BrowserRouter>
), document.getElementById('app'));
(→ GitHub 上的完整代码) 这比服务端代码简单多了!第 1 行的 render 函数就是 ReactDOM 的 render 函数。它把我的 React 组件渲染出来的布局附加到 DOM 节点上。 第 2 行使用了 BrowserRouter (代替了我在服务端进行渲染使用的 StaticRouter)。 第 3 行我使用 gist 数据对 App 组件进行实例化,以此代替通过 GitHub API 获取数据。gist 数据来自浏览器 DOM 的全局变量,它是后端通过 <script> 标签放在那里的。
基本上就这些了!
当我需要在浏览器中打开应用程序时,我可以点击侧边栏中的任何 Gist。客户端的 Reactor 路由确保每次点击链接时,页面的网址都会更新,并且依赖于新网址的网页部分会刷新。 当我点击浏览器的重载按钮时,后端的静态路由确保显示与之对应的数据页面。
UTO