服务端渲染
基础
在服务端渲染路由会和客户端渲染有略微的区别,(因为服务端渲染没有状态)。基本思想是,我们将路由包裹到无状态的<StaticRouter>
中,而不是包含状态的<BrowserRouter>
。 我们从服务端请求中获得 url 参数并将它传给<StaticRouter>
去做路径匹配。下一步,我们会讨论 context 属性。
// client
<BrowserRouter>
<App/>
</BrowserRouter>
// server (not the complete story)
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
当你在客户端渲染<Redirect>
组件时,react-router 会往 history API 写入新的记录并渲染新的页面。而在服务端渲染场景中,我们无法控制 web 应用的状态。作为替代,我们用 context 属性来获得实际渲染的结果。如果我们能获取到 context.url,那代表应用被重定向了。接下来我们可以发起相应的重定向请求
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
if (context.url) {
// Somewhere a `<Redirect>` was rendered
redirect(301, context.url)
} else {
// we're good, send the response
}
添加自定义的 context 信息
现在,路由器仅仅能控制 context.url。但实际场景中,你可能会想让部分重定向请求使用 301,而其他的使用 302,或者,你可能想在某些路径匹配中渲染 404 页面,亦或 401 如果用户没有权限。实际上,你拥有整个 context 对象的权限,你可以随意的改写它。下列示例区分了 301,302 重定向:
function RedirectWithStatus({ from, to, status }) {
return (
<Route
render={({ staticContext }) => {
// there is no `staticContext` on the client, so
// we need to guard against that here
if (staticContext) staticContext.status = status
return <Redirect from={from} to={to} />
}}
/>
)
}
// somewhere in your app
function App() {
return (
<Switch>
{/* some other routes */}
<RedirectWithStatus status={301} from="/users" to="/profiles" />
<RedirectWithStatus status={302} from="/courses" to="/dashboard" />
</Switch>
)
}
// on the server
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter context={context}>
<App />
</StaticRouter>
)
if (context.url) {
// can use the `context.status` that
// we added in RedirectWithStatus
redirect(context.status, context.url)
}
404, 401 或其他状态
我们可以做与上述相同的事情。创建一个添加 context 的组件,并将其呈现在 web 应用中的任何位置以获取不同的状态代码。 现在,你可以在应用的任意位置渲染 Status 来改变 context 的值。
function Status({ code, children }) {
return (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = code;
return children;
}}
/>
);
}
NotFound() {
return (
<Status code={404}>
<div>
<h1>Sorry, can’t find that.</h1>
</div>
</Status>
);
}
function App() {
return (
<Switch>
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
<Route component={NotFound} />
</Switch>
);
}
整合起来
上面示例并不是一个完整的应用,但他已经包含了你构建一个服务端渲染应用的基本部分。
// 服务端
import http from 'http'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import App from './App.js'
http
.createServer((req, res) => {
const context = {}
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
)
if (context.url) {
res.writeHead(301, {
Location: context.url
})
res.end()
} else {
res.write(`
<!doctype html>
<div id="app">${html}</div>
`)
res.end()
}
})
.listen(3000)
//客户端
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App.js'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('app')
)
加载数据
数据加载的方式有很多很多种,而社区目前也没有提炼出最佳实践,所以我们力求解决方案能和任何方式共同工作,而不是倾向并绑定一种解决方案。
主要限制是您要在渲染之前加载数据。React Router 导出 matchPath 其内部使用的静态功能,以将位置与路线匹配。您可以在服务器上使用此功能来帮助确定呈现之前的数据依存关系。
这种方法的要旨是依赖于静态路由配置,该配置既可以呈现您的路由,也可以在呈现之前进行匹配以确定数据依赖。
const routes = [
{
path: "/",
component: Root,
loadData: () => getSomeData()
}
// etc.
];```
接着用这个配置项渲染你的<Route>
组件:
import { routes } from './routes.js'
function App() {
return (
<Switch>
{routes.map(route => (
<Route {...route} />
))}
</Switch>
)
}
在服务端,你会有类似于此的代码:
import { matchPath } from 'react-router-dom'
// inside a request
const promises = []
// 使用 some 方法来确保仅匹配第一项找到的路由
routes.some(route => {
// use `matchPath` here
const match = matchPath(req.path, route)
if (match) promises.push(route.loadData(match))
return match
})
Promise.all(promises).then(data => {
// 操作数据以便客户端能够访问他并根据其渲染页面
})
最后,客户将需要提取数据。同样,我们不为您的应用程序规定数据加载模式,但是这些是您需要去手动实现的。您可能对我们的 React Router Config 包感兴趣,以通过静态路由配置帮助数据加载和服务器渲染。