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


114

의 가장 큰 장점 중 하나 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)처럼 또는 실제 독립형 및 독립형 구성 요소처럼?


정보를 얻으려면 비동기 호출이 서버 측에서도 발생합니까? 이 경우 일부 부분을 비워두고 뷰를 렌더링하고 비동기 응답의 결과가 도착할 때 채우는 것과는 대조적으로이 경우의 이점을 이해하지 못합니다. 뭔가 빠진 것 같네요, 죄송합니다!
phtrivier 2014 년

JavaScript에서 최신 게시물을 가져 오기위한 가장 간단한 쿼리조차도 비동기 적이라는 것을 잊지 말아야합니다. 따라서 뷰를 렌더링하는 경우 데이터베이스에서 데이터를 가져올 때까지 기다려야합니다. 그리고 서버 측 렌더링에는 명백한 이점이 있습니다. 예를 들어 SEO입니다. 또한 페이지가 깜박이는 것을 방지합니다. 실제로 서버 측 렌더링은 대부분의 웹 사이트에서 여전히 사용하는 표준 접근 방식입니다.
tobik 2014 년

물론입니다.하지만 전체 페이지를 렌더링하려고합니까 (모든 비동기 DB 쿼리가 응답하면)? 어떤 경우에는 순진하게 1 / 모든 데이터를 비동기 적으로 가져 오기 2 / 완료되면 "멍청한"React View에 전달하고 요청에 응답하는 것으로 순진하게 분리했을 것입니다. 아니면 서버 측 렌더링을 한 다음 동일한 코드로 클라이언트 측을 시도하고 있습니까 (그리고 반응 뷰에 가까워 지려면 비동기 코드가 필요합니까?) 어리석은 소리가 들리면 미안합니다. 당신이하는 일.
phtrivier 2014 년

문제 없습니다. 다른 사람들도 이해하는 데 문제가있을 수 있습니다. 방금 설명한 것은 두 번째 솔루션입니다. 그러나 예를 들어 질문에서 주석을 달기위한 구성 요소를 사용하십시오. 일반적인 클라이언트 측 응용 프로그램에서는 해당 구성 요소에서 모든 작업을 수행 할 수 있습니다 (주석로드 / 추가). 구성 요소는 외부 세계와 분리되고 외부 세계는이 구성 요소에 대해 신경 쓸 필요가 없습니다. 그것은 완전히 독립적이고 독립적 일 것입니다. 하지만 일단 서버 측 렌더링을 도입하려면 외부에서 비동기 작업을 처리해야합니다. 그리고 그것은 전체 원칙을 깨뜨립니다.
tobik 2014 년

명확하게 말하면, 나는 광섬유 사용을 옹호하는 것이 아니라 모든 비동기 호출을 수행하고, 모두 완료 되면 (Promise 등을 사용하여) 서버 측에서 구성 요소를 렌더링합니다. 합니다 (구성 요소는 모르겠다 반응 그래서 전혀 단지 의견의 것을, 지금. 비동기 물건에 대한)하지만 실제로 같은 완전히 구성 요소 반응에서 서버 통신에 관련된 아무것도 제거하는 아이디어 (이 정말 이곳에서만보기를 렌더링 할 수 있습니다 .) 그리고 저는 그것이 반응 뒤에있는 철학이라고 생각합니다. 당신이하는 일이 왜 약간 복잡한 지 설명 할 수 있습니다. 어쨌든 행운을 빕니다 :)
phtrivier

답변:


15

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(경로 계층 구조를 설명합니다) 개체는 클라이언트와 서버에 그대로 공유


감사. 문제는 내가 아는 한 경로 구성 요소만이 willTransitionTo방법을 지원한다는 것입니다. 즉, 질문에서 설명한 것과 같이 완전히 독립형 재사용 가능한 구성 요소를 작성하는 것이 여전히 불가능합니다. 그러나 우리가 Fibers를 기꺼이 사용하지 않는다면 이것은 아마도 서버 측 렌더링을 구현 하는 가장 좋고 가장 반응이 좋은 방법 일 것입니다 .
tobik

이건 재미 있네. willTransitionTo 메서드의 구현은 비동기 데이터를로드하는 것처럼 어떻게 보일까요?
Hyra 2014-12-16

transition개체를 매개 변수로 가져 오므로 간단히 transition.wait(yourPromise). 물론 약속을 지원하려면 API를 구현해야합니다. 이 접근 방식의 또 다른 단점은 클라이언트 측에서 "로딩 표시기"를 구현하는 간단한 방법이 없다는 것입니다. 모든 promise가 해결 될 때까지 전환은 경로 처리기 구성 요소로 전환되지 않습니다.
tobik

