反应服务器端提取(异步呈现)

时间:2017-06-27 17:54:55

标签: reactjs

简而言之,我如何在服务器上完成这项工作?

import React from 'react';

export default class RemoteText extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {text: null};
        fetch(props.src).then(res => res.text()).then(text => {
            this.setState({text});
        })
    }

    render() {
        if(this.state.text) {
            return <span>{this.state.text}</span>;
        }
        return null;
    }
}

即使我使用isomorphic-fetch,我也会收到此警告:

  

警告:setState(...):只能更新安装组件。这通常意味着您在服务器上的componentWillMount()之外调用了setState()。这是一个无操作。请检查RemoteText组件的代码。

哪个有道理。我相信我的组件在获取结束时已经被渲染为一个字符串。我需要以某种方式推迟渲染,直到完成,但我不知道如何。

Apollo以某种方式用getDataFromTree管理这个,所以我确定它是可能的。

我怎么能做同样的事情?即,遍历我的组件树,提取所有待处理的提取,然后在ReactDOMServer.renderToString之前等待它们解决?

由于它似乎不清楚,我想重申我希望抓取发生服务器端并且所有数据都应该在之前加载 strong>第一个渲染。提取应与组件共存(可能通过HOC)。

如果我可以在renderToString之前将数据存入我的Redux商店,我已经有了一些代码来序列化它并将其传递给客户端。作为参考,我正在渲染我的应用程序:

