Реализация SSR на основе React.js и Node.js

JavaScript React.js

Основные понятия

  1. SSR: рендеринг на стороне сервера Традиционный рендеринг на стороне сервера может быть реализован с использованием таких языков разработки, как Java и php.Благодаря постоянному развитию Node.js и связанных с ним технологий внешнего интерфейса студенты, изучающие интерфейс, также могут выполнять независимый рендеринг на стороне сервера на основе этого.

  2. Процесс: браузер отправляет запрос -> сервер запускает код реакции для создания страницы -> сервер возвращает страницу -> браузер загружает HTML-документ -> страница готова То есть содержимое текущей страницы генерируется сервером и отправляется в браузер.

  1. Соответствующий CSR: рендеринг на стороне клиента Процесс: браузер отправляет запрос -> сервер возвращает пустой HTML (HTML содержит корневой узел и файл js) -> браузер загружает файл js -> браузер запускает код реакции -> страница готова То есть: содержимое текущей страницы рендерится с помощью js

  1. Как определить, отображается ли страница на стороне сервера: Щелкните правой кнопкой мыши -> показать исходный код веб-страницы, если содержимое страницы находится в HTML-документе, это рендеринг на стороне сервера, в противном случае — рендеринг на стороне клиента.

  2. В сравнении

  • CSR: время рендеринга первого экрана велико, а код реакции выполняется в браузере, что потребляет производительность браузера.
  • SSR: время рендеринга первого экрана короткое, код React выполняется на сервере, потребляет производительность сервера.

Зачем использовать рендеринг на стороне сервера

  • Время загрузки первого экрана оптимизировано, потому что SSR напрямую возвращает HTML, который генерирует контент, в то время как обычный CSR сначала возвращает пустой HTML, а затем браузер динамически загружает сценарий JavaScript и отображает страницу до того, как контент будет завершен; поэтому загружается первый экран SSR. Быстрее, меньше белого экрана, лучший пользовательский интерфейс.

  • SEO (поисковая оптимизация), ранжирование при поиске по ключевым словам для большинства поисковых систем, не распознает содержимое JavaScript, распознает только содержимое HTML. (Примечание: В принципе лучше не использовать рендеринг на стороне сервера, поэтому если есть только требования SEO, можно использовать пререндеринг и другие технологии для замены)

Создание проекта с серверным рендерингом

(1) Используйте Node.js в качестве промежуточного уровня между сервером и клиентом, используйте прокси-сервер, обрабатывайте файлы cookie и другие операции.

(2) Использование гидрата: в случае рендеринга на стороне сервера вместо рендеринга используется гидрат, и его функция в основном состоит в том, чтобы вводить связанные события в HTML-страницу (т. е. позволить данным компонента React передаваться в браузера вместе с HTML-документом) серверная страница), что может обеспечить согласованность данных на стороне сервера с данными на стороне браузера, избежать заставок на экране и сделать первую загрузку более эффективной и плавной.

 ReactDom.hydrate(<App />, document.getElementById('root'));

(3) Компиляция кода сервера WebPack: обычно создается файл WebPack.Server.js.В дополнение к обычной конфигурации параметров необходимо установить целевой параметр «Узел».


const serverConfig = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, '../dist')
  },
  externals: [nodeExternals()],
  module: {
    rules: [{
      test: /\.js?$/,
      loader: 'babel-loader',
      exclude: [
        path.join(__dirname, './node_modules')
      ]
    }
    ...
   ]
  }
  (此处省略样式打包,代码压缩,运行坏境配置等等...)
  ...
};

(4) Используйте метод renderToString в react-dom/server для преобразования различных сложных компонентов и кодов в строки HTML на сервере и возврата их в браузер, а также для отправки тегов при первоначальном запросе, чтобы ускорить загрузку страницы и разрешить сканирование поисковых систем. страницы для целей SEO.

