Реагировать на рендеринг на стороне сервера от новичка до профессионала

React.js

предисловие

Эта статья представляет собой мое собственное использование рендеринга на стороне сервера в процессе создания личного веб-сайта, просмотра некоторых руководств и преодоления некоторых ям. Я хочу поделиться этим процессом. Я постараюсь четко объяснить каждый шаг и рассказать все, что понимаю.

Пример кода в тексте взят из этогосклад, который также является личным веб-сайтом, который я создаю, чтобы вы могли поделиться им со всеми. Пример кода немного отличается от кода хранилища, потому что он упрощен. Кроме того, я также записал в README некоторые ямки, с которыми столкнулся в процессе использования рендеринга на стороне сервера.

Техники, использованные в этой статьеReact V16 | React-Router v4 | Redux | Redux-thunk | express

Реагировать на рендеринг на стороне сервера

Основная процедура рендеринга на стороне сервера заключается в создании строки HTML содержимого веб-страницы, которую мы хотим видеть на стороне сервера, когда пользователь запрашивает ее, и возврате ее в браузер для отображения. После того, как браузер получает HTML, он отображает страницу, но взаимодействия с событием нет. В это время браузер обнаруживает, что некоторые файлы js (то есть js, отображаемые на стороне браузера) загружаются в HTML, и загружает их напрямую. После загрузки и выполнения событие будет привязано. В это время страница занята браузером. То есть процесс рендеринга страниц с помощью js, с которым мы знакомы.

Цели, которые необходимо достичь:

  • Рендеринг компонентов React на стороне сервера
  • Рендеринг маршрутов на стороне сервера
  • Убедитесь, что данные на сервере и в браузере уникальны
  • Рендеринг css на стороне сервера (прямой стиль)

общий рендеринг

  • Рендеринг на стороне сервера: на стороне сервера генерируется строка html и отправляется в браузер для рендеринга.
  • Рендеринг на стороне браузера: сервер возвращает пустой html-файл, загружает внутри js и полностью завершает рендеринг страницы с помощью js.

Преимущества и недостатки

Рендеринг на стороне сервера устраняет недостатки, связанные с медленной загрузкой на первом экране и недружественным SEO (Google уже может извлекать веб-страницы, отображаемые в браузере, но не все поисковые системы могут). Но это увеличивает сложность проекта и увеличивает стоимость обслуживания.

Если нет необходимости, постарайтесь не использовать рендеринг на стороне сервера.

вся идея

Требуются две стороны: серверная, браузерная (часть, отображаемая браузером). Во-первых: упаковать код на стороне браузера Второе: упакуйте код сервера и запустите службу. Третье: при доступе пользователя сервер считывает упакованный файл index.html на стороне браузера как строку, вставляет визуализированные компоненты, стили и данные в строку html и возвращает ее в браузер. Четвертое: браузер напрямую отображает полученный html-контент и загружает упакованный js-файл на стороне браузера, выполняет привязку событий, инициализирует данные состояния и завершает изоморфизм.

Рендеринг компонентов React на стороне сервера

Давайте взглянем на простейший процесс рендеринга на стороне сервера React. Для выполнения рендеринга на стороне сервера необходим корневой компонент, отвечающий за генерацию HTML-структуры.

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.hydrate(<Container />, document.getElementById('root'));

Конечно, здесь тоже можно использовать ReactDOM.render, но гидрат будет пытаться повторно использовать полученный контент, возвращаемый сервером, для дополнения привязки событий и других процессов, специфичных для браузера.

Представьте корневой компонент, который необходимо отобразить на стороне браузера, используя реагирующиеrenderToStringAPI для рендеринга

import { renderToString } from 'react-dom/server'
import Container from '../containers'
// 产生html
const content = renderToString(<Container/>)
const html = `
    <html>
      <body>${content}</body>
    </html>
`
res.send(html)

