Русский
Русский
English
Статистика
Реклама

SSR рендеринг ReactJS приложения на бекэнде используя PHP



Перед нами стояла задача реализовать конструктор сайтов. На фронте всем управляет React-приложение, которое на основе действий пользователя, формирует JSON с информацией о том, как построить HTML, и сохраняет его на PHP бэкенд. Вместо дублирования логики сборки HTML на бэкенде, мы решили переиспользовать JS-код. Очевидно, что это упросит поддержку, так как код будет меняться только в одном месте одним человеком. Тут нам на помощь приходит Server Side Rendering вместе с движком V8 и PHP-extension V8JS.

В этой статье мы расскажем, как мы использовали V8Js для нашей конкретной задачи, но варианты использования не ограничиваются только этим. Самым очевидным выглядит возможность использовать Server Side Rendering для реализации SEO-потребностей.

Настройка


Мы используем Symfony и Docker, поэтому первым делом необходимо инициализировать пустой проект и настроить окружение. Отметим основные моменты:

  1. В Dockerfile необходимо установить V8Js-extension:

    ...RUN apt-get install -y software-properties-commonRUN add-apt-repository ppa:stesie/libv8 && apt-get updateRUN apt-get install -y libv8-7.5 libv8-7.5-dev g++ expectRUN 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 installRUN echo extension=v8js.so > /etc/php/7.2/fpm/conf.d/99-v8js.iniRUN echo extension=v8js.so > /etc/php/7.2/cli/conf.d/99-v8js.ini...
    

  2. Устанавливаем React и ReactDOM самым простым способом
  3. Добавляем index роут и дефолтный контроллер:

    <?phpdeclare(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 с текстом 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);

Переходим на localhost:8088 (8088 указан в docker-compose.yml как порт nginx):



  1. БД
    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 компоненты. Положим его рядом с другими assets и назовем 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 (http://personeltest.ru/aways/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 <<<JSvar 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. reactPath путь до react.js
  2. domPath путь до react-dom.js
  3. domServerPath путь до react-dom-server.js
  4. ssrPath путь до нашего скрипта ssr.js

Переходим по ссылке /publish/3:



Как видно, все было отрисовано именно так, как нам нужно.

Заключение


В заключении хочется сказать, что Server Side Rendering оказывается не таким уж сложным и может быть очень полезным. Единственное что стоит здесь добавить рендер может занимать достаточно долгое время, и сюда лучше добавить очередь RabbitMQ или Gearman.

P.P.S. Исходный код можно посмотреть тут https://github.com/damir-in/ssr-php-symfony

Авторы
damir_in zinvapel
Источник: habr.com
К списку статей
Опубликовано: 10.08.2020 14:22:42
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Php

Symfony

Reactjs

Docker

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru