Các chiến lược để hiển thị phía máy chủ của các thành phần React.js được khởi tạo không đồng bộ


114

Một trong những lợi thế lớn nhất của React.js được cho là khả năng hiển thị phía máy chủ . Vấn đề là chức năng chính React.renderComponentToString()là đồng bộ nên không thể tải bất kỳ dữ liệu không đồng bộ nào khi cấu trúc phân cấp thành phần được hiển thị trên máy chủ.

Giả sử tôi có một thành phần phổ biến để nhận xét mà tôi có thể thả khá nhiều ở bất kỳ đâu trên trang. Nó chỉ có một thuộc tính, một số loại định danh (ví dụ: id của một bài báo bên dưới đặt các bình luận) và mọi thứ khác được xử lý bởi chính thành phần (tải, thêm, quản lý bình luận).

Tôi thực sự thích kiến trúc Flux vì nó làm cho nhiều thứ dễ dàng hơn nhiều và các cửa hàng của nó rất hoàn hảo để chia sẻ trạng thái giữa máy chủ và máy khách. Khi cửa hàng chứa các bình luận của tôi được khởi tạo, tôi có thể tuần tự hóa nó và gửi nó từ máy chủ đến máy khách để dễ dàng khôi phục.

Câu hỏi đặt ra là cách tốt nhất để tập trung vào cửa hàng của tôi là gì. Trong những ngày qua, tôi đã tìm kiếm trên Google rất nhiều và tôi đã tìm thấy một vài chiến lược, không có chiến lược nào có vẻ thực sự tốt khi tính đến mức độ mà tính năng này của React đang được "quảng bá".

  1. Theo tôi, cách đơn giản nhất là điền tất cả các cửa hàng của tôi trước khi kết xuất thực sự bắt đầu. Điều đó có nghĩa là một nơi nào đó bên ngoài cấu trúc phân cấp thành phần (được nối với bộ định tuyến của tôi chẳng hạn). Vấn đề với cách tiếp cận này là tôi sẽ phải xác định cấu trúc trang hai lần. Hãy xem xét một trang phức tạp hơn, ví dụ một trang blog có nhiều thành phần khác nhau (bài đăng blog thực tế, nhận xét, bài đăng liên quan, bài đăng mới nhất, luồng twitter ...). Tôi sẽ phải thiết kế cấu trúc trang bằng cách sử dụng các thành phần React và sau đó ở một nơi khác, tôi sẽ phải xác định quy trình điền từng cửa hàng cần thiết cho trang hiện tại này. Đó dường như không phải là một giải pháp tốt đối với tôi. Thật không may, hầu hết các hướng dẫn isomorphic được thiết kế theo cách này (ví dụ: hướng dẫn về thông lượng tuyệt vời này ).

  2. React-async . Cách tiếp cận này là hoàn hảo. Nó cho phép tôi xác định đơn giản trong một hàm đặc biệt trong mỗi thành phần cách khởi tạo trạng thái (không quan trọng là đồng bộ hay không đồng bộ) và các hàm này được gọi là hệ thống phân cấp đang được hiển thị cho HTML. Nó hoạt động theo cách mà một thành phần không được hiển thị cho đến khi trạng thái được khởi tạo hoàn toàn. Vấn đề là nó yêu cầu FibersĐó là, theo như tôi hiểu, một phần mở rộng Node.js có thể thay đổi hành vi JavaScript chuẩn. Mặc dù tôi rất thích kết quả đó, nhưng đối với tôi dường như thay vì tìm ra giải pháp, chúng tôi đã thay đổi luật chơi. Và tôi nghĩ rằng chúng ta không nên bắt buộc phải làm điều đó để sử dụng tính năng cốt lõi này của React.js. Tôi cũng không chắc về sự hỗ trợ chung của giải pháp này. Có thể sử dụng Fiber trên lưu trữ web Node.js tiêu chuẩn không?

  3. Tôi đã suy nghĩ một chút của riêng tôi. Tôi chưa thực sự nghĩ đến chi tiết triển khai nhưng ý tưởng chung là tôi sẽ mở rộng các thành phần theo cách tương tự như React-async và sau đó tôi sẽ liên tục gọi React.renderComponentToString () trên thành phần gốc. Trong mỗi lần vượt qua, tôi sẽ thu thập các cuộc gọi lại mở rộng và sau đó gọi chúng tại và của thẻ để điền vào các cửa hàng. Tôi sẽ lặp lại bước này cho đến khi tất cả các cửa hàng theo yêu cầu của hệ thống phân cấp thành phần hiện tại sẽ được điền. Có nhiều thứ phải giải quyết và tôi đặc biệt không chắc chắn về hiệu suất.

