Универсальный JS

 

Как работает серверный рендеринг приложения на React, React-Router и Redux

 

Черепанов Сергей

сооснователь Fullstack Development

sergey@fullstack-development.com

Терминология №1

Изоморфный — имеющий похожую форму, но все-таки отдельный объект

import _ from 'underscore';
import _ from 'lodash';

Терминология №2

Универсальный — применимый во всех случаях


Принципиальная схема

Server

Запрос пользователя за страницей

Сервер отвечает простой статикой index.html

Client

Браузер загружает index.html и bundle.js

Конфигурация SPA (отработка импортов, инициализация

роутов и т.д.)

Роутер определяет по URL-у страницу

SPA подгружает начальные данные от API

SPA начинает постройку DOM-а с на основе данных

Роутер определяет по URL-у страницу

SPA подгружает начальные данные по URL​

SPA рендерит всю разметку в строку

Скорость
SEO

Общая кодовая база

Зачем?

Скорость

С универсальностью

814ms

1147ms

1312ms

Скорость

1147ms

1517ms

БЕЗ универсальности

SEO

1. Yandex и Google умеют анализировать SPA;

2. Другие поисковые системы?

3. Проблемы с ботами других систем, например, социальных сетей, которые анализируют страницу при sharing

erikras/react-redux-universal-hot-example

React Redux Universal Hot Example

  • React
  • React Router
  • Babel
  • Webpack
  • Redux
  • ESLint
  • Express (for frontend server)
  • Webpack-isomorphic-tools

Как это работает у нас

Классическое SPA + дополнительный frontend server

Клиентский браузер

Фронтенд Сервер

Бэкенд с API

Как это работает у нас

Подробная схема

Server

User

Client

SPA начинает постройку DOM-а с самого нуля

handles

server.js

request

incoming

(create store)

initialization

Redux

match the

React-Router

current page

Initializing requests

to our API

React start building​

DOM

request

index.html with content

user gets dynamic

bundle script with 

React and Redux start with

already initialized state

Classic SPA begins

client.js entry point

Начнем по порядку, с server.js

Frontend Server

Simple Express.JS server with one main middleware:

import React from 'react';
const app = new Express();
app.use((req, res) => {
  // пример
  res.send(ReactDOM.renderToString(
    <App store={store} />
  ));
});

ES6 и JSX синтаксис? На сервере?

JSX и импорты .css в node.js?

для компиляции es6 и JSX на лету

require('babel-register')(config);

Вариант №1

•   webpack-isomorphic-tools

Webpack-isomorphic-tools

  • В конфигах описываем для  Webpack-isomorphic-tools, какие файлы ассетов ждать, и как каждый вид обрабатывать

webpack-assets.json
webpack-assets.json
  • Если не js, то ищем файл в

и подставляет его содержимое

  • Во время сборки Webpack-isomorphic-tools кастомным плагином создает
  • Вешает свой хук на все вызовы require, который проверяет, что импортируемый файл - не js

Webpack-assets.json

{
  "javascript": {
    "main": "/assets/main-d8c29e9b2a4623f696e8.js"
  },
  "styles": {
    "main": "/assets/main-d8c29e9b2a4623f696e8.css"
  }
}

JSX и импорты .css в node.js?

Собирать server.js с помощью webpack:

Вариант №2

  • 2 конфига (для сервера и для клиента) и 2 webpack-сервера с вотчерами
  • поддержку source map сделать руками (пока)
resolve.modulesDirectories
  • собрать все URLs для .css и .js файлов, вставить в <head>

  • заменить style-loader на (fake-style-loader или css-loader/locals)
  • еще парочка неожиданных хаков, чтобы все заработало :) (все описано в доках)

  • дублирование всех ассетов для обеих сборок? (вопрос еще поднят в universal-webpack)

Инициализация Redux

По прилету первого запроса на сервер первым делом создается store

app.use((req, res) => {
  const client = new ApiClient(req);  // Next Slide
  const memoryHistory = createHistory(req.originalUrl);
  const store = createStore(memoryHistory, client);
  
  // … more code here
});

