异步初始化 React.js 组件的服务器端呈现策略

React.js最大的优点之一应该是 服务器端渲染服务器端渲染。问题是关键函数 React.renderComponentToString()是同步的,这使得在服务器上呈现组件层次结构时不可能加载任何异步数据。

假设我有一个通用的评论组件,我可以把它放在页面的任何地方。它只有一个属性,某种标识符(例如,一篇文章的 id,注释放在其下) ,其他所有事情都由组件本身处理(加载、添加、管理注释)。

我非常喜欢 变化架构,因为它使很多事情变得更容易,而且它的 商店非常适合在服务器和客户端之间共享状态。一旦我的包含注释的存储被初始化,我可以只序列化它,并将它从服务器发送到客户机,在那里它很容易恢复。

问题是怎样才能最好地填充我的商店。在过去的日子里,我一直在谷歌搜索很多,我遇到了几个策略,没有一个看起来真的很好,考虑到多少这个功能的反应是“促进”。

  1. 在我看来,最简单的方法是在实际呈现开始之前填充我的所有存储。这意味着在组件层次结构之外的某个地方(例如连接到我的路由器)。这种方法的问题在于,我必须两次定义页面结构。考虑一个更复杂的页面,例如一个包含许多不同组件的博客页面(实际的博客帖子、评论、相关帖子、最新帖子、 twitter 流...)。我必须使用 React 组件设计页面结构,然后在其他地方定义填充当前页面所需的每个存储的过程。我觉得这不是个好办法。不幸的是,大多数同构教程都是这样设计的(例如这个很棒的 通量教程)。

  2. 反应-异步。这个方法很完美。它允许我在每个组件中的一个特殊函数中定义如何初始化状态(无论是同步还是异步) ,并且当层次结构呈现为 HTML 时调用这些函数。它的工作方式是在完全初始化状态之前不呈现组件。问题是它需要 Fibers,据我所知,Fibers是一个 Node.js 扩展,可以改变标准的 JavaScript 行为。虽然我真的很喜欢这个结果,但在我看来,我们并没有找到解决方案,而是改变了游戏规则。我认为我们不应该被迫使用 React.js 的这个核心特性。我也不确定这个解决方案是否得到普遍支持。是否可以在标准的 Node.js 主机上使用光纤?

  3. I was thinking a little on my own. I haven't really thought trough the implementation details but the general idea is that I would extend the components in similar way to React-async and then I would repeatedly call React.renderComponentToString() on the root component. During each pass I would collect the extending callbacks and then call them at the and of the pass to populate the stores. I would repeat this step until all stores required by current component hierarchy would be populated. There are many things to be solved and I'm particularly unsure about the performance.

我错过什么了吗?是否有其他方法/解决办法?现在,我正在考虑采用异步反应/纤维反应的方式,但是我不能完全确定是否如第二点所解释的那样。

GitHub 相关讨论。显然,目前还没有正式的方法,甚至没有解决方案。也许真正的问题是如何使用反应组件。喜欢简单的视图层(基本上我的建议第一)或喜欢真正的独立和独立的组件?

15688 次浏览

我知道这可能不完全是您想要的,也可能没有意义,但是我记得稍微修改组件来处理这两者:

  • 在服务器端呈现,所有的初始状态已经被检索,如果需要的话可以异步呈现)
  • 在客户端呈现,如果需要,使用 ajax

So something like :

/** @jsx React.DOM */


var UserGist = React.createClass({
getInitialState: function() {


if (this.props.serverSide) {
return this.props.initialState;
} else {
return {
username: '',
lastGistUrl: ''
};
}


},


componentDidMount: function() {
if (!this.props.serverSide) {


$.get(this.props.source, function(result) {
var lastGist = result[0];
if (this.isMounted()) {
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}
}.bind(this));


}


},


render: function() {
return (
<div>
{this.state.username}'s last gist is
<a href={this.state.lastGistUrl}>here</a>.
</div>
);
}
});


// On the client side
React.renderComponent(
<UserGist source="https://api.github.com/users/octocat/gists" />,
mountNode
);


// On the server side
getTheInitialState().then(function (initialState) {


var renderingOptions = {
initialState : initialState;
serverSide : true;
};
var str = Xxx.renderComponentAsString( ... renderingOptions ...)


});

对不起,我手头没有准确的代码,所以这可能不适用于盒子,但我发布的兴趣讨论。

同样,我们的想法是将组件的大部分视为哑视图,并尽可能多地处理组件的 出去数据获取。

如果使用 反应路由器,只需在组件中定义一个 willTransitionTo方法,就可以传递一个可以调用 .waitTransition对象。