Tôi đã bỏ lỡ điều gì đó? Có cách tiếp cận / giải pháp nào khác không? Hiện tại tôi đang nghĩ đến việc sử dụng phản ứng không đồng bộ / sợi nhưng tôi không hoàn toàn chắc chắn về nó như đã giải thích ở điểm thứ hai.

Thảo luận liên quan trên GitHub . Rõ ràng, không có cách tiếp cận chính thức hoặc thậm chí là giải pháp. Có thể câu hỏi thực sự là các thành phần React được dự định sử dụng như thế nào. Giống như lớp xem đơn giản (khá nhiều gợi ý số một của tôi) hoặc thích các thành phần độc lập và độc lập thực sự?


Chỉ để có được những thứ: các cuộc gọi không đồng bộ cũng sẽ xảy ra ở phía máy chủ? Tôi không hiểu những lợi ích trong trường hợp này trái ngược với việc hiển thị chế độ xem với một số phần bị bỏ trống và lấp đầy nó khi kết quả từ phản hồi không đồng bộ đến. Có lẽ là thiếu một cái gì đó, xin lỗi!
phtrivier

Bạn không được quên rằng trong JavaScript, ngay cả truy vấn đơn giản nhất đến cơ sở dữ liệu để tìm nạp các bài đăng mới nhất cũng không đồng bộ. Vì vậy, nếu bạn đang hiển thị một dạng xem, bạn phải đợi cho đến khi dữ liệu được tìm nạp từ cơ sở dữ liệu. Và có những lợi ích rõ ràng khi hiển thị ở phía máy chủ: ví dụ như SEO. Và nó cũng ngăn trang không bị nhấp nháy. Trên thực tế, kết xuất phía máy chủ là cách tiếp cận tiêu chuẩn mà hầu hết các trang web vẫn sử dụng.
tobik

Chắc chắn rồi, nhưng bạn đang cố kết xuất toàn bộ trang (sau khi tất cả các truy vấn db không đồng bộ đã phản hồi)? Trong trường hợp đó, tôi đã ngây thơ phân tách nó thành 1 / tìm nạp tất cả dữ liệu một cách không đồng bộ 2 / khi hoàn tất, hãy chuyển nó đến một React View "câm" và phản hồi yêu cầu. Hay bạn đang cố gắng thực hiện cả kết xuất phía máy chủ, sau đó là phía máy khách với cùng một mã (và bạn cần mã không đồng bộ để gần với chế độ xem phản ứng?) Xin lỗi nếu điều đó nghe có vẻ ngớ ngẩn, tôi không chắc mình hiểu Bạn đang làm gì vậy.
phtrivier

Không sao, có lẽ những người khác cũng có vấn đề để hiểu :) Những gì bạn vừa mô tả là giải pháp số hai. Nhưng hãy lấy ví dụ thành phần để bình luận từ câu hỏi. Trong ứng dụng phía máy khách thông thường, tôi có thể làm mọi thứ trong thành phần đó (tải / thêm nhận xét). Thành phần sẽ được tách biệt với thế giới bên ngoài và thế giới bên ngoài sẽ không phải quan tâm đến thành phần này. Nó sẽ hoàn toàn độc lập và độc lập. Nhưng một khi tôi muốn giới thiệu kết xuất phía máy chủ, tôi phải xử lý nội dung không đồng bộ bên ngoài. Và điều đó phá vỡ toàn bộ nguyên tắc.
tobik

Nói rõ hơn, tôi không ủng hộ việc sử dụng các sợi, mà chỉ thực hiện tất cả các lệnh gọi không đồng bộ và sau khi chúng hoàn thành (sử dụng hứa hẹn hoặc bất cứ điều gì), hãy hiển thị thành phần ở phía máy chủ. (Vì vậy, phản ứng linh kiện sẽ không biết gì cả về những thứ không đồng bộ.) Bây giờ, đó là chỉ đưa ra ý kiến, nhưng tôi thực sự như ý tưởng hoàn toàn loại bỏ bất cứ điều gì liên quan đến truyền thông máy chủ từ Phản ứng linh kiện (mà thực sự duy nhất ở đây để làm cho xem .) Và tôi nghĩ đó là triết lý đằng sau phản ứng, có thể giải thích tại sao những gì bạn đang làm hơi phức tạp. Dù sao thì, chúc may mắn :)
phtrivier 29/09

Câu trả lời:


15

Nếu bạn sử dụng bộ định tuyến phản ứng , bạn chỉ có thể xác định một willTransitionTophương thức trong các thành phần, phương thức này sẽ được chuyển qua một Transitionđối tượng mà bạn có thể gọi .wait.

Không quan trọng nếu renderToString là đồng bộ vì lệnh gọi lại tới Router.runsẽ không được gọi cho đến khi tất cả các .waitlời hứa ed được giải quyết, vì vậy vào thời điểm renderToStringđược gọi trong phần mềm trung gian, bạn có thể đã điền các cửa hàng. Ngay cả khi các cửa hàng là các ổ đĩa đơn, bạn chỉ có thể đặt dữ liệu của chúng tạm thời ngay trong thời gian trước khi gọi hiển thị đồng bộ và thành phần sẽ nhìn thấy nó.

Ví dụ về phần mềm trung gian:

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);
        });
    };
};

Đối routestượng (mô tả hệ thống phân cấp các tuyến đường) được chia sẻ nguyên văn với máy khách và máy chủ


Cảm ơn. Vấn đề là theo như tôi biết, chỉ có các thành phần tuyến đường hỗ trợ willTransitionTophương pháp này . Điều đó có nghĩa là vẫn không thể viết các thành phần hoàn toàn độc lập có thể tái sử dụng như cái tôi đã mô tả trong câu hỏi. Nhưng trừ khi chúng ta sẵn sàng để đi với Sợi, đây có lẽ là tốt nhất và hầu hết các phản ứng cách để thực hiện server-side rendering.
tobik

Hay đấy. Việc triển khai phương thức willTransitionTo trông như thế nào khi tải dữ liệu không đồng bộ?
Hyra

Bạn sẽ lấy transitionđối tượng dưới dạng tham số, vì vậy bạn sẽ chỉ cần gọi transition.wait(yourPromise). Điều đó tất nhiên có nghĩa là bạn phải triển khai API của mình để hỗ trợ các lời hứa. Một nhược điểm khác của phương pháp này là không có cách đơn giản nào để triển khai "chỉ báo tải" ở phía máy khách. Quá trình chuyển đổi sẽ không chuyển sang thành phần trình xử lý tuyến đường cho đến khi tất cả các lời hứa được giải quyết.
tobik

Nhưng tôi thực sự không chắc về cách tiếp cận "đúng lúc". Nhiều trình xử lý tuyến đường lồng nhau có thể khớp với một url có nghĩa là nhiều lời hứa sẽ phải được giải quyết. Không có gì đảm bảo rằng tất cả chúng sẽ kết thúc cùng một lúc. Nếu các cửa hàng là đơn lẻ, nó có thể gây ra xung đột. @Esailija, bạn có thể giải thích câu trả lời của bạn một chút được không?
tobik

