Strategi untuk rendering sisi server dari komponen React.js yang diinisialisasi secara asinkron


114

Salah satu keuntungan terbesar dari React.js adalah rendering sisi server . Masalahnya adalah bahwa fungsi kuncinya React.renderComponentToString()sinkron sehingga tidak mungkin memuat data asinkron apa pun saat hierarki komponen dirender di server.

Katakanlah saya memiliki komponen universal untuk berkomentar yang dapat saya jatuhkan di mana saja di halaman. Ini hanya memiliki satu properti, beberapa jenis pengenal (misalnya id artikel di bawah ini tempat komentar ditempatkan), dan yang lainnya ditangani oleh komponen itu sendiri (memuat, menambah, mengelola komentar).

Saya sangat menyukai arsitektur Flux karena ia membuat banyak hal menjadi lebih mudah, dan tokonya sempurna untuk berbagi status antara server dan klien. Setelah toko saya yang berisi komentar diinisialisasi, saya hanya dapat membuat serial dan mengirimkannya dari server ke klien di mana ia dapat dengan mudah dipulihkan.

Pertanyaannya adalah bagaimana cara terbaik untuk mengisi toko saya. Selama beberapa hari terakhir ini saya telah banyak mencari di Google dan menemukan beberapa strategi, tidak ada satupun yang terlihat sangat bagus mengingat seberapa besar fitur React ini "dipromosikan".

  1. Menurut pendapat saya, cara paling sederhana adalah mengisi semua toko saya sebelum rendering sebenarnya dimulai. Itu berarti di suatu tempat di luar hierarki komponen (terhubung ke router saya misalnya). Masalah dengan pendekatan ini adalah saya harus cukup banyak mendefinisikan struktur halaman dua kali. Pertimbangkan halaman yang lebih kompleks, misalnya halaman blog dengan banyak komponen berbeda (posting blog aktual, komentar, posting terkait, posting terbaru, aliran twitter ...). Saya harus mendesain struktur halaman menggunakan komponen React dan kemudian di tempat lain saya harus menentukan proses pengisian setiap toko yang diperlukan untuk halaman ini. Bagi saya, itu bukan solusi yang bagus. Sayangnya sebagian besar tutorial isomorfik dirancang dengan cara ini (misalnya tutorial fluks yang hebat ini ).

  2. React-async . Pendekatan ini sempurna. Ini memungkinkan saya untuk menentukan dalam fungsi khusus di setiap komponen bagaimana menginisialisasi status (tidak peduli apakah secara sinkron atau asinkron) dan fungsi ini disebut sebagai hierarki yang dirender ke HTML. Ia bekerja sedemikian rupa sehingga komponen tidak dirender hingga status benar-benar diinisialisasi. Masalahnya adalah itu membutuhkan Seratyang, sejauh yang saya pahami, ekstensi Node.js yang mengubah perilaku JavaScript standar. Meskipun saya sangat menyukai hasilnya, saya tetap merasa bahwa alih-alih menemukan solusi, kami mengubah aturan permainan. Dan saya pikir kita seharusnya tidak dipaksa melakukan itu untuk menggunakan fitur inti React.js ini. Saya juga tidak yakin tentang dukungan umum dari solusi ini. Apakah mungkin menggunakan Fiber pada hosting web Node.js standar?

  3. Saya berpikir sedikit sendiri. Saya belum benar-benar memikirkan detail implementasi tetapi gagasan umumnya adalah bahwa saya akan memperluas komponen dengan cara yang mirip dengan React-async dan kemudian saya akan berulang kali memanggil React.renderComponentToString () pada komponen root. Selama setiap pass, saya akan mengumpulkan callback yang diperpanjang dan kemudian memanggil mereka di dan dari pass untuk mengisi toko. Saya akan mengulangi langkah ini sampai semua penyimpanan yang diperlukan oleh hierarki komponen saat ini akan terisi. Ada banyak hal yang harus diselesaikan dan saya sangat tidak yakin dengan penampilannya.