const render = (store, routes, req, context) => {
  const content = renderToString((
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          {renderRoutes(routes)}
        </div>
      </StaticRouter>
    </Provider>
  ));
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id='root'>${content}</div>
        <script src='/index.js'></script>
      </body>
    </html>
  `;
}
app.get('*', function (req, res) {
  ...
  const html = render(store, routes, req, context);
  res.send(html);
});
      

Аналогичные функции для renderToString: I. renderToStaticMarkup: разница в том, что renderToStaticMarkup отображает чистый HTML без data-reactid.После загрузки JavaScript он повторно отображается, потому что он не распознает содержимое, отображаемое сервером ранее (возможно, страница начнет мигать).

ii. renderToNodeStream: визуализирует элемент React в исходный HTML, возвращая читаемый поток выходных строк HTML.

3. renderToStaticNodeStream: аналогично renderToNodeStream, за исключением того, что не создает дополнительных свойств DOM, которые React использует внутри, например, data-reactroot.

(5) Используйте redux для выполнения обязанностей по подготовке данных и поддержанию состояния, обычно с react-redux, redux-thunk (промежуточное ПО: отправка асинхронных запросов в действие). (В настоящее время эта обезьяна в основном использует Redux и Mobx, а Redux используется здесь в качестве примера). A. Создайте хранилище (серверу необходимо создать его один раз для каждого запроса, а клиент создает его только один раз):

const reducer = combineReducers({
  home: homeReducer,
  page1: page1Reducer,
  page2: page2Reducer
});

export const getStore = (req) => {
  return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}

export const getClientStore = () => {
  return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

B. действие: отвечает за передачу данных из приложения в магазин и является единственным источником данных магазина.

export const getData = () => {
  return (dispatch, getState, axiosInstance) => {
    return axiosInstance.get('interfaceUrl/xxx')
      .then((res) => {
        dispatch({
          type: 'HOME_LIST',
          list: res.list
        })
      });
  }
}
      

C. редуктор: получает старое состояние и действия, возвращает новое состояние, отвечает на действия и отправляет их в хранилище.

export default (state = { list: [] }, action) => {
  switch(action.type) {
    case 'HOME_LIST':
      return {
        ...state,
        list: action.list
      }
    default:
      return state;
  }
}
export default (state = { list: [] }, action) => {
  switch(action.type) {
    case 'HOME_LIST':
      return {
        ...state,
        list: action.list
      }
    default:
      return state;
  }
}     

D. Используя соединение react-redux, поставщик подключает компонент к хранилищу.

  • Провайдер передает ранее созданный магазин в качестве реквизита Провайдеру
const content = renderToString((
  <Provider store={store}>
    <StaticRouter location={req.path} context={context}>
      <div>
        {renderRoutes(routes)}
      </div>
    </StaticRouter>
  </Provider>
));     
  • connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) получает четыре параметра Первые два свойства обычно используются Функция mapStateToProps позволяет нам привязать данные в хранилище в качестве реквизита к компоненту. mapDispatchToProps привязывает действие как реквизит к компоненту.
 connect(mapStateToProps(),mapDispatchToProps())(MyComponent)

(6) Используйте react-router, чтобы взять на себя ответственность за маршрутизацию В отличие от маршрутизации на стороне клиента, маршрутизация на стороне сервера не имеет состояния. React предоставляет компонент без сохранения состояния StaticRouter, передает текущий URL-адрес StaticRouter и вызывает ReactDOMServer.renderToString() для соответствия представлению маршрута.

Сервер

import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'

<StaticRouter location={req.path} context={{context}}>
{renderRoutes(routes)}
</StaticRouter>     

сторона браузера

import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'

<BrowserRouter>
  {renderRoutes(routes)}
</BrowserRouter>

При изменении адресной строки браузера внешний интерфейс будет соответствовать представлению маршрутизации, в то же время из-за изменения req.path сервер будет соответствовать представлению маршрутизации, что поддерживает согласованность внешнего интерфейса и внутренние представления маршрутизации. Когда страница обновляется, все еще возможно Отображать текущее представление в обычном режиме. Если используется только маршрутизация на стороне браузера и используется BrowserRouter, то при обновлении страницы после изменения адреса страницы страница не будет найдена, поскольку нет соответствующего HTML. Верните клиенту полный HTML-код, и страница все равно будет отображаться. как обычно. Рекомендуется использовать подключаемый модуль react-router-config, а затем добавить renderRoutes (маршруты) в дочерние элементы тегов StaticRouter и BrowserRouter, как указано выше: Создайте файл router.js

const routes = [{ component: Root,
  routes: [
    { path: '/',
      exact: true,
      component: Home,
      loadData: Home.loadData
    },
    { path: '/child/:id',
      component: Child,
      loadData: Child.loadData
      routes: [
        path: '/child/:id/grand-child',
        component: GrandChild,
        loadData: GrandChild.loadData
      ]
    }
  ]
}];

Когда браузер запрашивает адрес, server.js может использовать matchRouters для определения контента, который должен быть отрисован до фактического рендеринга, вызвать функцию loaderData для отправки действия, вернуть promise->promiseAll->renderToString и, наконец, сгенерировать возврат HTML-документа.

import { matchRoutes } from 'react-router-config'
  const loadBranchData = (location) => {
    const branch = matchRoutes(routes, location.pathname)

    const promises = branch.map(({ route, match }) => {
      return route.loadData
        ? route.loadData(match)
        : Promise.resolve(null)
    })

    return Promise.all(promises)
}

(7) При написании компонентов обратите внимание на изоморфизм кода (т. е. набор кода React выполняется один раз на стороне сервера и один раз на стороне клиента). Поскольку событие привязки на стороне сервера недопустимо, сервер возвращает только стиль страницы (и данные, заполненные водой) и возвращает файл JavaScript. Событие может быть связано только тогда, когда JavaScript загружается и выполняется в браузере, и мы надеюсь, что этот процесс только вам нужно написать код один раз, в это время будет использоваться изоморфизм, сервер будет отображать стиль, и событие будет привязано, когда клиент его выполнит.

Преимущества: общий интерфейсный код, экономия времени разработки. Недостатки: из-за разницы между серверной и браузерной средами это может привести к некоторым проблемам, таким как невозможность найти документ и другие объекты, ошибка расчета DOM, внешний рендеринг и рендеринг содержимого на стороне сервера несовместимы и т. д. .; внешний интерфейс может выполнять очень сложное слияние запросов и отложенную обработку, но для изоморфизма все эти запросы рендерятся только при предварительной выборке результата.

об авторе

Дора, главный инженер-разработчик Tongbanjie, присоединилась к команде в августе 2015 года и в настоящее время в основном отвечает за разработку проектов на стороне приложений на стороне эксплуатации.

В этой статье в основном описывается, как реализовать SSR на основе React.js и Node.js в конкретных случаях. Чтобы получить больше информации, связанной с React, отсканируйте код, чтобы подписаться на общедоступную учетную запись WeChat «Tongbanjie Technology», и ответьте «React» в фон или «реагировать-нативный» для большего количества отличного контента.