Здесь также можно заменить renderToString на renderToNodeStream, разница в том, что первый генерирует HTML синхронно, то есть если на генерацию HTML уходит 1000 миллисекунд, Затем контент будет возвращен в браузер через 1000 миллисекунд, что, очевидно, занимает слишком много времени. Последний в виде потока, а результаты рендеринга запихиваются в объект ответа, то есть столько, сколько выходит. Сколько возвращается в браузер, что может относительно сократить время, затрачиваемое на

Рендеринг маршрутов на стороне сервера

В общих сценариях наше приложение не может иметь только одну страницу, и обязательно будут скачки маршрута. Обычно мы используем это:

import { BrowserRouter, Route } from 'react-router-dom'
const App = () => (
    <BrowserRouter>
        {/*...Routes*/}
    <BrowserRouter/>
)

Но именно так он используется при рендеринге на стороне браузера. При выполнении рендеринга на стороне сервера вам необходимо использоватьBrowserRouterзаменитьStaticRouterРазница в том, что BrowserRouter синхронизирует страницы с URL-адресами через API истории, предоставляемый HTML5, в то время как StaticRouter не изменит URL

import { createServer } from 'http'
import { StaticRouter } from 'react-router-dom'
createServer((req, res) => {
    const html = renderToString(
        <StaticRouter
            location={req.url}
            context={{}}
        >
            <Container />
        <StaticRouter/>)

})

Здесь StaticRouter нужно получить два свойства:

  • location: StaticRouter автоматически сопоставит соответствующий компонент React в соответствии с этим свойством, поэтому страница будет обновлена, а группа соответствующего маршрута, возвращаемая сервером, будет согласована с браузером.
  • контекст: обычно используется для передачи некоторых данных, эквивалентных несущей, которые будут использоваться, когда дело доходит до рендеринга стилей на стороне сервера.

Редуксный изоморфизм

Я думаю, что предварительная выборка данных, обезвоживание и впрыскивание воды — это трудности рендеринга на стороне сервера.

Что это значит? То есть веб-страница, отображаемая на первом экране, обычно требует внешних данных, и мы надеемся получить все данные, необходимые для этой страницы, до создания HTML. Затем вставьте его на страницу.Этот процесс, называемый «Обезвоживание», генерирует HTML и возвращает его в браузер. Браузер получает HTML с данными, Чтобы запросить js на стороне браузера, возьмите страницу и используйте эти данные для инициализации компонента. Этот процесс называется «гидратация». Завершите унификацию данных на стороне сервера и на стороне браузера.

Зачем ты это делаешь? Только представьте, если нет предварительной выборки данных, а напрямую возвращается HTML-структура без данных и только с фиксированным содержимым, что получится в результате?

Во-первых: поскольку на странице нет эффективной информации, это не способствует SEO.

Во-вторых: поскольку возвращенная страница не имеет содержимого, но JS на стороне браузера берет на себя управление страницей и возвращается к запросу данных и их отображению, страница какое-то время будет мигать, и взаимодействие с пользователем будет не очень хорошим.

Мы используем Redux для управления состоянием, потому что есть код на стороне сервера и код на стороне браузера, поэтому нам нужно два хранилища для управления данными на стороне сервера и на стороне браузера соответственно.

конфигурация компонента

Если компоненту необходимо запрашивать данные при рендеринге сервера, можно смонтировать метод для отправки асинхронных запросов на компонент, который здесь называется loadData, и получает в качестве параметра хранилище сервера. Затем store.dispatch, чтобы расширить магазин на сервере.

class Home extends React.Component {
    componentDidMount() {
        this.props.callApi()
    }
    render() {
        return <div>{this.props.state.name}</div>
    }
}
Home.loadData = store => {
  return store.dispatch(callApi())
}
const mapState = state => state
const mapDispatch = {callApi}
export default connect(mapState, mapDispatch)(Home)

Трансформация маршрутизации

Поскольку сервер маршрутизации определяет, какой компонент по текущему рендерингу асинхронного запроса может быть отправлен в это время. Поэтому вам также необходимо настроить маршрутизацию для поддержки метода loadData. Сервер при рендеринге, Маршрутизацию можно использовать для рендеринга react-router-config. Эта библиотека используется следующим образом (Mount loadData фокусируется на методе маршрутизации):