Apakah saya melewatkan sesuatu? Apakah ada pendekatan / solusi lain? Saat ini saya sedang berpikir untuk menggunakan cara react-async / fibres tetapi saya tidak sepenuhnya yakin tentang hal itu seperti yang dijelaskan pada poin kedua.

Diskusi terkait di GitHub . Ternyata, tidak ada pendekatan resmi atau bahkan solusi. Mungkin pertanyaan sebenarnya adalah bagaimana komponen React dimaksudkan untuk digunakan. Seperti lapisan tampilan sederhana (cukup banyak saran saya nomor satu) atau seperti komponen mandiri dan mandiri nyata?


Hanya untuk mendapatkan sesuatu: panggilan asinkron akan terjadi di sisi server juga? Saya tidak memahami manfaat dalam kasus ini dibandingkan dengan menampilkan tampilan dengan beberapa bagian dibiarkan kosong, dan mengisinya saat hasil dari respons asinkron tiba. Mungkin melewatkan sesuatu, maaf!
phtrivier

Anda tidak boleh lupa bahwa dalam JavaScript bahkan kueri paling sederhana ke database untuk mengambil posting terbaru adalah asinkron. Jadi jika Anda merender tampilan, Anda harus menunggu hingga data diambil dari database. Dan ada manfaat nyata untuk merender di sisi server: SEO misalnya. Dan juga mencegah halaman berkedip-kedip. Sebenarnya rendering sisi server adalah pendekatan standar yang masih digunakan sebagian besar situs web.
tobik

Tentu, tetapi apakah Anda mencoba merender seluruh halaman (setelah semua kueri db asinkron merespons)? Dalam hal ini, saya akan memisahkannya secara naif sebagai 1 / mengambil semua data secara asinkron 2 / setelah selesai, meneruskannya ke React View yang "bodoh", dan menanggapi permintaan tersebut. Atau apakah Anda mencoba melakukan perenderan sisi server, lalu sisi klien dengan kode yang sama (dan Anda memerlukan kode asinkron agar dekat dengan tampilan reaksi?) Maaf jika kedengarannya konyol, saya tidak yakin saya mengerti apa yang kamu lakukan.
phtrivier

Tidak masalah, mungkin orang lain juga kesulitan untuk memahaminya :) Yang baru saja Anda jelaskan adalah solusi nomor dua. Tetapi ambil contoh komponen untuk berkomentar dari pertanyaan. Dalam aplikasi sisi klien yang umum, saya dapat melakukan semuanya dalam komponen itu (memuat / menambahkan komentar). Komponen akan dipisahkan dari dunia luar dan dunia luar tidak perlu peduli dengan komponen ini. Itu akan sepenuhnya independen dan mandiri. Tapi begitu saya ingin memperkenalkan rendering sisi server, saya harus menangani hal-hal asinkron di luar. Dan itu merusak seluruh prinsip.
tobik

Untuk memperjelas, saya tidak menganjurkan penggunaan fiber, tetapi hanya melakukan semua panggilan asinkron, dan setelah semuanya selesai (menggunakan promise atau apa pun), render komponen di sisi server. (Jadi komponen react tidak akan tahu sama sekali tentang hal-hal asynchronous.) Sekarang, itu hanya sebuah opini, tapi saya sebenarnya menyukai ide untuk sepenuhnya menghapus apapun yang berhubungan dengan komunikasi server dari komponen React (yang sebenarnya hanya di sini untuk membuat tampilan .) Dan saya pikir itulah filosofi di balik bereaksi, yang mungkin menjelaskan mengapa apa yang Anda lakukan agak rumit. Pokoknya, semoga berhasil :)
phtrivier

Jawaban:


15

Jika Anda menggunakan react-router , Anda cukup menentukan willTransitionTometode dalam komponen, yang akan meneruskan Transitionobjek yang dapat Anda panggil .wait.

Tidak masalah jika renderToString sinkron karena callback ke Router.runtidak akan dipanggil hingga semua .waitjanji ed diselesaikan, jadi pada saat renderToStringdipanggil di middleware, Anda dapat mengisi penyimpanan. Bahkan jika penyimpanannya lajang, Anda dapat menyetel datanya untuk sementara waktu tepat sebelum panggilan rendering sinkron dan komponen akan melihatnya.

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

