[PHP, Symfony, ReactJS] SSR: рендеринг ReactJS приложения на бекэнде используя PHP
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Перед нами стояла задача реализовать конструктор сайтов. На фронте всем управляет React-приложение, которое на основе действий пользователя, формирует JSON с информацией о том, как построить HTML, и сохраняет его на PHP бэкенд. Вместо дублирования логики сборки HTML на бэкенде, мы решили переиспользовать JS-код. Очевидно, что это упросит поддержку, так как код будет меняться только в одном месте одним человеком. Тут нам на помощь приходит Server Side Rendering вместе с движком V8 и PHP-extension V8JS.
В этой статье мы расскажем, как мы использовали V8Js для нашей конкретной задачи, но варианты использования не ограничиваются только этим. Самым очевидным выглядит возможность использовать Server Side Rendering для реализации SEO-потребностей.
Настройка
Мы используем Symfony и Docker, поэтому первым делом необходимо инициализировать пустой проект и настроить окружение. Отметим основные моменты:
- В Dockerfile необходимо установить V8Js-extension:
...
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
...
- Устанавливаем React и ReactDOM самым простым способом (https://ru.reactjs.org/docs/add-react-to-a-website.html)
- Добавляем index роут и дефолтный контроллер:
<?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');
}
}
- Добавляем шаблон 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):
- БД
create table data(
id serial not null primary key,
data json not null
);
- Роут
/**
* @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 (https://unpkg.com/browse/react-dom@16.13.0/umd/reac...duction.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)
);
}
Здесь:
- reactPath — путь до react.js
- domPath — путь до react-dom.js
- domServerPath — путь до react-dom-server.js
- 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
===========
Похожие новости:
- [Разработка веб-сайтов, PHP, Программирование, Будущее здесь] Как будет выглядеть программирование в 2025 году? (перевод)
- Началось бета-тестирование PHP 8
- [Open source, Виртуализация, Разработка под Linux, Openshift] Современные приложения на OpenShift, часть 2: связанные сборки chained builds
- [Разработка веб-сайтов, JavaScript, Интерфейсы, ReactJS] Concurrent Mode в React: адаптируем веб-приложения под устройства и скорость интернета
- [JavaScript, Интерфейсы, ReactJS, TypeScript] Когда и CRA мало. Доклад Яндекса
- [CMS, JavaScript, Распределённые системы] Создание собственной Headless CMS и интеграция с блогом (перевод)
- [Ajax, PHP, MySQL, JavaScript, jQuery] Пишем комментарии для сайта на чистом PHP + MySQL + Ajax
- [Big Data, Машинное обучение] Разворачиваем модель машинного обучения с Docker – Часть 1 (перевод)
- [*nix, Настройка Linux, Софт] NextCloud в качестве сервиса по созданию защищенных ссылок
- [JavaScript, Node.JS, PostgreSQL, ReactJS] Javascript платформа Objectum
Теги для поиска: #_php, #_symfony, #_reactjs, #_php, #_symfony, #_docker, #_reactjs, #_php, #_symfony, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:36
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Перед нами стояла задача реализовать конструктор сайтов. На фронте всем управляет React-приложение, которое на основе действий пользователя, формирует JSON с информацией о том, как построить HTML, и сохраняет его на PHP бэкенд. Вместо дублирования логики сборки HTML на бэкенде, мы решили переиспользовать JS-код. Очевидно, что это упросит поддержку, так как код будет меняться только в одном месте одним человеком. Тут нам на помощь приходит Server Side Rendering вместе с движком V8 и PHP-extension V8JS. В этой статье мы расскажем, как мы использовали V8Js для нашей конкретной задачи, но варианты использования не ограничиваются только этим. Самым очевидным выглядит возможность использовать Server Side Rendering для реализации SEO-потребностей. Настройка Мы используем Symfony и Docker, поэтому первым делом необходимо инициализировать пустой проект и настроить окружение. Отметим основные моменты:
Использование Для демонстрации 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):
Нажимаем кнопку сохранить, при нажатии на наш роут отправляется 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 (https://unpkg.com/browse/react-dom@16.13.0/umd/reac...duction.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) ); } Здесь:
Переходим по ссылке /publish/3: Как видно, все было отрисовано именно так, как нам нужно. Заключение В заключении хочется сказать, что Server Side Rendering оказывается не таким уж сложным и может быть очень полезным. Единственное что стоит здесь добавить — рендер может занимать достаточно долгое время, и сюда лучше добавить очередь — RabbitMQ или Gearman. P.P.S. Исходный код можно посмотреть тут https://github.com/damir-in/ssr-php-symfony Авторы damir_in zinvapel =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:36
Часовой пояс: UTC + 5