Tôi có hệ thống ống nước tự động tại chỗ thu thập tất cả những lời hứa .waitedcho quá trình chuyển đổi. Khi tất cả chúng được hoàn thành, lệnh .rungọi lại được gọi. Ngay trước khi .render()tôi thu thập tất cả dữ liệu cùng nhau từ các lời hứa và thiết lập trạng thái lưu trữ singelton, sau đó trên dòng tiếp theo sau lệnh gọi kết xuất, tôi khởi tạo lại các lưu trữ singleton. Nó khá hacky nhưng tất cả đều diễn ra tự động và mã ứng dụng thành phần và cửa hàng hầu như vẫn giữ nguyên.
Esailija

0

Tôi biết đây có thể không phải là chính xác những gì bạn muốn và nó có thể không có ý nghĩa, nhưng tôi nhớ đã sửa đổi nhanh thành phần để xử lý cả hai:

  • hiển thị ở phía máy chủ, với tất cả trạng thái ban đầu đã được truy xuất, không đồng bộ nếu cần)
  • hiển thị ở phía máy khách, với ajax nếu cần

Vì vậy, một cái gì đó như:

/** @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 ...)  

});

Tôi xin lỗi vì tôi không có mã chính xác trong tay, vì vậy điều này có thể không hoạt động tốt, nhưng tôi đang đăng vì lợi ích của cuộc thảo luận.

Một lần nữa, ý tưởng là coi hầu hết thành phần là một chế độ xem ngu ngốc và xử lý việc tìm nạp dữ liệu ra khỏi thành phần càng nhiều càng tốt .


1
Cảm ơn bạn. Tôi có ý tưởng, nhưng nó thực sự không phải là điều tôi muốn. Giả sử tôi muốn xây dựng một số trang web phức tạp hơn bằng React, như bbc.com . Nhìn vào trang, tôi có thể thấy "linh kiện" ở khắp mọi nơi. Một phần (thể thao, kinh doanh ...) là một thành phần điển hình. Bạn sẽ thực hiện nó như thế nào? Bạn sẽ tìm nạp trước tất cả dữ liệu ở đâu? Để thiết kế một trang web phức tạp như vậy, các thành phần (về nguyên tắc, như các thùng chứa MVC nhỏ) là cách rất tốt (nếu có thể là duy nhất). Cách tiếp cận thành phần phổ biến cho nhiều khuôn khổ phía máy chủ điển hình. Câu hỏi đặt ra là: tôi có thể sử dụng React cho việc đó không?
tobik

Bạn sẽ tìm nạp trước dữ liệu ở phía máy chủ (vì nó có thể được thực hiện trong trường hợp này, trước khi chuyển nó sang hệ thống mẫu phía máy chủ "truyền thống"); chỉ vì việc hiển thị dữ liệu được hưởng lợi từ mô-đun, điều đó có nghĩa là việc tính toán dữ liệu bắt buộc phải tuân theo cùng một cấu trúc? Tôi đang chơi trò biện hộ của quỷ một chút ở đây, tôi đã gặp rắc rối giống như bạn gặp phải khi kiểm tra om. Và tôi chắc chắn hy vọng ai đó có thêm hiểu biết về điều này sau đó tôi làm - việc soạn nội dung liên tục trên bất kỳ mặt nào của dây sẽ giúp ích rất nhiều.
phtrivier

1
Theo ý tôi là ở đâu trong mã. Trong bộ điều khiển? Vì vậy, phương thức controller xử lý trang chủ của bbc sẽ chứa hàng tá truy vấn tương tự, cho mỗi phần một? Đó là một con đường dẫn đến địa ngục. Vì vậy, có, tôi làm nghĩ tính toán rằng nên môđun hóa là tốt. Mọi thứ được đóng gói trong một thành phần, trong một thùng chứa MVC. Đó là cách tôi phát triển các ứng dụng phía máy chủ tiêu chuẩn và tôi khá tự tin rằng cách tiếp cận này là tốt. Và lý do tại sao tôi rất hào hứng với React.js là có tiềm năng lớn để sử dụng phương pháp này trên cả phía máy khách và máy chủ để tạo ra các ứng dụng đẳng hình tuyệt vời.
tobik

1
Trên bất kỳ trang web nào (lớn / nhỏ), bạn chỉ phải hiển thị phía máy chủ (SSR) trang hiện tại với trạng thái init của nó; bạn không cần trạng thái init cho mọi trang. Máy chủ lấy trạng thái init, hiển thị nó và chuyển nó cho máy khách <script type=application/json>{initState}</script>; theo cách đó, dữ liệu sẽ ở trong HTML. Bù nước / liên kết các sự kiện giao diện người dùng với trang bằng cách gọi hiển thị trên máy khách. Các trang tiếp theo được tạo bởi mã js của khách hàng (tìm nạp dữ liệu khi cần thiết) và được hiển thị bởi ứng dụng khách. Bằng cách đó, bất kỳ lần làm mới nào cũng sẽ tải các trang SSR mới và nhấp vào một trang sẽ là CSR. = isomorphic & thân thiện với SEO
Federico

0

Tôi thực sự bối rối với điều này ngày hôm nay, và mặc dù đây không phải là câu trả lời cho vấn đề của bạn, tôi đã sử dụng cách tiếp cận này. Tôi muốn sử dụng Express để định tuyến hơn là React Router và tôi không muốn sử dụng Fibers vì tôi không cần hỗ trợ phân luồng trong nút.

Vì vậy, tôi vừa đưa ra quyết định rằng đối với dữ liệu ban đầu cần được hiển thị cho kho lưu trữ thông lượng khi tải, tôi sẽ thực hiện một yêu cầu AJAX và chuyển dữ liệu ban đầu vào cửa hàng

Tôi đã sử dụng Fluxxor cho ví dụ này.

Vì vậy, trên tuyến đường cao tốc của tôi, trong trường hợp này là một /productstuyến đường:

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));

Sau đó, phương thức kết xuất khởi tạo của tôi sẽ chuyển dữ liệu đến cửa hàng.

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

Tôi biết câu hỏi này đã được hỏi một năm trước nhưng chúng tôi đã gặp phải vấn đề tương tự và chúng tôi giải quyết nó bằng những lời hứa lồng nhau có nguồn gốc từ các thành phần sẽ được kết xuất. Cuối cùng, chúng tôi đã có tất cả dữ liệu cho ứng dụng và chỉ cần gửi nó xuống.

Ví dụ:

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>
        );

    }

});

và trong bộ định tuyến

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

Tôi muốn chia sẻ với bạn phương pháp kết xuất phía máy chủ của tôi bằng cách sử dụng Flux, ví dụ:

  1. Giả sử chúng ta có componentdữ liệu ban đầu từ cửa hàng:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Nếu lớp yêu cầu một số dữ liệu được tải trước cho trạng thái ban đầu, hãy tạo Trình tải cho MyComponent:

     class MyComponentLoader {
        constructor() {
            myStore.addChangeListener(this.onFetch);
        }
        load() {
            return new Promise((resolve, reject) => {
                this.resolve = resolve;
                myActions.getInitialData(); 
            });
        }
        onFetch = () => this.resolve(data);
    }
  3. Cửa hàng:

    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. Bây giờ chỉ cần tải dữ liệu trong bộ định tuyến:

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

0

giống như một bản tổng hợp ngắn -> GraphQL sẽ giải quyết vấn đề này một cách nhanh chóng cho ngăn xếp của bạn ...

  • thêm GraphQL
  • sử dụng apollo và react-apollo
  • sử dụng "getDataFromTree" trước khi bạn bắt đầu kết xuất

-> getDataFromTree sẽ tự động tìm tất cả các truy vấn liên quan trong ứng dụng của bạn và thực thi chúng, tạo bộ đệm apollo của bạn trên máy chủ và do đó, cho phép SSR hoạt động đầy đủ .. BÄM

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.