The routesobjek (yang menggambarkan hirarki rute) dibagi verbatim dengan klien dan server


Terima kasih. Masalahnya adalah sejauh yang saya tahu, hanya komponen rute yang mendukung willTransitionTometode ini . Artinya, masih tidak mungkin untuk menulis komponen yang sepenuhnya dapat digunakan kembali mandiri seperti yang saya jelaskan dalam pertanyaan. Tetapi kecuali kami bersedia menggunakan Serat, ini mungkin cara terbaik dan paling bereaksi untuk menerapkan rendering sisi server.
tobik

Ini menarik. Bagaimana implementasi metode willTransitionTo akan terlihat jika data asinkron dimuat?
Hyra

Anda akan mendapatkan transitionobjek sebagai parameter, jadi Anda cukup memanggil transition.wait(yourPromise). Itu tentu saja berarti Anda harus mengimplementasikan API Anda untuk mendukung promise. Kerugian lain dari pendekatan ini adalah tidak ada cara sederhana untuk mengimplementasikan "indikator pemuatan" di sisi klien. Transisi tidak akan beralih ke komponen pengendali rute sampai semua janji diselesaikan.
tobik

Tapi saya sebenarnya tidak yakin tentang pendekatan "tepat waktu". Beberapa penangan rute bersarang dapat cocok dengan satu url yang berarti bahwa beberapa janji harus diselesaikan. Tidak ada jaminan semuanya akan berakhir pada waktu yang sama. Jika toko lajang, itu bisa menyebabkan konflik. @ Esailija bisakah Anda menjelaskan sedikit jawaban Anda?
tobik

Saya memiliki pipa otomatis di tempat yang mengumpulkan semua janji .waiteduntuk transisi. Setelah semuanya terpenuhi, .runcallback dipanggil. Tepat sebelum .render()saya mengumpulkan semua data bersama-sama dari promise dan menetapkan status singelton store, lalu pada baris berikutnya setelah panggilan render, saya menginisialisasi penyimpanan singleton kembali. Ini cukup hacky tetapi semuanya terjadi secara otomatis dan kode aplikasi komponen dan penyimpanan tetap hampir sama.
Esailija

0

Saya tahu ini mungkin bukan yang Anda inginkan, dan mungkin tidak masuk akal, tetapi saya ingat berhasil sedikit memodifikasi komponen untuk menangani keduanya:

  • rendering di sisi server, dengan semua status awal sudah diambil, secara asinkron jika diperlukan)
  • rendering di sisi klien, dengan ajax jika diperlukan

Jadi sesuatu seperti:

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

});

Maaf, saya tidak memiliki kode persisnya, jadi ini mungkin tidak berhasil, tetapi saya memposting untuk kepentingan diskusi.

Sekali lagi, idenya adalah untuk mengobati sebagian besar komponen sebagai pandangan bodoh, dan berurusan dengan mengambil data sebanyak mungkin keluar dari komponen.


1
Terima kasih. Saya mengerti, tapi sebenarnya bukan itu yang saya inginkan. Katakanlah saya ingin membuat situs web yang lebih kompleks menggunakan React, seperti bbc.com . Melihat halaman tersebut, saya dapat melihat "komponen" di mana-mana. Bagian (olahraga, bisnis ...) adalah komponen tipikal. Bagaimana Anda akan menerapkannya? Di mana Anda akan mengambil semua data terlebih dahulu? Untuk merancang situs yang kompleks, komponen (sebagai prinsip, seperti wadah MVC kecil) adalah cara yang sangat baik (jika mungkin satu-satunya) untuk digunakan. Pendekatan komponen umum untuk banyak kerangka kerja sisi server. Pertanyaannya adalah: bisakah saya menggunakan React untuk itu?
tobik

