SSR: rendu d'une application ReactJS dans le backend en utilisant PHP





Notre tâche était de mettre en place un constructeur de site Web. Sur le front, tout est exécuté par une application React qui, en fonction des actions de l'utilisateur, génère du JSON avec des informations sur la façon de construire du HTML et le stocke dans le backend PHP. Au lieu de dupliquer la logique d'assemblage HTML sur le backend, nous avons décidé de réutiliser le code JS. Évidemment, cela simplifiera la maintenance, puisque le code ne changera qu'à un seul endroit par une seule personne. Ici, Server Side Rendering vient à la rescousse avec le moteur V8 et l'extension PHP V8JS.



Dans cet article, nous verrons comment nous avons utilisé les V8J pour notre tâche spécifique, mais les cas d'utilisation ne se limitent pas à cela. Le plus évident est la possibilité d'utiliser le rendu côté serveur pour répondre à vos besoins en matière de référencement.



Mise en place



Nous utilisons Symfony et Docker, donc la première étape consiste à initialiser un projet vide et à configurer l'environnement. Notons les principaux points:



  1. L'extension V8Js doit être installée dans le 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. Installer React et ReactDOM de la manière la plus simple

  3. Ajouter une route d'index et un contrôleur par défaut:



    <?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. Ajoutez le modèle index.html.twig avec React inclus



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


En utilisant



Pour démontrer la V8, créons un simple script de rendu H1 et P avec le texte 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);


Accédez à localhost: 8088 (8088 est spécifié dans docker-compose.yml comme port nginx):







  1. DB

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


  2. Route

    /**
    * @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()]);
    }




Nous appuyons sur le bouton Enregistrer, lorsque vous cliquez sur notre itinéraire, JSON est envoyé:



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


En réponse, l'ID de l'enregistrement dans la base de données est renvoyé:



/**
* @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()]);
}


Maintenant que vous avez quelques données de test, vous pouvez essayer le V8 en action. Pour ce faire, vous devrez esquisser un script React qui formera des composants à partir des accessoires Dom passés. Mettons-le à côté d'autres actifs et appelons-le 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);
           })
       );
   }
}


Afin de former une chaîne à partir de l'arborescence DOM générée, nous utiliserons le composant ReactDomServer (https://unpkg.com/browse/react-dom@16.13.0/umd/react-dom-server.browser.production.min.js). Écrivons une route avec du HTML prêt:




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


Ici:



  1. reactPath - chemin vers react.js
  2. domPath - chemin vers react-dom.js
  3. domServerPath - chemin vers react-dom-server.js
  4. ssrPath - le chemin vers notre script ssr.js


Suivez le lien / publier / 3:







Comme vous pouvez le voir, tout a été rendu exactement comme nous en avons besoin.



Conclusion



En conclusion, je voudrais dire que le rendu côté serveur ne s'avère pas si difficile et peut être très utile. La seule chose à ajouter ici est que le rendu peut prendre un certain temps, et il est préférable d'ajouter une file d'attente ici - RabbitMQ ou Gearman.



Le code source PPS peut être consulté ici https://github.com/damir-in/ssr-php-symfony



Auteurs

damir_in zinvapel



All Articles