Серверний рендеринг, розділення коду і ліниве завантаження з React Router v4

11 хв. читання

Деякі відомості про серверний рендеринг в Airbnb

Історично, Airbnb був Rails застосунком. Декілька років тому це почало змінюватись. Ми почали використовувати Rails тільки на рівні даних, а всю логіку візуалізації переносити в JS у формі React. Щоб підтримувати серверний рендеринг, ми створили сервіс Javascript рендерингу Hypernova з відкритим кодом.

Ми презентували маршрутизацію на стороні клієнта і розбивання коду на основі маршрутів з React Router v3 як частину оновлення архітектури, вивівши серверний рендеринг на новий рівень.

Відкриваємо React Router v4

Серверний рендеринг + розділення коду зводиться до однієї вимоги: щоб вони працювали разом, ви повинні мати можливість зіставити поточний маршрут перед рендерингом.

(Решта цієї публікації є досить важкою в плані коду, тому якщо ви ще не знайомі з RRv4, знадобиться деякий час, щоб прочитати документацію React Router. Крім того не зашкодить глянути документацію webpack з розділення коду).

Проблема полягає в тому, що React Router v4 перейшов з централізованої конфігурації маршрутів (з функцією getComponent для асинхронного завантаження) до децентралізованої версії. Маршрути зараз можна вбудовувати у компонент таким чином:

export default function App() {
  return (
    <div>
      <h1>Welcome!</h1>
      <Route path="/about" component={About}/>
      <Route path="/dashboard" component={Dashboard}/>
    </div>
  );
}

Визначення маршрутів таким чином означає, що ви не будете знати, які маршрути/компоненти потрібні, щоб відрендерити вашу сторінку допоки власне не почнеться рендеринг. Щоб продемонструвати, чому це є проблемою, уявіть, що ми асинхронно завантажимо компонент About.

Коли ми рендеримо на стороні сервера, то хочемо відобразити увесь контент, тому html для компонента About буде згенеровано і вставлено у DOM дерево. Однак на стороні клієнта ми не зможемо ні зіставити маршрут about, ні компонент About поки не увійдемо у цикл рендерингу. Це призведе до помилки невідповідності клієнт-сервер, тому що не завантаживши компонент About, клієнт не зможе створити такий же html як і сервер. Це також ймовірно спричинить мерехтіння контенту і втрату рендеру, що веде до гіршого досвіду ваших користувачів.

Повторна централізація маршрутів

Для вирішення унікальних проблем із вбудованими маршрутами було створено react-router-config. Цей конфіг дозволить вам й надалі визначати маршрути централізовано і зіставляти їх до запуску початкового рендеру. З використанням бібліотеки, визначення маршрутів може мати такий вигляд:

const routes = [{ 
  component: Root,
  routes: [
    {
      path: '/',
      exact: true,
      component: Home,
    },
    {
      path: '/child/:id',
      component: Child,
      routes: [{
        path: '/child/:id/grandchild',
        component: require('./Grandchild.jsx'),
      }],
    },
  ],
}];
export default routes;

Децентралізація повторно централізованих маршрутів

(Я знаю, що ви думаєте...)

Використовувати react-router-config – чудово, але все ще залишається трохи роботи. В react-router-config немає підтримки завантаження компонентів асинхронно і дочірні маршрути повинні бути заданими надто явно. Зверніть увагу, що всі значення path є абсолютними.

Такий запис може стати громіздким і обмежує повторне використання. Щоб вирішити цю проблему, ми використали функцію, яка дозволяє компонентам визначати дочірні маршрути

// Це рекурсивно перетворить наш формат маршрутів у той формат,
// який очікує react-router-config
export default function convertCustomRouteConfig(routes, parentRoute) {
  return customRouteConfig.map((route) => {
    const pathResult = typeof route.path === 'function' ? route.path(parentRoute || '') : `${parentRoute}/${route.path}`;
    return {
      path: pathResult.replace('//', '/'),
      component: route.component,
      exact: route.exact,
      routes: route.routes ? convertCustomRouteConfig(route.routes, pathResult) : [],
    };
  });
  return mapping;
}
// convertCustomRouteConfig.js
// Визначення одного маршруту
export default {
  path: (parentPath) => `${parentPath}/grand-child`,
  component: require('./Grandchild.jsx'),
  routes: [
    // стільки маршрутів, скільки потрібно.
  ]
};
// grandchildRoute.js
import grandchildRoute from './grandchildRoute';
import convertCustomRouteConfig from './convertCustomRouteConfig';
const routes = [{
  component: Root,
  routes: [
    {
      path: '/',
      exact: true,
      component: Home,
    },
    {
      path: '/child/:id',
      component: Child,
      routes: [
        grandchildRoute,
      ],
    },
  ],
}];
export default convertCustomRouteConfig(routes);
// routes.js

Це дасть нам більше свободи під час розробки. Маршруту другого рівня вкладеності (grandchildRoute) більше не потрібно знати його повний шлях, і його можна вставити у центральну конфігурацію у будь-яке місце розташування для створення глибоко вкладених маршрутів.

