[node.js] 비동기 적으로 초기화 된 React.js 구성 요소의 서버 측 렌더링 전략

의 가장 큰 장점 중 하나 React.js이 있어야하는데 서버 측 렌더링 . 문제는 키 기능 React.renderComponentToString()이 동기식이므로 구성 요소 계층이 서버에서 렌더링 될 때 비동기 데이터를로드 할 수 없다는 것입니다.

페이지의 어느 곳에 나 놓을 수있는 주석 작성을위한 범용 구성 요소가 있다고 가정 해 보겠습니다. 여기에는 하나의 속성, 일종의 식별자 (예 : 주석이 배치되는 기사의 ID) 만 있고 나머지 모든 것은 구성 요소 자체에서 처리합니다 (주석로드, 추가, 관리).

저는 Flux 아키텍처가 정말 마음에 듭니다. 왜냐하면 그것은 많은 일을 훨씬 쉽게 만들어주고 그 저장소 는 서버와 클라이언트 사이의 상태 공유에 완벽하기 때문입니다. 댓글이 포함 된 스토어가 초기화되면 직렬화하여 쉽게 복원 할 수있는 서버에서 클라이언트로 보낼 수 있습니다.

질문은 내 상점을 채우는 가장 좋은 방법입니다. 지난 며칠 동안 저는 인터넷 검색을 많이했고 몇 가지 전략을 접했습니다. React의이 기능이 얼마나 “홍보”되고 있는지 고려할 때 정말 좋은 전략은 없었습니다.

  1. 제 생각에 가장 간단한 방법은 실제 렌더링이 시작되기 전에 모든 매장을 채우는 것입니다. 즉, 구성 요소 계층 외부 어딘가 (예 : 라우터에 연결됨)를 의미합니다. 이 접근법의 문제점은 페이지 구조를 거의 두 번 정의해야한다는 것입니다. 더 복잡한 페이지, 예를 들어 다양한 구성 요소 (실제 블로그 게시물, 댓글, 관련 게시물, 최신 게시물, 트위터 스트림 …)가있는 블로그 페이지를 고려하십시오. React 구성 요소를 사용하여 페이지 구조를 디자인하고 다른 곳에서이 현재 페이지에 필요한 각 저장소를 채우는 프로세스를 정의해야합니다. 그것은 나에게 좋은 해결책이 아닌 것 같습니다. 안타깝게도 대부분의 동형 튜토리얼은 이런 방식으로 설계되었습니다 (예 :이 훌륭한 플럭스 튜토리얼 ).

  2. React-async . 이 접근 방식은 완벽합니다. 이를 통해 각 구성 요소의 특수 함수에서 상태를 초기화하는 방법을 간단하게 정의 할 수 있으며 (동기식이든 비동기식이든 상관 없음) 이러한 함수는 계층 구조가 HTML로 렌더링 될 때 호출됩니다. 상태가 완전히 초기화 될 때까지 구성 요소가 렌더링되지 않는 방식으로 작동합니다. 문제는 섬유 가 필요하다는 것입니다.내가 이해하는 한 표준 JavaScript 동작을 변경하는 Node.js 확장입니다. 나는 결과가 정말 마음에 들지만 해결책을 찾는 대신 게임의 규칙을 변경 한 것 같습니다. 그리고 React.js의 핵심 기능을 사용하기 위해 그렇게해서는 안된다고 생각합니다. 이 솔루션의 일반적인 지원에 대해서도 잘 모르겠습니다. 표준 Node.js 웹 호스팅에서 Fiber를 사용할 수 있습니까?

  3. 나는 혼자서 조금 생각하고 있었다. 구현 세부 사항을 철저히 생각하지는 않았지만 일반적인 생각은 React-async와 유사한 방식으로 구성 요소를 확장 한 다음 루트 구성 요소에서 React.renderComponentToString ()을 반복적으로 호출한다는 것입니다. 각 패스 중에 확장 콜백을 수집 한 다음 패스 및 패스에서 호출하여 상점을 채 웁니다. 현재 구성 요소 계층 구조에 필요한 모든 저장소가 채워질 때까지이 단계를 반복합니다. 해결해야 할 사항이 많고 특히 성능에 대해 확신이 없습니다.

내가 뭐 놓친 거 없니? 다른 접근법 / 솔루션이 있습니까? 지금은 react-async / fibers 방식을 고려하고 있지만 두 번째 요점에서 설명한대로 완전히 확신 할 수 없습니다.

GitHub에 대한 관련 토론 . 분명히 공식적인 접근 방식이나 해결책이 없습니다. 진짜 질문은 React 컴포넌트가 어떻게 사용되는지 일 것입니다. 단순한 뷰 레이어 (제 제안 번호 1)처럼 또는 실제 독립형 및 독립형 구성 요소처럼?



답변

react-router 를 사용하는 경우 호출 할 수 willTransitionTo있는 Transition객체 가 전달되는 구성 요소 의 메서드를 정의 할 수 있습니다 .wait.

Router.run모든 .waited promise가 해결 될 때까지 콜백 이 호출되지 않기 때문에 renderToString이 동기식인지 여부는 중요하지 않습니다 . 따라서 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(경로 계층 구조를 설명합니다) 개체는 클라이언트와 서버에 그대로 공유


답변

나는 이것이 정확히 당신이 원하는 것이 아니라는 것을 알고 있으며 말이 안 될 수도 있지만 두 가지를 모두 처리하기 위해 구성 요소를 약간 수정하여 얻은 것을 기억합니다.

  • 모든 초기 상태가 이미 검색된 상태에서 필요한 경우 비동기 적으로 서버 측에서 렌더링)
  • 필요한 경우 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 ...)

});

정확한 코드를 알지 못해 죄송합니다.이 기능이 즉시 작동하지 않을 수 있지만 토론을 위해 게시하고 있습니다.

다시 말하지만, 아이디어는 대부분의 구성 요소를 멍청한보기로 취급하고 구성 요소에서 가능한 많이 데이터를 가져 오는 입니다.


답변

나는 오늘 이것을 정말로 엉망으로 만들었고 이것이 당신의 문제에 대한 답은 아니지만이 접근 방식을 사용했습니다. 라우팅에 React Router보다는 Express를 사용하고 싶었고 노드에서 스레딩 지원이 필요하지 않았기 때문에 Fibers를 사용하고 싶지 않았습니다.

그래서 방금로드시 플럭스 스토어에 렌더링해야하는 초기 데이터에 대해 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 });


답변

나는이 질문이 1 년 전에 요청되었다는 것을 알고 있지만 우리는 동일한 문제를 가지고 있었고 렌더링 할 구성 요소에서 파생 된 중첩 된 약속으로 해결했습니다. 결국 우리는 앱에 대한 모든 데이터를 가지고 있었고 그냥 전송했습니다.

예를 들면 :

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. 클래스가 초기 상태에 대해 미리로드 된 데이터를 필요로하는 경우 Loader를 생성 해 보겠습니다 MyComponent.

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

    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. 이제 라우터에 데이터를로드합니다.

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

답변

짧은 롤업-> GraphQL이 스택에 대해이 문제를 해결합니다.

  • GraphQL 추가
  • 아폴로와 리 액트 아폴로 사용
  • 렌더링을 시작하기 전에 “getDataFromTree”를 사용하십시오.

-> getDataFromTree는 앱에서 관련된 모든 쿼리를 자동으로 찾아 실행하여 서버에 아폴로 캐시를 추가하여 완전히 작동하는 SSR을 활성화합니다. BÄM


답변