export default async (req,res) => {
    const rrCtx = {};
    const store = createStore(combineReducers({
        apollo: gqlClient.reducer(),
    }), {}, applyMiddleware(gqlClient.middleware()));

    const Chain = (
        <ApolloProvider store={store} client={gqlClient}>
            <StaticRouter location={req.url} context={rrCtx}>
                <App/>
            </StaticRouter>
        </ApolloProvider>
    );

    await getDataFromTree(Chain); // <--- waits for GraphQL data to resolve before first render -- I wan the same but for fetch()
    const html = ReactDOMServer.renderToString(Chain);

    if(rrCtx.url) {
        ctx.redirect(rrCtx.url);
    } else {


        const stats = await readJson(`${__dirname}/../webpack/stats.json`);
        const styles = stats.assetsByChunkName.main.filter(f => f.endsWith('.css'));
        const scripts = stats.assetsByChunkName.main.filter(f => f.endsWith('.js'));

        let manifest = {};
        try {
            manifest = await readJson(`${__dirname}/../webpack/manifest.json`);
        } catch(_){}

        if(stats.assetsByChunkName.vendor) {
            styles.unshift(...stats.assetsByChunkName.vendor.filter(f => f.endsWith('.css')));
            scripts.unshift(...stats.assetsByChunkName.vendor.filter(f => f.endsWith('.js')));
        }

        res.send(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="Content-Language" content="en" />
  <title>RedSpider</title>
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  ${styles.map(f => `<link rel="stylesheet" href="${stats.publicPath}${f}">`).join('\n')}
  <link rel="icon" type="image/png" href="/spider.png">
  ${scripts.map(f => `<script src="${stats.publicPath}${f}" defer></script>`).join('\n')}
</head>
<body>
  <div id="react-root">${html}</div>
  <script>
  window.__STATE__=${jsSerialize(store.getState())};
  window.__MANIFEST__=${jsSerialize(manifest)};
  </script>
</body>
</html>`);
    }
}

我正在寻找类似于react-apollo的内容,但适用于任意提取/ GET请求,而不仅仅是GraphQL查询。

每个人都说&#34;只是await它&#34;。等什么?提取是组件中的,我需要等待的位置在另一个文件中。问题是我如何收集fetch-promises,以便我可以等待它们,然后将这些数据恢复到组件中?

但是没关系,我会尝试逆向工程getDataFromTree并看看我是否可以开始工作。

4 个答案:

答案 0 :(得分:2)

当您从服务器调用renderToString时,react将调用构造函数,componentWillMount并呈现,然后从呈现的元素创建一个字符串。由于异步操作将在另一个tick上解析,因此不能使用componentWillMount或构造函数中的fetch。

更简单的方法(如果你不使用任何处理它的框架/ lib)就是在调用renderToString之前解析fetch

答案 1 :(得分:2)

  

我相信我的组件在获取结束时已经被渲染为一个字符串。我需要以某种方式推迟渲染,直到完成,但我不知道如何。 [...]我可以在我调用renderToString的地方获取数据(不是我想要的),但是我如何将其线程回到组件中?

AFAIK,您必须await任何异步调用完成,然后才能以某种方式调用renderToString

这个问题并不常见,虽然这只是一个通用的答案,因为我使用自定义的flux实现而不是Redux。此外,此问题并不是React组件树的服务器端呈现所特有的,因为您可能需要等待客户端才能继续呈现到DOM中。

基本上,我如何解决这个特定问题(无论是在客户端还是服务器上)如下:

  1. 在调用await之前只需fetch renderToString(正如我所提到的,这几乎是不可避免的,因为您的数据必须存在才能继续响应)
  2. fetch结果存储在您的商店
  3. 如果该特定数据尚未“预加载”到您的商店,
  4. 在该组件中,只会为数据发起fetch
  5. 在渲染器入口点,简单地说(虽然我相信这应该是显而易见的):

    export default async (req, res) => {
        await fetch().then(res => res.text()).then(text => {
            this.setState({text});
        });
    
        // Store your data in your store here and instantiate your components.
    
        const html = ReactDOMServer.renderToString(Chain);
    }
    

    在您的组件中:

    export default class RemoteText extends React.PureComponent {
        constructor(props, context) {
            super(props, context);
    
            // If your store has the data you need, set it directly.
            // Otherwise, call your fetch so that it works on client seamlessly.
        }
    }
    

    总的来说,这将确保您的组件在客户端上也能正常工作,并且在服务器上预呈现时具有所需的数据呈现。我担心你的取消电话awaiting无法替代。

答案 2 :(得分:0)

如上所述,请在componentWillMount

中执行异步操作
componentWillMount=()=>{
    fetch(props.src).then(res => res.text()).then(text => {
        this.setState({text});
    })
}

答案 3 :(得分:-1)

夫妻俩。

fetch请求移出构造函数,它不属于那里。 setState()也不属于构造函数。

fetch请求和后续setState()调用属于componentDidMount(),而不是构造函数。

此外,当您拨打setState()电话时,您需要setState({text: text})而不是setState({text})。作为旁注,放弃具有单行的箭头函数的块是惯用的,尤其是在链接获取请求时。同样,将链式语句放在单独的行上可以提高可读性。即:

fetch(props.src)
.then(res => res.text())
.then(text => this.setState({text: text}))

如果您愿意,可以在最后链接.catch(error => console.log(error));。同样重要的是,将fetch请求和setState()电话移至componentDidMount()

定义通用的获取方法是合适的(特别是如果其他地方会有更多);它可以接受requestObj,并将回调作为参数。您可以在componentDidMount()内以及在您的应用中进行抓取请求的任何其他位置调用它。

render()来电中,将您返回的所有内容封装在顶级div中。在渲染调用中不需要返回null,即使this.state.text为null,它仍然是不必要的。相反,当你在开始时学习React时,你最好总是返回一个div。如果您在使用内联运算符进行条件渲染之前没有看到它,它看起来像这样:     render(){       const text = this.state.text;

  return (
    <div>
      {text &&
        <span>{text}</span>
      }
    </div>
  );
}

编辑:我第一眼就误解了你的目标。如果您想要的是在渲染之前加载所有数据,请通过props将数据传递给组件。我不确定组件树的结构是什么样的,或者你在服务器端做什么,但如果你不能单独使用props传递数据,则通过上下文将它传递给组件。为此,您需要一个更高阶的组件,将您的数据设置为上下文。 您现有的构造函数将如下所示:

constructor(props, context) {

  super(props, context);
  this.state = this.context.text || window.__INITIAL_STATE__ || {text: null};
}