SSR: Rendern einer ReactJS-Anwendung im Backend mit PHP





Unsere Aufgabe war es, einen Website Builder zu implementieren. Auf der Vorderseite wird alles von einer React-Anwendung ausgeführt, die basierend auf Benutzeraktionen JSON mit Informationen zum Erstellen von HTML generiert und im PHP-Backend speichert. Anstatt die HTML-Assembly-Logik im Backend zu duplizieren, haben wir beschlossen, den JS-Code wiederzuverwenden. Dies vereinfacht natürlich die Wartung, da sich der Code nur an einer Stelle von einer Person ändert. Hier hilft Server Side Rendering zusammen mit der V8-Engine und der PHP-Erweiterung V8JS.



In diesem Artikel wird erläutert, wie wir V8Js für unsere spezifische Aufgabe verwendet haben, aber die Anwendungsfälle sind nicht nur darauf beschränkt. Am offensichtlichsten ist die Möglichkeit, Server Side Rendering zu verwenden, um Ihre SEO-Anforderungen zu erfüllen.



Einrichten



Wir verwenden Symfony und Docker. Der erste Schritt besteht darin, ein leeres Projekt zu initialisieren und die Umgebung einzurichten. Beachten wir die wichtigsten Punkte:



  1. Die V8Js-Erweiterung muss in der Docker-Datei installiert sein:



    ...
    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. React und ReactDOM auf einfachste Weise installieren

  3. Indexroute und Standardcontroller hinzufügen:



    <?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. Fügen Sie die Vorlage index.html.twig mit React hinzu



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


Verwenden von



Um V8 zu demonstrieren, erstellen wir ein einfaches H1- und P-Rendering-Skript mit Assets / front.jsx-Text:



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


Gehen Sie zu localhost: 8088 (8088 wird in docker-compose.yml als Nginx-Port angegeben):







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




Wir klicken auf die Schaltfläche Speichern. Wenn Sie auf unsere Route klicken, wird JSON gesendet:



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


Als Antwort wird die ID des Datensatzes in der Datenbank zurückgegeben:



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


Nachdem Sie einige Testdaten haben, können Sie den V8 in Aktion testen. Dazu müssen Sie ein React-Skript skizzieren, das Komponenten aus den übergebenen Dom-Requisiten bildet. Stellen wir es neben andere Assets und nennen es 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);
           })
       );
   }
}


Um eine Zeichenfolge aus dem generierten DOM-Baum zu bilden, verwenden wir die ReactDomServer-Komponente (https://unpkg.com/browse/react-dom@16.13.0/umd/react-dom-server.browser.production.min.js). Schreiben wir eine Route mit fertigem HTML:




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


Hier:



  1. reactPath - Pfad zu react.js
  2. domPath - Pfad zu react-dom.js
  3. domServerPath - Pfad zu react-dom-server.js
  4. ssrPath - der Pfad zu unserem ssr.js-Skript


Folgen Sie dem Link / Publish / 3:







Wie Sie sehen, wurde alles genau so gerendert, wie wir es brauchen.



Fazit



Abschließend möchte ich sagen, dass das serverseitige Rendern nicht so schwierig ist und sehr nützlich sein kann. Das einzige, was es wert ist, hier hinzugefügt zu werden, ist, dass das Rendern ziemlich lange dauern kann, und es ist besser, hier eine Warteschlange hinzuzufügen - RabbitMQ oder Gearman.



Der PPS-Quellcode kann hier eingesehen werden: https://github.com/damir-in/ssr-php-symfony



Autoren

damir_in Zinvapel



All Articles