import { BrowserRouter } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import Home from './Home'
export const routes = [
  {
    path: '/',
    component: Home,
    loadData: Home.loadData,
    exact: true,
  }
]
const Routers = <BrowserRouter>
    {renderRoutes(routes)}
<BrowserRouter/>

Сервер получает данные

На стороне сервера необходимо определить, все ли компоненты в соответствующем маршруте имеют метод loadData, и вызвать их, если они есть. Пройдите в магазин на стороне сервера, чтобы расширить магазин на стороне сервера. * Также обратите внимание, что страница может состоять из нескольких компонентов, и * они будут отправлять свои собственные запросы, что означает, что мы должны дождаться отправки всех запросов, прежде чем вернуться в HTML.

import express from 'express'
import serverRender from './render'
import { matchRoutes } from 'react-router-config'
import { routes } from '../routes'
import serverStore from "../store/serverStore"

const app = express()
app.get('*', (req, res) => {
  const context = {css: []}
  const store = serverStore()
  // 用matchRoutes方法获取匹配到的路由对应的组件数组
  const matchedRoutes = matchRoutes(routes, req.path)
  const promises = []
  for (const item of matchedRoutes) {
    if (item.route.loadData) {
      const promise = new Promise((resolve, reject) => {
        item.route.loadData(store).then(resolve).catch(resolve)
      })
      promises.push(promise)
    }
  }
  // 所有请求响应完毕,将被HTML内容发送给浏览器
  Promise.all(promises).then(() => {
    // 将生成html内容的逻辑封装成了一个函数,接收req, store, context
    res.send(serverRender(req, store, context))
  })
})

Внимательные студенты могли заметить, что я обернул каждую loadData обещанием.

const promise = new Promise((resolve, reject) => {
  item.route.loadData(store).then(resolve).catch(resolve)
  console.log(item.route.loadData(store));
})
promises.push(promise)

Это для отказоустойчивости.Если в запросе есть ошибка, метод Promise.all ниже не будет выполнен, поэтому цель обертывания слоя промиса состоит в том, что даже если запрос завершится ошибкой, он будет разрешен, не затрагивая промис .все методы. То есть только компонент с запросом об ошибке не будет иметь данных, а другие компоненты не будут затронуты.

вводить данные

Наш запрос был отправлен, и хранилище на стороне сервера было расширено в методе loadData компонента, поэтому данные со стороны сервера можно извлечь и внедрить в HTML для возврата в браузер. Посмотрите на метод serverRender

const serverRender = (req, store, context) => {
  // 读取客户端生成的HTML
  const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <Container/>
      </StaticRouter>
    </Provider>
  )
  // 注入数据
  const initialState = `<script>
    window.context = {
      INITIAL_STATE: ${JSON.stringify(store.getState())}
    }
</script>`
  return template.replace('<!--app-->', content)
    .replace('<!--initial-state-->', initialState)
}

Сторона браузера инициализирует хранилище данными, полученными на стороне сервера.

После описанного выше процесса мы уже можем получить данные, предварительно извлеченные сервером из window.context, Что нам нужно сделать на данный момент, так это использовать эти данные для инициализации хранилища на стороне браузера. Обеспечьте унификацию данных на обоих концах.

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'

const defaultStore = window.context && window.context.INITIAL_STATE
const clientStore = createStore(
  rootReducer,
  defaultStore,// 利用服务端的数据初始化浏览器端的store
  compose(
    applyMiddleware(thunk),
    window.devToolsExtension ? window.devToolsExtension() : f=>f
  )
)

На данный момент проблема объединения данных рендеринга на стороне сервера решена.Давайте рассмотрим весь процесс:

  • Пользователь обращается к маршруту, и сервер сопоставляет массив компонентов в соответствующем маршруте в соответствии с маршрутом.
  • Зациклить массив, вызвать метод loadData, смонтированный на компоненте, отправить запрос и развернуть хранилище сервера
  • После выполнения всех запросов данные, предварительно полученные сервером, получаются через store.getState и вводятся в window.context.
  • Браузер отображает возвращенный HTML, загружает js на стороне браузера, извлекает данные из window.context для инициализации хранилища на стороне браузера и отображает компоненты.

