服务端渲染

基础

在服务端渲染路由会和客户端渲染有略微的区别,(因为服务端渲染没有状态)。基本思想是,我们将路由包裹到无状态的<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 包感兴趣,以通过静态路由配置帮助数据加载和服务器渲染。

最后修改时间: 50 seconds ago