服务器端渲染asynchronous初始化React.js组件的策略
React.js最大的优点之一就是服务器端的渲染 。 问题是关键函数React.renderComponentToString()
是同步的,因为在服务器上呈现组件层次结构时,不可能加载任何asynchronous数据。
比方说,我有一个通用的组件来评论,我可以放在页面上的任何地方。 它只有一个属性,某种types的标识符(例如,下面放置注释的文章的ID),其他所有内容都由组件本身处理(加载,添加,pipe理注释)。
我非常喜欢Flux体系结构,因为它使许多事情变得更加容易,而且它的存储对于共享服务器和客户端之间的状态是非常完美的。 一旦我的商店包含评论初始化,我可以序列化,并从服务器发送到客户端,它很容易恢复。
问题是什么是填充我的商店的最佳方式。 在过去的几天里,我一直在使用googlesearch,而且我遇到了很less的策略,考虑到React的这个特性被“提升”了多less,没有一个看起来真的很好。
-
在我看来,最简单的方法是在实际渲染开始之前填充我所有的商店。 这意味着在组件层次之外的地方(例如,挂钩到我的路由器)。 这种方法的问题是我几乎不得不两次定义页面结构。 考虑一个更复杂的页面,例如一个包含许多不同组件的博客页面(实际的博客文章,评论,相关文章,最新文章,推特stream……)。 我将不得不使用React组件devise页面结构,然后在其他地方我将不得不定义为当前页面填充每个需要的商店的过程。 这似乎不是一个很好的解决scheme给我。 不幸的是,大多数同构教程都是这样devise的(例如这个伟大的通量教程 )。
-
反应asynchronous 。 这种方法是完美的。 它让我简单地在每个组件的特殊函数中定义如何初始化状态(无论是同步还是asynchronous都无关紧要),并且在层次结构呈现给HTML时调用这些函数。 它的工作原理是一个组件在状态被完全初始化之前不会被渲染。 问题是,就我所知,它需要Fibers ,它是一个Node.js扩展,它改变了标准的JavaScript行为。 虽然我很喜欢这个结果,但在我看来,我们并不是find了解决scheme,而是改变了游戏规则。 我想我们不应该被迫使用React.js的这个核心特性。 我也不确定这个解决scheme的普遍支持。 是否有可能使用标准Node.js虚拟主机上的光纤?
-
我自己想了一下。 我没有真正想过通过实现的细节,但一般的想法是,我将以类似的方式扩展组件到React-async,然后我将重复调用根组件上的React.renderComponentToString()。 在每次过程中,我会收集扩展callback,然后在通行证处打电话给商店。 我会重复这一步,直到当前组件层次结构所需的所有存储将被填充。 有很多事情要解决,我特别不确定的performance。
我错过了什么? 还有另一种方法/解决scheme吗? 现在我正在考虑采取反应asynchronous/纤维的方式,但是我不完全确定,就像第二点所解释的那样。
关于GitHub的相关讨论 。 显然,没有官方的解决办法。 也许真正的问题是如何使用React组件。 像简单的视图层(几乎我的build议第一)或像真正的独立和独立的组件?
如果您使用react-router ,则可以在组件中定义一个willTransitionTo
方法,该方法将传递一个Transition
对象,您可以调用.wait
。
renderToString是同步的并不重要,因为直到所有.wait
ed promise都被parsing,才会调用到Router.run
的callbackRouter.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
对象(描述路由层次结构)与客户端和服务器逐字共享
我知道这可能不是你想要的,也许没有意义,但我记得稍微修改组件来处理两者:
- 在服务器端渲染,所有初始状态已经被检索,如果需要的话是asynchronous的)
- 在客户端渲染,如果需要的话用ajax
所以像这样:
/** @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 ...) });
对不起,我手边没有准确的代码,所以这可能不是现成的,但我为了讨论的兴趣发布。
同样,这个想法是把大部分组件视为愚蠢的观点,并且尽可能地从组件中处理获取数据。
今天我真的被搞混了,虽然这不是你的问题的答案,但我已经使用了这种方法。 我想使用Express而不是React Router,而我不想使用Fibers,因为我不需要在节点中支持线程。
所以我只是做了一个决定,对于初始数据需要在负载下呈现给flux store,我将执行一个AJAX请求并将初始数据传递给商店
这个例子我使用Fluxxor。
所以在我的快递路线上,在这种情况下,一个/products
路线:
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));
然后我初始化渲染方法,将数据传递给商店。
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 });
我知道这个问题在一年前被问过,但是我们遇到了同样的问题,我们用来自将要呈现的组件的嵌套promise来解决这个问题。 最后,我们获得了应用程序的所有数据,并将其发送给了我们。
例如:
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
进行服务器端渲染的方法,有点简单,比如:
-
假设我们有来自商店的初始数据的
component
:class MyComponent extends Component { constructor(props) { super(props); this.state = { data: myStore.getData() }; } }
-
如果类需要一些预加载的初始状态的数据,我们来创build
MyComponent
的Loader:class MyComponentLoader { constructor() { myStore.addChangeListener(this.onFetch); } load() { return new Promise((resolve, reject) => { this.resolve = resolve; myActions.getInitialData(); }); } onFetch = () => this.resolve(data); }
-
商店:
class MyStore extends StoreBase { constructor() { switch(action => { case 'GET_INITIAL_DATA': this.yourFetchFunction() .then(response => { this.data = response; this.emitChange(); }); break; } getData = () => this.data; }
-
现在只需在路由器中加载数据:
on('/my-route', async () => { await new MyComponentLoader().load(); return <MyComponent/>; });