SSR: merender aplikasi ReactJS di backend menggunakan PHP





Tugas kami adalah menerapkan pembuat situs web. Di depan, semuanya dijalankan oleh aplikasi React yang, berdasarkan aksi pengguna, menghasilkan JSON dengan informasi tentang bagaimana membangun HTML, dan menyimpannya di backend PHP. Alih-alih menduplikasi logika perakitan HTML di backend, kami memutuskan untuk menggunakan kembali kode JS. Jelas, ini akan menyederhanakan pemeliharaan, karena kode hanya akan berubah di satu tempat oleh satu orang. Di sini Server Side Rendering datang untuk menyelamatkan bersama dengan mesin V8 dan ekstensi PHP V8JS.



Di artikel ini, kami akan membahas bagaimana kami menggunakan V8J untuk tugas khusus kami, tetapi kasus penggunaan tidak terbatas hanya itu. Yang paling jelas adalah kemampuan untuk menggunakan Server Side Rendering untuk memenuhi kebutuhan SEO Anda.



Pengaturan



Kami menggunakan Symfony dan Docker, jadi langkah pertama adalah menginisialisasi proyek kosong dan mengatur lingkungan. Mari perhatikan poin utama:



  1. Ekstensi V8Js harus diinstal di Dockerfile:



    ...
    RUN apt-get install -y software-properties-common
    RUN add-apt-repository ppa:stesie/libv8 && apt-get update
    RUN apt-get install -y libv8-7.5 libv8-7.5-dev g++ expect
    RUN git clone https://github.com/phpv8/v8js.git /usr/local/src/v8js && \
       cd /usr/local/src/v8js && phpize && ./configure --with-v8js=/opt/libv8-7.5 && \
       export NO_INTERACTION=1 && make all -j4 && make test install
    
    RUN echo extension=v8js.so > /etc/php/7.2/fpm/conf.d/99-v8js.ini
    RUN echo extension=v8js.so > /etc/php/7.2/cli/conf.d/99-v8js.ini
    ...
    


  2. Menginstal React dan ReactDOM dengan cara termudah

  3. Tambahkan rute indeks dan pengontrol default:



    <?php
    declare(strict_types=1);
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    
    final class DefaultController extends AbstractController
    {
       /**
        * @Route(path="/")
        */
       public function index(): Response
       {
           return $this->render('index.html.twig');
       }
    }
    


  4. Tambahkan template index.html.twig dengan React disertakan



    <html>
    <body>
        <div id="app"></div>
        <script src="{{ asset('assets/react.js') }}"></script>
        <script src="{{ asset('assets/react-dom.js') }}"></script>
        <script src="{{ asset('assets/babel.js') }}"></script>
        <script type="text/babel" src="{{ asset('assets/front.jsx') }}"></script>
    </body>
    </html>
    


Menggunakan



Untuk mendemonstrasikan V8, mari buat skrip render H1 dan P sederhana dengan teks assets / front.jsx:



'use strict';

class DataItem extends React.Component {
   constructor(props) {
       super(props);

       this.state = {
           checked: props.name,
           names: ['h1', 'p']
       };

       this.change = this.change.bind(this);
       this.changeText = this.changeText.bind(this);
   }

   render() {
       return (
           <li>
               <select value={this.state.checked} onChange={this.change} >
                   {
                       this.state.names.map((name, k) => {
                           return (
                               <option key={k} value={name}>{name}</option>
                           );
                       })
                   }
               </select>
               <input type='text' value={this.state.value} onChange={this.changeText} />
           </li>
       );
   }

   change(e) {
       let newval = e.target.value;
       if (this.props.onChange) {
           this.props.onChange(this.props.number, newval)
       }
       this.setState({checked: newval});
   }

   changeText(e) {
       let newval = e.target.value;
       if (this.props.onChangeText) {
           this.props.onChangeText(this.props.number, newval)
       }
   }
}

class DataList extends React.Component {
   constructor(props) {
       super(props);
       this.state = {
           message: null,
           items: []
       };

       this.add = this.add.bind(this);
       this.save = this.save.bind(this);
       this.updateItem = this.updateItem.bind(this);
       this.updateItemText = this.updateItemText.bind(this);
   }

   render() {
       return (
           <div>
               {this.state.message ? this.state.message : ''}
               <ul>
                   {
                       this.state.items.map((item, i) => {
                           return (
                               <DataItem
                                   key={i}
                                   number={i}
                                   value={item.name}
                                   onChange={this.updateItem}
                                   onChangeText={this.updateItemText}
                               />
                           );
                       })
                   }
               </ul>
               <button onClick={this.add}></button>
               <button onClick={this.save}></button>
           </div>
       );
   }

