使用 react-router v4 和 express.js 进行服务器渲染

Posted

技术标签:

【中文标题】使用 react-router v4 和 express.js 进行服务器渲染【英文标题】:server rendering with react-router v4 and express.js 【发布时间】:2017-05-20 02:25:08 【问题描述】:

我正在尝试使用最新版本的 react-router v.4 设置服务器端渲染。我遵循了本教程https://react-router.now.sh/ServerRouter。

刷新浏览器时出现以下错误:Invariant Violation: React.Children.only 预计会收到单个 React 元素子元素。

我的 routes.jsx 文件:

export default () =>
  <div>
    <Header />
    <Match pattern="/" component=Home />
    <Match pattern="/about" component=About />
    <Miss component=NotFound />
  </div>;

index.jsx 我正在渲染应用程序

import  BrowserRouter from 'react-router';    
import Routes from './routes';

ReactDOM.render(<BrowserRouter> <Routes /> </BrowserRouter>, document.getElementById('app'));

现在作为服务器,我使用 express.js。这是我的配置:

import  Routes  from '../routes';

server.use((req, res) => 
  const context = createServerRenderContext();
  let markup = renderToString(
    <ServerRouter location=req.url context=context > <Routes /> </ServerRouter>);
  const result = context.getResult();

  if (result.redirect) 
    res.writeHead(301, 
      Location: result.redirect.pathname,
    );
    res.end();
   else 
    if (result.missed) 
      res.writeHead(404);
      markup = renderToString(
        <ServerRouter location=req.url context=context> <Routes /> </ServerRouter>);
    
    res.write(markup);
    res.end();
  
);

除了官方的,我没有找到任何使用这个版本的 react-routes 进行服务器渲染的教程。 谁能帮助我我做错了什么?谢谢。

【问题讨论】:

【参考方案1】:

最大的问题是&lt;BrowserRouter&gt; 预计只有一个孩子,所以你应该将它的孩子包装在div 中。这样做是为了让 React Router 与环境无关(您不能在 React Native 中渲染 div,因此 RR 希望您包含适当的包装器)。

export default () =>
  <BrowserRouter>
    <div>
      <Header />
      <Match pattern="/" component=Home />
      <Match pattern="/about" component=About />
      <Miss component=NotFound />
    </div>
  </BrowserRouter>;

作为次要问题,您将&lt;BrowserRouter&gt; 包含在您的&lt;App&gt; 组件中,因此它将在服务器上呈现。你不想要这个。只有&lt;ServerRouter&gt; 应该在服务器上呈现。您应该将&lt;BrowserRouter&gt; 进一步向上移动客户端组件层次结构以避免这种情况。

// App
export default () =>
  <div>
    <Header />
    <Match pattern="/" component=Home />
    <Match pattern="/about" component=About />
    <Miss component=NotFound />
  </div>;

// index.js
render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('app'))

【讨论】:

感谢您的回答。我解决了这个问题,但仍然遇到同样的错误。 您能用代码的当前状态更新您的问题吗? 我更新了我的代码并删除了 App.jsx 文件,因为不再需要使用它了。 完整的错误是什么?它是否引用了代码中的特定组件或行? 删除&lt;Routes /&gt; 组件周围的空格。当您的 JSX 被转换为 React.createElement 表达式时,这些被解释为空格。【参考方案2】:

解决了!

第一个问题是我在&lt;Routes /&gt; 标签周围有空格。

正确的解决方案:

<ServerRouter location=req.url context=context><Routes /></ServerRouter>);

第二个问题是在 routes.jsx 文件中包含 &lt;Header /&gt; 标签。

我有以下错误(不变违规:元素类型无效:期望字符串(对于内置组件)或类/函数(对于复合组件)但得到:未定义。检查渲染方法StatelessComponent)

文件 Header.jsx 包含以下代码行:

import Link  from 'react-router';

正确的解决方案:(我忘了放大括号):

 import  Link  from 'react-router';

【讨论】:

【参考方案3】:

因为react-router 上不存在BrowserRouter,请尝试从react-router-dom 安装并导入它

【讨论】:

【参考方案4】:

对于以后来的人,Ryan Florence 添加了一个关于如何完成此任务的 sn-p。

s-s-r in React Router v4

// routes.js
const routes = [
  
    path: '/',
    component: Home,
    exact: true
  ,
  
    path: '/gists',
    component: Gists
  ,
  
    path: '/settings',
    component: Settings
  
]

// components
class Home extends React.Component 

  // called in the server render, or in cDM
  static fetchData(match) 
    // going to want `match` in here for params, etc.
    return fetch(/*...*/)
  

  state = 
    // if this is rendered initially we get data from the server render
    data: this.props.initialData || null
  

  componentDidMount() 
    // if rendered initially, we already have data from the server
    // but when navigated to in the client, we need to fetch
    if (!this.state.data) 
      this.constructor.fetchData(this.props.match).then(data => 
        this.setState( data )
      )
    
  

  // ...


// App.js
const App = ( routes, initialData = [] ) => (
  <div>
    routes.map((route, index) => (
      // pass in the initialData from the server for this specific route
      <Route ...route initialData=initialData[index] />
    ))
  </div>
)

// server.js
import  matchPath  from 'react-router'

handleRequest((req, res) => 
  // we'd probably want some recursion here so our routes could have
  // child routes like ` path, component, routes: [  route, route  ] `
  // and then reduce to the entire branch of matched routes, but for
  // illustrative purposes, sticking to a flat route config
  const matches = routes.reduce((matches, route) => 
    const match = matchPath(req.url, route.path, route)
    if (match) 
      matches.push(
        route,
        match,
        promise: route.component.fetchData ?
          route.component.fetchData(match) : Promise.resolve(null)
      )
    
    return matches
  , [])

  if (matches.length === 0) 
    res.status(404)
  

  const promises = matches.map((match) => match.promise)

  Promise.all(promises).then((...data) => 
    const context = 
    const markup = renderToString(
      <StaticRouter context=context location=req.url>
        <App routes=routes initialData=data/>
      </StaticRouter>
    )

    if (context.url) 
      res.redirect(context.url)
     else 
      res.send(`
        <!doctype html>
        <html>
          <div id="root">$markup</div>
          <script>DATA = $escapeBadStuff(JSON.stringify(data))</script>
        </html>
      `)
    
  , (error) => 
    handleError(res, error)
  )
)

// client.js
render(
  <App routes=routes initialData=window.DATA />,
  document.getElementById('root')
)

【讨论】:

【参考方案5】:

我相信上面列出的答案已经过时了。截至今天,官方 react-router 文档建议使用 StaticRouter 而不是 ServerRouter 用于服务器端呈现的应用程序。

可以在here.找到一个很棒的文档

【讨论】:

以上是关于使用 react-router v4 和 express.js 进行服务器渲染的主要内容,如果未能解决你的问题,请参考以下文章

react-router v4 使用 history 控制路由跳转

浅入浅出 react-router v4 实现嵌套路由

从 v3 迁移到 v4 react-router 时出现错误

react-router v3和v4区别

如何使用 react-router v4 检测路由变化?

react-router 从 v3 版本升到 v4 版本,升级小记