Такий спосіб організації маршрутів є дуже дієвим, особливо у великих проектах. Маршрути можуть бути повторно використані в декількох місцях, а логіка компонентів не буде прив'язаною до маршрутів, що збільшує можливість їх повторного використання. Це робить наш поточний перехід до великого односторінкового застосунку(SPA) можливим, оскільки ми можемо створювати додаткові маршрути, без збільшення нашого основного потоку. З ростом нашого SPA при додаванні нових сторінок продукту, ми будемо впевнені в тому, що будь-яка окрема сторінка містить лише те, що потрібно.

Визначення асинхронного маршруту

Ми повинні забезпечити завантаження компонентів ще до виклику рендеру, незалежно від того, який маршрут обрано.

Для початку, створимо асинхронне визначення компоненту, після цього змінимо маршрут другого рівня вкладеності, щоб використати новий компонент. Зверніть увагу на статичну функцію load, пізніше її буде використано, щоб переконатися у готовності до завантаження.

/**
* Повертає новий React компонент, готовий для наслідування (прим.: для створення екземплярів об'єкту).
* Зверніть увагу, що тут замикання захищає Component, і передає унікальний
* екземпляр Component у статичну реалізацію `load`.
*  
 */
export function generateAsyncRouteComponent({ loader, Placeholder }) {
  let Component = null;

  return class AsyncRouteComponent extends React.Component {
    constructor() {
      super();
      this.updateState = this.updateState.bind(this);
      this.state = {
        Component,
      };
    }
    
    componentWillMount() {
      AsyncRouteComponent.load().then(this.updateState);
    }
    
    updateState() {
      // Оновіть стан тільки якщо у нас ще немає посилання на
      // компонент, це дозволить уникнути зайвих рендерів.
      if (this.state.Component !== Component) {
        this.setState({
          Component,
        });
      }
    }
    
    /**
     * Static - для того, щоб можна було викликати load, навіть якщо версія компоненту не готова до наслідування.
     * Цей метод потрібно викликати лише раз, до початку рендеру.
    */
    static load() {
      return loader().then((ResolvedComponent) => {
       Component = ResolvedComponent.default || ResolvedComponent;
      });
    }

    render() {
      const { Component: ComponentFromState } = this.state;
      if (ComponentFromState) {
       return <ComponentFromState {...this.props} />;
      }
      if (Placeholder) {
       return <Placeholder {...this.props} />;
      }
      return null;
    }
  };
}
//generateAsyncComponent.js

Також, оновімо маршрут другого рівня вкладеності, щоб використати нову допоміжну функцію.

import generateAsyncComponent from './generateAsyncComponent';

// Це визначення одного маршруту
export default {
  path: (parentPath) => `${parentPath}/grand-child`,
  component: generateAsyncComponent(() => import('./Grandchild.jsx'));
  routes: [
    // стільки маршрутів, скільки потрібно.
  ]
};
// grandchildRoute.js

Забезпечення готовності маршрутів

Тепер ми експортуємо конфігурацію маршрутів, де є маршрут із компонентом, що визначає статичну load функцію. Все, що залишилось зробити до початку рендеру – впевнитись, що усе завантажується.

import routes from './routes';
import { matchRoutes } from 'react-router-config';

/**
 * Спочатку зіставте маршрути за допомогою функції `matchRoutes` з react-router-config.
 * Тоді проітеруйте усі зіставлені маршрути, якщо вони мають функцію load, викличте її.
 *
 * Це допоможе забезпечити завантаження асинхронного коду до рендеру.
 */
export function ensureReady(routeConfig, providedLocation) {
  const matches = matchRoutes(routeConfig, providedLocation || location.pathname);
  return Promise.all(matches.map((match) => {
    const { component } = match.route;
    if (component && component.load) {
      return component.load();
    }
    return undefined;
  }));
}
// rawensureReady.js

Збираємо все разом

Тепер, коли все готово, ми можемо візуалізувати наш застосунок. Ось як все виглядає разом (без визначення допоміжних функцій ensureReady, generateAsyncComponent і convertCustomRouteConfig).

import generateAsyncComponent from './generateAsyncComponent';
// Визначення одного маршруту
export default {
  path: (parentPath) => `${parentPath}/grand-child`,
  component: generateAsyncComponent(() => import('./Grandchild.jsx')),
  routes: [
    // стільки sub маршрутів, скільки потрібно.
  ]
};
// randchildRoute.js
import { renderRoutes } from 'react-router-config';
import { BrowserRouter } from 'react-router-dom';
import ReactDOM from 'react-dom';

import ensureReady from './ensureReady';
import routes from './routes';

// забезпечте готовність всіх маршрутів.
ensureReady(matchedRoutes).then(() => {
  if (typeof window !== 'undefined') {
    // логіка рендеру на серверній стороні
  } else {
    ReactDOM.render((
      <BrowserRouter>
        {renderRoutes(routes)}
      </BrowserRouter>
    ), document.getElementById('container'));
  }
});
// index.js
import grandchildRoute from './grandchildRoute';

const routes = [{ 
  component: Root,
  routes: [
    {
      path: '/',
      exact: true,
      component: Home,
    },
    {
      path: '/child/:id',
      component: Child,
      routes: [
        grandchildRoute,
      ],
    },
  ],
}];
// routes.js

Демо

У цьому пості багато коду і я подумав, що було б корисно побачити все в дії. Ознайомтеся з демонстраційним репозиторієм. Не соромтеся досліджувати код!

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація