Server Rendering | 服务器端渲染

在服务端渲染对比客户端有许多不同, 因为服务器端是无状态的. 这个想法源自于我们包装app时, 曾用无状态的 <StaticRouter> 取代了 <BrowserRouter>, 我们通过来自于服务端的请求url使得路由能够匹配上. 我们接下来会讨论 context 属性 Rendering on the server is a bit different since it’s all stateless. The basic idea is that we wrap the app in a stateless <StaticRouter> instead of a <BrowserRouter>. We pass in the requested url from the server so the routes can match and a context prop we’ll discuss next.

  1. // client
  2. <BrowserRouter>
  3. <App/>
  4. </BrowserRouter>
  5. // server (not the complete story)
  6. <StaticRouter
  7. location={req.url}
  8. context={context}
  9. >
  10. <App/>
  11. </StaticRouter>

当你在客户端渲染 <Redirect>, 浏览器历史会改变状态使我们能获得新的视图, 在一个静态的服务环境下, 我们不能够改变app的状态. 作为替代, 我们使用 context 属性去找到渲染的出来的结果是什么. 如果我们找到了 context.url, 那么我们知道这个app重定向了. 这能让我们向服务端发送一个重定向请求 When you render a <Redirect> on the client, the browser history changes state and we get the new screen. In a static server environment we can’t change the app state. Instead, we use the context prop to find out what the result of rendering was. If we find a context.url, then we know the app redirected. This allows us to send a proper redirect from the server.

  1. const context = {}
  2. const markup = ReactDOMServer.renderToString(
  3. <StaticRouter
  4. location={req.url}
  5. context={context}
  6. >
  7. <App/>
  8. </StaticRouter>
  9. )
  10. if (context.url) {
  11. // Somewhere a `<Redirect>` was rendered
  12. redirect(301, context.url)
  13. } else {
  14. // we're good, send the response
  15. }

Adding app specific context information

路由只能添加 context.url. 但是你可能需要重定向时发送一个 30x 的响应. 或许你在某些特别的UI渲染后需要发送一个404响应, 又甚者没有认证的情况下发送401. context属性是属于你的, 你可以改变它. 而这里我们给出了分辨301与302重定向的方法 The router only ever adds context.url. But you may want some redirects to be 301 and others 302. Or maybe you’d like to send a 404 response if some specific branch of UI is rendered, or a 401 if they aren’t authorized. The context prop is yours, so you can mutate it. Here’s a way to distinguish between 301 and 302 redirects:

  1. const RedirectWithStatus = ({ from, to, status }) => (
  2. <Route render={({ staticContext }) => {
  3. // there is no `staticContext` on the client, so
  4. // we need to guard against that here
  5. if (staticContext)
  6. staticContext.status = status
  7. return <Redirect from={from} to={to}/>
  8. }}/>
  9. )
  10. // somewhere in your app
  11. const App = () => (
  12. <Switch>
  13. {/* some other routes */}
  14. <RedirectWithStatus
  15. status={301}
  16. from="/users"
  17. to="/profiles"
  18. />
  19. <RedirectWithStatus
  20. status={302}
  21. from="/courses"
  22. to="/dashboard"
  23. />
  24. </Switch>
  25. )
  26. // on the server
  27. const context = {}
  28. const markup = ReactDOMServer.renderToString(
  29. <StaticRouter context={context}>
  30. <App/>
  31. </StaticRouter>
  32. )
  33. if (context.url) {
  34. // can use the `context.status` that
  35. // we added in RedirectWithStatus
  36. redirect(context.status, context.url)
  37. }

404, 401, 或者其他状态

We can do the same thing as above. Create a component that adds some context and render it anywhere in the app to get a different status code.

  1. const Status = ({ code, children }) => (
  2. <Route render={({ staticContext }) => {
  3. if (staticContext)
  4. staticContext.status = code
  5. return children
  6. }}/>
  7. )

Now you can render a Status anywhere in the app that you want to add the code to staticContext.

  1. const NotFound = () => (
  2. <Status code={404}>
  3. <div>
  4. <h1>Sorry, cant find that.</h1>
  5. </div>
  6. </Status>
  7. )
  8. // somewhere else
  9. <Switch>
  10. <Route path="/about" component={About}/>
  11. <Route path="/dashboard" component={Dashboard}/>
  12. <Route component={NotFound}/>
  13. </Switch>

Putting it all together

这并不是一个真正的app, 但是它会展示所有你需要放在一起的组件 This isn’t a real app, but it shows all of the general pieces you’ll need to put it all together.

  1. import { createServer } from 'http'
  2. import React from 'react'
  3. import ReactDOMServer from 'react-dom/server'
  4. import { StaticRouter } from 'react-router'
  5. import App from './App'
  6. createServer((req, res) => {
  7. const context = {}
  8. const html = ReactDOMServer.renderToString(
  9. <StaticRouter
  10. location={req.url}
  11. context={context}
  12. >
  13. <App/>
  14. </StaticRouter>
  15. )
  16. if (context.url) {
  17. res.writeHead(301, {
  18. Location: context.url
  19. })
  20. res.end()
  21. } else {
  22. res.write(`
  23. <!doctype html>
  24. <div id="app">${html}</div>
  25. `)
  26. res.end()
  27. }
  28. }).listen(3000)

And then the client:

  1. import ReactDOM from 'react-dom'
  2. import { BrowserRouter } from 'react-router-dom'
  3. import App from './App'
  4. ReactDOM.render((
  5. <BrowserRouter>
  6. <App/>
  7. </BrowserRouter>
  8. ), document.getElementById('app'))

Data Loading

There are so many different approaches to this, and there’s no clear best practice yet, so we seek to be composable with any approach, and not prescribe or lean toward one or the other. We’re confident the router can fit inside the constraints of your application.

The primary constraint is that you want to load data before you render. React Router exports the matchPath static function that it uses internally to match locations to routes. You can use this function on the server to help determine what your data dependencies will be before rendering.

The gist of this approach relies on a static route config used to both render your routes and match against before rendering to determine data dependencies.

  1. const routes = [
  2. { path: '/',
  3. component: Root,
  4. loadData: () => getSomeData(),
  5. },
  6. // etc.
  7. ]

Then use this config to render your routes in the app:

  1. import { routes } from './routes'
  2. const App = () => (
  3. <Switch>
  4. {routes.map(route => (
  5. <Route {...route}/>
  6. ))}
  7. </Switch>
  8. )

Then on the server you’d have something like:

  1. import { matchPath } from 'react-router-dom'
  2. // inside a request
  3. const promises = []
  4. // use `some` to imitate `<Switch>` behavior of selecting only
  5. // the first to match
  6. routes.some(route => {
  7. // use `matchPath` here
  8. const match = matchPath(req.url, route)
  9. if (match)
  10. promises.push(route.loadData(match))
  11. return match
  12. })
  13. Promise.all(promises).then(data => {
  14. // do something w/ the data so the client
  15. // can access it then render the app
  16. })

And finally, the client will need to pick up the data. Again, we aren’t in the business of prescribing a data loading pattern for your app, but these are the touch points you’ll need to implement.

You might be interested in our React Router Config package to assist with data loading and server rendering with static route configs.