SSR: عرض تطبيق ReactJS في الواجهة الخلفية باستخدام PHP





كانت مهمتنا تنفيذ منشئ مواقع الويب. في المقدمة ، يتم تشغيل كل شيء بواسطة تطبيق React الذي يقوم ، بناءً على إجراءات المستخدم ، بإنشاء JSON بمعلومات حول كيفية إنشاء HTML ، وتخزينه في PHP الخلفية. بدلاً من تكرار منطق تجميع HTML على الواجهة الخلفية ، قررنا إعادة استخدام كود JS. من الواضح أن هذا سوف يبسط الصيانة ، لأن الرمز لن يتغير إلا في مكان واحد بواسطة شخص واحد. هنا يأتي عرض جانب الخادم للإنقاذ مع محرك V8 و PHP-extension V8JS.



في هذه المقالة ، سنغطي كيفية استخدامنا لمحركات V8J في مهمتنا المحددة ، ولكن حالات الاستخدام لا تقتصر على ذلك فقط. الأمر الأكثر وضوحًا هو القدرة على استخدام العرض الجانبي للخادم لتلبية احتياجات تحسين محركات البحث.



التخصيص



نستخدم Symfony و Docker ، لذا فإن الخطوة الأولى هي تهيئة مشروع فارغ وإعداد البيئة. دعنا نلاحظ النقاط الرئيسية:



  1. يجب تثبيت امتداد V8Js في 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. تثبيت React و ReactDOM بأسهل طريقة

  3. أضف مسار الفهرس ووحدة التحكم الافتراضية:



    <?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. أضف النموذج index.html.twig مع تضمين React



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


باستخدام



لإثبات V8 ، دعنا ننشئ نصًا بسيطًا للعرض H1 و P مع نص الأصول / 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);


انتقل إلى المضيف المحلي: 8088 (تم تحديد 8088 في Docker-compose.yml كمنفذ nginx):







  1. DB

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


  2. طريق

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




نضغط على زر الحفظ ، عندما نضغط على طريقنا ، يتم إرسال JSON:



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


استجابةً لذلك ، يتم إرجاع معرف السجل في قاعدة البيانات:



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


الآن بعد أن أصبح لديك بعض بيانات الاختبار ، يمكنك تجربة V8 أثناء العمل. للقيام بذلك ، ستحتاج إلى رسم سكربت React الذي سيشكل مكونات من دعائم Dom التي تم تمريرها. لنضعها بجانب الأصول الأخرى ونسميها 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);
           })
       );
   }
}


من أجل تكوين سلسلة من شجرة DOM التي تم إنشاؤها ، سنستخدم مكون ReactDomServer (https://unpkg.com/browse/react-dom@16.13.0/umd/react-dom-server.browser.production.min.js). لنكتب مسارًا باستخدام 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)
       );
}


هنا:



  1. رد فعل - مسار رد. js
  2. domPath - مسار رد فعل dom.js
  3. domServerPath - مسار رد فعل dom-server.js
  4. ssrPath - المسار إلى النص ssr.js الخاص بنا


اتبع الرابط / انشر / 3:







كما ترى ، تم تقديم كل شيء تمامًا كما نحتاج إليه.



خاتمة



في الختام ، أود أن أقول إن عرض جانب الخادم تبين أنه ليس صعبًا ويمكن أن يكون مفيدًا للغاية. الشيء الوحيد الذي يستحق الإضافة هنا هو أن العرض قد يستغرق وقتًا طويلاً ، ومن الأفضل إضافة قائمة انتظار هنا - RabbitMQ أو Gearman.



يمكن الاطلاع على كود مصدر PPS هنا https://github.com/damir-in/ssr-php-symfony



Authors

دامير زينفابيل



All Articles