   add() {
       let items = this.state.items;
       items.push({
           name: 'h1',
           value: ''
       });

       this.setState({message: null, items: items});
   }

   save() {
       fetch(
           '/save',
           {
               method: 'POST',
               headers: {
                   'Content-Type': 'application/json;charset=utf-8'
               },
               body: JSON.stringify({
                   items: this.state.items
               })
           }
       ).then(r => r.json()).then(r => {
           this.setState({
               message: r.id,
               items: []
           })
       });
   }

   updateItem(k, v) {
       let items = this.state.items;
       items[k].name = v;

       this.setState({items: items});
   }

   updateItemText(k, v) {
       let items = this.state.items;
       items[k].value = v;

       this.setState({items: items});
   }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(React.createElement(DataList), domContainer);


Pergi ke localhost: 8088 (8088 ditentukan di docker-compose.yml sebagai port nginx):







  1. DB

    create table data(
       id serial not null primary key,
       data json not null
    );


  2. Rute

    /**
    * @Route(path="/save")
    */
    public function save(Request $request): Response
    {
       $em = $this->getDoctrine()->getManager();
    
       $data = (new Data())->setData(json_decode($request->getContent(), true));
       $em->persist($data);
       $em->flush();
    
       return new JsonResponse(['id' => $data->getId()]);
    }




Kami menekan tombol simpan, ketika kami mengklik rute kami, JSON dikirim:



{
  "items":[
     {
        "name":"h1",
        "value":" "
     },
     {
        "name":"p",
        "value":" "
     },
     {
        "name":"h1",
        "value":"  "
     },
     {
        "name":"p",
        "value":"   "
     }
  ]
}


Sebagai tanggapan, pengidentifikasi catatan dalam database dikembalikan:



/**
* @Route(path="/save")
*/
public function save(Request $request): Response
{
   $em = $this->getDoctrine()->getManager();

   $data = (new Data())->setData(json_decode($request->getContent(), true));
   $em->persist($data);
   $em->flush();

   return new JsonResponse(['id' => $data->getId()]);
}


Sekarang setelah Anda memiliki beberapa data uji, Anda dapat mencoba V8 beraksi. Untuk melakukan ini, Anda perlu membuat sketsa skrip React yang akan membentuk komponen dari props Dom yang diteruskan. Mari kita letakkan di sebelah aset lain dan beri nama ssr.js:



'use strict';

class Render extends React.Component {
   constructor(props) {
       super(props);
   }

   render() {
       return React.createElement(
           'div',
           {},
           this.props.items.map((item, k) => {
               return React.createElement(item.name, {}, item.value);
           })
       );
   }
}


Untuk membentuk string dari pohon DOM yang dihasilkan, kita akan menggunakan komponen ReactDomServer (https://unpkg.com/browse/react-dom@16.13.0/umd/react-dom-server.browser.production.min.js). Mari tulis rute dengan HTML yang sudah siap:




/**
* @Route(path="/publish/{id}")
*/
public function renderPage(int $id): Response
{
   $data = $this->getDoctrine()->getManager()->find(Data::class, $id);

   if (!$data) {
       return new Response('<h1>Page not found</h1>', Response::HTTP_NOT_FOUND);
   }

   $engine = new \V8Js();

   ob_start();
   $engine->executeString($this->createJsString($data));

   return new Response(ob_get_clean());
}

private function createJsString(Data $data): string
{
   $props = json_encode($data->getData());
   $bundle = $this->getRenderString();

   return <<<JS
var global = global || this, self = self || this, window = window || this;
$bundle;
print(ReactDOMServer.renderToString(React.createElement(Render, $props)));
JS;
}

private function getRenderString(): string
{
   return
       sprintf(
           "%s\n%s\n%s\n%s",
           file_get_contents($this->reactPath, true),
           file_get_contents($this->domPath, true),
           file_get_contents($this->domServerPath, true),
           file_get_contents($this->ssrPath, true)
       );
}


Sini:



  1. reactPath - jalur ke react.js
  2. domPath - jalur ke react-dom.js
  3. domServerPath - jalur ke react-dom-server.js
  4. ssrPath - jalur ke skrip ssr.js kita


Ikuti tautan / terbitkan / 3:







Seperti yang Anda lihat, semuanya telah ditampilkan persis seperti yang kami butuhkan.



Kesimpulan



Sebagai kesimpulan, saya ingin mengatakan bahwa Server Side Rendering tidak terlalu sulit dan bisa sangat berguna. Satu-satunya hal yang perlu ditambahkan di sini adalah rendering dapat memakan waktu cukup lama, dan lebih baik menambahkan antrean di sini - RabbitMQ atau Gearman.



Source code PPS dapat dilihat disini https://github.com/damir-in/ssr-php-symfony



Penulis

damir_in zinvapel



All Articles