RenderToString 是否同步并不重要,因为在解决所有 .waited 承诺之前,不会调用对 Router.run的回调,所以在中间件中调用 renderToString时,您可能已经填充了存储。即使存储是单例的,您也可以在同步呈现调用之前临时实时地设置它们的数据,组件就会看到它们。

中间件示例:

var Router = require('react-router');
var React = require("react");
var url = require("fast-url-parser");


module.exports = function(routes) {
return function(req, res, next) {
var path = url.parse(req.url).pathname;
if (/^\/?api/i.test(path)) {
return next();
}
Router.run(routes, path, function(Handler, state) {
var markup = React.renderToString(<Handler routerState={state} />);
var locals = {markup: markup};
res.render("layouts/main", locals);
});
};
};

routes对象(描述路由层次结构)与客户机和服务器逐字共享

我今天真的搞砸了,虽然这不是你的问题的答案,我已经使用了这种方法。我想使用 Express 路由而不是 React 路由器,我不想使用纤程,因为我不需要在节点中支持线程。

所以我决定,对于需要在加载时呈现到通量存储的初始数据,我将执行一个 AJAX 请求并将初始数据传递到存储中

我在这个例子中使用了 Flexxor。

So on my express route, in this case a /products route:

var request = require('superagent');
var url = 'http://myendpoint/api/product?category=FI';


request
.get(url)
.end(function(err, response){
if (response.ok) {
render(res, response.body);
} else {
render(res, 'error getting initial product data');
}
}.bind(this));

然后,我的 initialize 呈现方法将数据传递给存储。

var render = function (res, products) {
var stores = {
productStore: new productStore({category: category, products: products }),
categoryStore: new categoryStore()
};


var actions = {
productActions: productActions,
categoryActions: categoryActions
};


var flux = new Fluxxor.Flux(stores, actions);


var App = React.createClass({
render: function() {
return (
<Product flux={flux} />
);
}
});


var ProductApp = React.createFactory(App);
var html = React.renderToString(ProductApp());
// using ejs for templating here, could use something else
res.render('product-view.ejs', { app: html });

我知道这个问题是一年前提出的,但是我们遇到了同样的问题,我们用嵌套的承诺来解决这个问题,这些承诺来自即将被渲染的组件。最后,我们有了应用程序的所有数据,只是发送下来的方式。

例如:

var App = React.createClass({


/**
*
*/
statics: {
/**
*
* @returns {*}
*/
getData: function (t, user) {


return Q.all([


Feed.getData(t),


Header.getData(user),


Footer.getData()


]).spread(
/**
*
* @param feedData
* @param headerData
* @param footerData
*/
function (feedData, headerData, footerData) {


return {
header: headerData,
feed: feedData,
footer: footerData
}


});


}
},


/**
*
* @returns {XML}
*/
render: function () {


return (
<label>
<Header data={this.props.header} />
<Feed data={this.props.feed}/>
<Footer data={this.props.footer} />
</label>
);


}


});

还有路由器

var AppFactory = React.createFactory(App);


App.getData(t, user).then(
/**
*
* @param data
*/
function (data) {


var app = React.renderToString(
AppFactory(data)
);


res.render(
'layout',
{
body: app,
someData: JSON.stringify(data)
}
);


}
).fail(
/**
*
* @param error
*/
function (error) {
next(error);
}
);

想与你分享我的方法,使用 Flux服务器端渲染,几乎没有简化的例子:

  1. 假设我们有 component和来自存储的初始数据:

    class MyComponent extends Component {
    constructor(props) {
    super(props);
    this.state = {
    data: myStore.getData()
    };
    }
    }
    
  2. If class require some preloaded data for initial state let's create Loader for MyComponent:

     class MyComponentLoader {
    constructor() {
    myStore.addChangeListener(this.onFetch);
    }
    load() {
    return new Promise((resolve, reject) => {
    this.resolve = resolve;
    myActions.getInitialData();
    });
    }
    onFetch = () => this.resolve(data);
    }
    
  3. Store:

    class MyStore extends StoreBase {
    constructor() {
    switch(action => {
    case 'GET_INITIAL_DATA':
    this.yourFetchFunction()
    .then(response => {
    this.data = response;
    this.emitChange();
    });
    break;
    }
    getData = () => this.data;
    }
    
  4. Now just load data in router:

    on('/my-route', async () => {
    await new MyComponentLoader().load();
    return <MyComponent/>;
    });
    

正如一个简短的汇总-> GraphQL 将完全为您的堆栈解决这个问题..。

  • 添加 GraphQL
  • 使用阿波罗和反应-阿波罗
  • 在开始呈现之前使用“ getDataFromTree”

- > getDataFromTree 将自动找到所有相关的查询,并执行它们,将您的 Apollo 缓存填充到服务器上,从而启用完全工作的 SSR。.BÄM