Здесь есть еще один момент, то есть, когда мы заходим из маршрута на другие страницы, метод loadData в компоненте выполняться не будет, он будет выполняться только при обновлении маршрута и отрисовке маршрута сервером. На данный момент данных не будет. Так что нам также нужно отправить запрос в componentDidMount, чтобы решить эту проблему. Поскольку componentDidMount не будет выполняться на стороне сервера, Так что не беспокойтесь о повторной отправке запроса.

рендеринг стилей на стороне сервера

Выше мы сделали только то, чтобы содержимое веб-страницы отображалось сервером, но стиль будет добавлен только после того, как браузер загрузит css, поэтому содержимое веб-страницы, возвращенное в начале, не имеет стиля, и страница все еще будет мигать. Чтобы решить эту проблему, нам нужно сделать так, чтобы стиль также возвращался при рендеринге сервера.

Во-первых, при рендеринге сервера, парсинге css-файлов нельзя использовать style-loader, а следует использовать isomorphic-style-loader.

{
    test: /\.css$/,
    use: [
        'isomorphic-style-loader',
        'css-loader',
        'postcss-loader'
    ],
}

Однако как получить стиль компонента в текущем маршруте на стороне сервера? Напомним, что когда мы делаем рендеринг маршрутизации на стороне сервера, мы используем StaticRouter, который получает объект контекста, который можно использовать как носитель для передачи некоторой информации. Мы используем это!

Идея состоит в том, чтобы получить объект контекста в компоненте при рендеринге компонента, получить стиль компонента, поместить его в контекст, сервер получит стиль и вставить его в тег стиля в возвращаемом HTML.

Посмотрим, как компонент читает стили:

import style from './style/index.css'
class Index extends React.Component {
    componentWillMount() {
      if (this.props.staticContext) {
        const css = style._getCss()
        this.props.staticContext.css.push(css)
      }
    }
}

Компоненты в роуте могут получать staticContext в реквизитах, то есть контекст, переданный через StaticRouter, isomorphic-style-loader обеспечивает_getCss()метод, чтобы мы могли прочитать стиль css и поместить его в staticContext.不在路由之内的组件,可以通过父级组件,传递props的方法,或者用react-router的withRouter包裹一下

На самом деле, эту часть логики извлечения CSS можно написать как компонент более высокого порядка, чтобы его можно было использовать повторно.

import React, { Component } from 'react'

export default (DecoratedComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {
      if (this.props.staticContext) {
        const css = styles._getCss()
        this.props.staticContext.css.push(css)
      }
    }
    render() {
      return <DecoratedComponent {...this.props}/>
    }
  }
}

На стороне сервера, после отрисовки компонента, в контексте уже есть контент, в это время мы обрабатываем стиль и возвращаем его в браузер, чтобы стиль мог отрисовываться на стороне сервера.

const serverRender = (req, store) => {
  const context = {css: []}
  const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8')
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <Container/>
      </StaticRouter>
    </Provider>
  )
  // 经过渲染之后,context.css内已经有了样式
  const cssStr = context.css.length ? context.css.join('\n') : ''
  const initialState = `<script>
    window.context = {
      INITIAL_STATE: ${JSON.stringify(store.getState())}
    }
</script>`
  return template.replace('<!--app-->', content)
    .replace('server-render-css', cssStr)
    .replace('<!--initial-state-->', initialState)
}

На этом рендеринг на стороне сервера завершен.

Суммировать

Для рендеринга React на стороне сервера лучшим решением является Next.js. Если ваше приложение не нуждается в SEO-оптимизации или не уделяет особого внимания скорости рендеринга на первом экране, то старайтесь не использовать рендеринг на стороне сервера. Потому что это усложняет проект. Кроме того, помимо рендеринга на стороне сервера, существует множество методов SEO-оптимизации, например пререндеринг.