하지만 실제로 "적시"접근 방식에 대해서는 잘 모르겠습니다. 여러 개의 중첩 된 라우트 핸들러가 하나의 URL과 일치 할 수 있습니다. 즉, 여러 promise를 해결해야합니다. 모두 동시에 종료된다는 보장은 없습니다. 상점이 싱글 톤이면 충돌이 발생할 수 있습니다. @Esailija 당신의 대답을 조금 설명해 주시겠습니까?
tobik 2014

.waited전환에 대한 모든 약속을 수집하는 자동 배관이 있습니다. 모두 충족되면 .run콜백이 호출됩니다. 바로 전에 .render()내가 약속에서 함께 모든 데이터를 수집하고 렌더링 호출 후 다음 줄에 다음의 singelton 저장 상태를 설정 내가 다시 싱글 저장소를 초기화합니다. 꽤 해키하지만 모든 것이 자동으로 발생하며 구성 요소 및 저장소 응용 프로그램 코드는 거의 동일하게 유지됩니다.
Esailija 2014

0

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

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

});

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

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


1
감사합니다. 나는 아이디어를 얻었지만 실제로 내가 원하는 것이 아닙니다. bbc.com 과 같이 React를 사용하여 좀 더 복잡한 웹 사이트를 만들고 싶다고 가정 해 보겠습니다 . 페이지를 보면 모든 곳에서 "구성 요소"를 볼 수 있습니다. 섹션 (스포츠, 비즈니스 ...)은 일반적인 구성 요소입니다. 어떻게 구현 하시겠습니까? 모든 데이터를 어디에서 프리 페치 하시겠습니까? 이러한 복잡한 사이트를 설계하려면 구성 요소 (원칙적으로 작은 MVC 컨테이너와 같은)가 매우 좋은 방법입니다 (유일한 경우). 구성 요소 접근 방식은 많은 일반적인 서버 측 프레임 워크에서 일반적입니다. 문제는 React를 사용할 수 있습니까?
tobik 2014 년

서버 측에서 데이터를 프리 페치합니다 (이 경우에는 아마도 "전통적인"서버 측 템플릿 시스템으로 전달하기 전에 수행 할 것입니다). 저스트 때문에 디스플레이 모듈되는 데이터의 이점은 그것이 의미 하는가 연산 데이터를 necessarilly 동일한 구조를 수행하는? 나는 여기서 악마의 옹호자 역할을하고 있는데, 옴을 확인할 때와 같은 문제가 있었다. 그리고 나는 누군가가 이것에 대해 더 많은 통찰력을 가지고 있기를 바랍니다. 와이어의 어느 쪽이든 원활하게 구성하면 많은 도움이 될 것입니다.
phtrivier 2014 년

1
코드에서 어디를 의미합니다. 컨트롤러에서? 그렇다면 bbc의 홈 페이지를 처리하는 컨트롤러 메서드는 각 섹션 1에 대해 수십 개의 유사한 쿼리를 포함할까요? 그것은 imho 지옥의 길입니다. 그래서 그래, 내가 그 계산뿐만 아니라 모듈 형 있어야한다고 생각합니다. 모든 것이 하나의 MVC 컨테이너에 하나의 구성 요소로 포장됩니다. 이것이 제가 표준 서버 측 앱을 개발하는 방법이며이 접근 방식이 좋다고 확신합니다. 그리고 제가 React.js에 대해 흥분하는 이유는 클라이언트와 서버 모두에서이 접근 방식을 사용하여 멋진 동형 앱을 만들 수있는 큰 잠재력이 있기 때문입니다.
tobik 2014 년

1
어떤 사이트 (대형 / 소형)에서든 init 상태로 현재 페이지를 서버 측 렌더링 (SSR)하기 만하면됩니다. 모든 페이지에 대해 초기화 상태가 필요하지 않습니다. 서버는 초기화 상태를 가져 와서 렌더링하고 클라이언트에 전달합니다 <script type=application/json>{initState}</script>. 그런 식으로 데이터가 HTML에 있습니다. 클라이언트에서 render를 호출하여 UI 이벤트를 페이지에 재수 화 / 바인딩합니다. 후속 페이지는 클라이언트의 js 코드 (필요에 따라 데이터 가져 오기)에 의해 생성되고 클라이언트가 렌더링합니다. 이렇게하면 새로 고침이 새로운 SSR 페이지를로드하고 페이지를 클릭하면 CSR이됩니다. = 동형 및 SEO 친화적
Federico

0

나는 오늘 이것을 정말로 엉망으로 만들었고 이것이 당신의 문제에 대한 답은 아니지만이 접근 방식을 사용했습니다. 라우팅에 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 });

0

나는이 질문이 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);
    }
);

0

를 사용하여 서버 측 렌더링에 대한 제 접근 방식을 공유하고 싶습니다 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/>;
    });

0

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

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.