Anda akan mengambil data di sisi server (seperti yang mungkin dilakukan dalam kasus ini, sebelum meneruskannya ke sistem template sisi server "tradisional"); hanya karena tampilan datanya diuntungkan karena bersifat modular, apakah itu berarti komputasi datanya harus mengikuti struktur yang sama? Saya sedikit bermain sebagai pendukung iblis di sini, saya memiliki masalah yang sama dengan yang Anda alami ketika memeriksa om. Dan saya berharap seseorang memiliki lebih banyak wawasan tentang ini daripada saya - menulis hal-hal dengan mulus di sisi mana pun akan sangat membantu.
phtrivier

1
Yang saya maksud di mana dalam kode. Di pengontrol? Jadi metode pengontrol yang menangani beranda bbc akan berisi lusinan kueri serupa, untuk setiap bagian satu? Itulah cara menuju neraka. Jadi ya, saya lakukan pikir perhitungan yang harus modular juga. Semuanya dikemas dalam satu komponen, dalam satu wadah MVC. Begitulah cara saya mengembangkan aplikasi sisi server standar dan saya cukup yakin bahwa pendekatan ini bagus. Dan alasan mengapa saya sangat senang dengan React.js adalah karena ada potensi besar untuk menggunakan pendekatan ini di sisi klien dan server untuk membuat aplikasi isomorfik yang mengagumkan.
tobik

1
Di situs apa pun (besar / kecil), Anda hanya perlu merender sisi server (SSR) laman saat ini dengan status initnya; Anda tidak memerlukan status init untuk setiap halaman. Server mengambil status init, membuatnya, dan meneruskannya ke klien <script type=application/json>{initState}</script>; dengan begitu data akan berada di HTML. Rehidrasi / ikat peristiwa UI ke halaman dengan memanggil render pada klien. Halaman selanjutnya dibuat oleh kode js klien (mengambil data sesuai kebutuhan) dan diberikan oleh klien. Dengan cara itu setiap penyegaran akan memuat halaman SSR baru & mengklik halaman akan menjadi CSR. = isomorfik & SEO friendly
Federico

0

Saya benar-benar mengacaukannya hari ini, dan meskipun ini bukan jawaban untuk masalah Anda, saya telah menggunakan pendekatan ini. Saya ingin menggunakan Express untuk perutean daripada React Router, dan saya tidak ingin menggunakan Serat karena saya tidak memerlukan dukungan threading di node.

Jadi saya baru saja membuat keputusan bahwa untuk data awal yang perlu diberikan ke penyimpanan fluks saat dimuat, saya akan melakukan permintaan AJAX dan meneruskan data awal ke penyimpanan

Saya menggunakan Fluxxor untuk contoh ini.

Jadi pada rute ekspres saya, dalam hal ini /productsrute:

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

Kemudian saya menginisialisasi metode render yang meneruskan data ke toko.

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

Saya tahu pertanyaan ini ditanyakan setahun yang lalu tetapi kami memiliki masalah yang sama dan kami menyelesaikannya dengan janji bertingkat yang berasal dari komponen yang akan dirender. Pada akhirnya kami memiliki semua data untuk aplikasi dan mengirimkannya begitu saja.

Sebagai contoh:

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

    }

});

dan di router

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

Ingin berbagi dengan Anda pendekatan saya tentang rendering sisi server menggunakan Flux, sedikit disederhanakan misalnya:

  1. Katakanlah kita memiliki componentdata awal dari toko:

    class MyComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          data: myStore.getData()
        };
      }
    }
  2. Jika kelas memerlukan beberapa data pramuat untuk keadaan awal, mari buat Loader untuk MyComponent:

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

    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. Sekarang muat saja data di router:

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

0

hanya sebagai rollup singkat -> GraphQL akan menyelesaikan ini dengan entri untuk tumpukan Anda ...

  • tambahkan GraphQL
  • gunakan apollo dan react-apollo
  • gunakan "getDataFromTree" sebelum Anda mulai merender

-> getDataFromTree akan secara otomatis menemukan semua kueri yang terlibat di aplikasi Anda dan menjalankannya, memisahkan cache apollo Anda di server dan dengan demikian, memungkinkan SSR yang berfungsi penuh .. BÄM

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.