ApiClient

Экземпляры класса - обертки для библиотеки по отправке HTTP-запросов:

superagent
  • Добавление хоста к абсолютным путям;

стоило бы просто вообще к                    перейти)

axios
  • Передача кук из запроса на сам сервер, чтобы сохранить сессии и т.п. вещи;

  • Приведение API к более удобному виду (но это из-за

React-Router match the current page

app.use((req, res) => {
  // previous code
  match(
    {
      store,
      routes: getRoutes(store),
      location: req.originalUrl
    },
    (error, redirectLocation, renderProps) => {
      if (redirectLocation) {
        res.redirect(redirectLocation.pathname);
      } else if (error) {
        res.status(500);
      }
    // … more code here
  });
});

Initializing requests to our API

Redux Async Connect

@asyncConnect({
  lunch: (params, helpers) => Promise.resolve(
    {id: 1, name: 'Borsch'}
  )
})
class App extends React.Component {
  render() {
    const lunch = this.props.lunch
    return (<div>{lunch.name}</div>);
  }
}
  • Задерживает рендеринг страницы до тех пор, пока не выполнены асинхронные задачи;

  • Позволяет сохранить в state полученные данные

Redux Async Connect

// previous code
loadOnServer({ 
  ...renderProps, 
  store,
  helpers: { client } 
}).then(() => {
   // store is populated with server response
}); 
  • Если frontend-сервер и API находятся на одной машине, то данные можно получать с localhost!

React рендерит HTML в строку

// previous code
const component = (
  <Provider store={store} key="provider">
    <ReduxAsyncConnect {...renderProps} />
  </Provider>
);
res.send(
  '<!doctype html>\n' +
  ReactDOM.renderToString(
    <Html
      assets={webpackIsomorphicTools.assets()}
      component={component}
      store={store}
    />
  )
);

index.html

// Html.js
class Html extends Component {
  static propTypes = {
    assets: PropTypes.object,
    component: PropTypes.node,
    store: PropTypes.object,
  };
  render() {
    const content = component ? ReactDOM.renderToString(component) : '';
    return (
      <html lang="en-us">
        <head>
          // meta tags, links to css etc
        </head>
        <body>
          <div id="content" dangerouslySetInnerHTML={{ __html: content }} />
          <script 
            dangerouslySetInnerHTML={{ 
              __html: `window.__data=${serialize(store.getState())};` }} 
            charSet="UTF-8" 
          />
          <script src={assets.javascript.main} {...mainScriptAttrs} charSet="UTF-8"/>
        </body>
      </html>
    );
  }
}

1

2

3

4

client.js - входная точка на клиенте

ReactDOM.render(...)
  • Собираем заново клиентский store из уже инициализированного state

  • Подключение React-Router
  • _
  •  
window.onerror(...)

Как React подключается к уже отданной верстке

.render()
  • Метод                       должен быть чистым

  • Сравниваем                      с тем значением, что получено на основе клиентского рендеринга

  • React подключает обработчики событий к уже существующему DOM  

checksums

Old-school библиотеки

<script src="...?callback=initMap"></script>
window.initMap = function () {
  console.log(window.google);
}

Наша компонента для инициализации использует объект                               (например, автодополнение для адресов)

window.google

Client-side detection

Invoked once, only on the client (not on the server), immediately after the initial rendering occurs

class SomeEl extends Component {
  componentDidMount() {
    this.setState({inBrowser: true});
  }
  render() {
    if (this.state.inBrowser) {
      // render with window.google
    }
  }
}

Область применения

  • Большой тяжелый проект с кучей запросов при старте некоторых страниц (например, лента какого-нибудь сложного агрегатора);
  • Большие требования к скорости (начальной загрузке страницы);
  • Большие требования к полной SEO-совместимости

Спасибо

Черепанов Сергей

сооснователь Fullstack Development

sergey@fullstack-development.com

Sergey

By julya_key09

Sergey

  • 460