node.js - node - Estrategias para la representación del lado del servidor de los componentes React.js inicializados de forma asíncrona
react js stack (5)
Estuve realmente enredado con esto hoy, y aunque esta no es una respuesta a tu problema, he usado este enfoque. Quería usar Express para el enrutamiento en lugar de React Router, y no quería usar Fibras ya que no necesitaba el soporte de subprocesos en el nodo.
Así que tomé la decisión de que para los datos iniciales que se deben presentar en la tienda de flujo en carga, realizaré una solicitud de AJAX y pasaré los datos iniciales a la tienda.
Estaba usando Fluxxor para este ejemplo.
Entonces, en mi ruta expresa, en este caso una ruta de /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));
Luego mi método de renderización de inicialización que pasa los datos a la tienda.
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 });
Se supone que una de las mayores ventajas de React.js es la representación del lado del servidor . El problema es que la función clave React.renderComponentToString()
es síncrona, lo que hace imposible cargar datos asíncronos a medida que la jerarquía de componentes se procesa en el servidor.
Digamos que tengo un componente universal para comentar que puedo colocar casi en cualquier parte de la página. Tiene solo una propiedad, algún tipo de identificador (por ejemplo, id de un artículo debajo del cual se colocan los comentarios), y todo lo demás lo maneja el componente en sí (cargar, agregar, administrar comentarios).
Me gusta mucho la arquitectura Flux porque facilita mucho las cosas y sus tiendas son perfectas para compartir el estado entre el servidor y el cliente. Una vez que se inicializa mi tienda que contiene comentarios, puedo serializarla y enviarla de servidor a cliente, donde se restaura fácilmente.
La pregunta es cuál es la mejor manera de poblar mi tienda. Durante los últimos días he estado buscando en Google y he encontrado algunas estrategias, ninguna de las cuales parecía realmente buena teniendo en cuenta cuánto se está "promocionando" esta característica de React.
En mi opinión, la forma más sencilla es poblar todas mis tiendas antes de que comience la renderización real. Eso significa que está fuera de la jerarquía de componentes (enganchado a mi enrutador, por ejemplo). El problema con este enfoque es que tendría que definir bastante la estructura de la página dos veces. Considere una página más compleja, por ejemplo, una página de blog con muchos componentes diferentes (publicación de blog real, comentarios, publicaciones relacionadas, publicaciones más recientes, transmisión de Twitter ...). Tendría que diseñar la estructura de la página utilizando los componentes de React y luego en otro lugar tendría que definir el proceso de llenar cada tienda requerida para esta página actual. Eso no parece una buena solución para mí. Desafortunadamente, la mayoría de los tutoriales isomórficos están diseñados de esta manera (por ejemplo, este gran flux-tutorial ).
React-async . Este enfoque es perfecto. Me permite simplemente definir en una función especial en cada componente cómo inicializar el estado (no importa si es sincrónico o asíncrono) y estas funciones se invocan a medida que la jerarquía se representa en HTML. Funciona de manera que un componente no se representa hasta que el estado se inicializa por completo. El problema es que requiere Fibers que es, por lo que yo entiendo, una extensión Node.js que altera el comportamiento estándar de JavaScript. Aunque realmente me gusta el resultado, todavía me parece que en lugar de encontrar una solución, cambiamos las reglas del juego. Y creo que no deberíamos obligarnos a hacer eso para usar esta función principal de React.js. Tampoco estoy seguro del soporte general de esta solución. ¿Es posible utilizar Fiber en el alojamiento web estándar Node.js?
Estaba pensando un poco por mi cuenta. Realmente no he pensado a través de los detalles de la implementación, pero la idea general es que ampliaría los componentes de manera similar a React-async y luego repetidamente llamaría a React.renderComponentToString () en el componente raíz. Durante cada pasada, recolectaría las devoluciones de llamada y luego las llamaría al y del pase para llenar las tiendas. Repito este paso hasta que se completen todas las tiendas requeridas por la jerarquía de componentes actual. Hay muchas cosas por resolver y estoy particularmente inseguro sobre el rendimiento.
¿Me he perdido algo? ¿Hay otro enfoque / solución? En este momento estoy pensando en utilizar el modo reaccionar-asincrónico / fibras, pero no estoy completamente seguro de ello, como se explica en el segundo punto.
Discusión relacionada sobre GitHub . Aparentemente, no hay un enfoque oficial o incluso una solución. Tal vez la verdadera pregunta es cómo los componentes React están destinados a ser utilizados. Al igual que la capa de vista simple (más o menos mi sugerencia número uno) o como componentes independientes independientes e independientes?
Quiero compartir con ustedes mi enfoque de la representación del lado del servidor utilizando Flux
, poco se simplificará, por ejemplo:
Digamos que tenemos
component
con datos iniciales de la tienda:class MyComponent extends Component { constructor(props) { super(props); this.state = { data: myStore.getData() }; } }
Si la clase requiere algunos datos precargados para el estado inicial,
MyComponent
Loader paraMyComponent
:class MyComponentLoader { constructor() { myStore.addChangeListener(this.onFetch); } load() { return new Promise((resolve, reject) => { this.resolve = resolve; myActions.getInitialData(); }); } onFetch = () => this.resolve(data); }
Almacenar:
class MyStore extends StoreBase { constructor() { switch(action => { case ''GET_INITIAL_DATA'': this.yourFetchFunction() .then(response => { this.data = response; this.emitChange(); }); break; } getData = () => this.data; }
Ahora solo cargue datos en el enrutador:
on(''/my-route'', async () => { await new MyComponentLoader().load(); return <MyComponent/>; });
Sé que esta pregunta fue hecha hace un año, pero tuvimos el mismo problema y lo resolvemos con promesas anidadas que se derivaron de los componentes que se van a procesar. Al final tuvimos todos los datos de la aplicación y simplemente la enviamos por el camino.
Por ejemplo:
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>
);
}
});
y en el enrutador
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);
}
);
Sé que probablemente esto no sea exactamente lo que quieres, y puede que no tenga sentido, pero recuerdo haber logrado modificar ligeramente el componente para manejar ambos:
- representación en el lado del servidor, con todo el estado inicial ya recuperado, de forma asíncrona si es necesario)
- representación en el lado del cliente, con ajax si es necesario
Entonces algo así como:
/** @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 ...)
});
Lamento no tener el código exacto a mano, por lo que podría no funcionar de la caja, pero lo estoy publicando con el interés de la discusión.
Una vez más, la idea es tratar la mayor parte del componente como una vista tonta, y tratar con la obtención de datos tanto como sea posible del componente.
Si usa react-router , puede simplemente definir un método willTransitionTo
en los componentes, que pasa un objeto de Transition
que puede llamar .wait
on.
No importa si renderToString es síncrono porque la devolución de llamada a Router.run
no se .wait
hasta que se .wait
resuelto todas las promesas .wait
ed, de modo que cuando se renderToString
en el middleware podría haber poblado las tiendas. Incluso si las tiendas son singletons, puede simplemente establecer sus datos temporalmente justo antes de la llamada de renderizado sincrónico y el componente lo verá.
Ejemplo de middleware:
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);
});
};
};
El objeto de routes
(que describe la jerarquía de rutas) se comparte textualmente con el cliente y el servidor