Серверный рендеринг на ReactJS

Настройка SSR для проектов написанных на ReactJS.

Опубликовал expert_m в 10:34, 17.06.2019

В данной статье расскажу как настраивал SSR на своих проектах. Конечно уже много статей написано на эту тему, но все равно тяжело пройти мимо 🙂

Теперь давайте начнем. Вот ссылка на github: https://github.com/expert-m/react-ssr-example. Там все исходники. Файлов много и весь код нет смысла сюда вставлять.

Файловая структура проекта:

react-ssr-example/
├── babel.config.js
├── package.json
├── src
│   ├── actions.js
│   ├── client
│   │   ├── components
│   │   │   └── AppClient.js
│   │   └── index.js
│   ├── components
│   │   ├── AppRoot.js
│   │   └── Home.js
│   ├── reducers.js
│   ├── routes.js
│   ├── server
│   │   ├── components
│   │   │   └── AppServer.js
│   │   ├── dev.tpl.html
│   │   ├── index.js
│   │   ├── prod.tpl.ejs
│   │   └── SSR.js
│   └── store.js
├── webpack.config.js
└── yarn.lock

В папке client хранится код для клиента, а в server соответственно для сервера. Папку server можете сразу скопировать в свой проект. Только нужно будет заменить шаблоны, ну и поправить код под себя 😉

В ./package.json добавляем скрипт для запуска сервера:

{
  ...
  "scripts": {
    "ssr": "node ./src/server/index.js",
    ...
  },
  ...
}

Теперь давайте заглянем в ./src/server/index.js. Там есть обработчик:

...

app.use('/', (req, res) => {
  try {
    const ssr = new SSR(req, res)
    ssr.render(req, res)
  } catch (error) {
    console.error(error)
    res.status(500).send('Something broke!')
  }
})

...

В этом обработчике вызывается ssr.render(), который как раз и будет генерировать HTML страницу.

Теперь рассмотрим класс SSR. Он содержит в себе почти весь код для серверного рендеринга:

class SSR {
  constructor(req, res) {
    this.req = req
    this.res = res
  }

  async render() {
    const context = {}
    const store = configureStore({})

    await this.fetchData({ store })

    const html = this.renderElement({ context, store })

    if (context.url) {
      const status = context.status || 301
      this.res.redirect(status, context.url)
      return
    }

    const status = context.status || 200
    this.res.status(status)

    this.res.render('./prod.tpl.ejs', {
      initialState: serialize(store.getState(), { isJSON: true }),
      app: html,
      assets,
    })
  }

  fetchData({ store }) {
    const [path, search] = this.req.url.split('?')
    const branch = matchRoutes(routes, path)

    const promises = branch.map(({ route, match }) => {
      const fetchData = route.component.fetchData

      if (fetchData instanceof Function) {
        return fetchData({
          dispatch: store.dispatch,
          match: match.params,
          location: {
            path,
            search: search || '',
          },
        })
      } else {
        return Promise.resolve(null)
      }
    })

    return Promise.all(promises)
  }

  renderElement({ context, store }) {
    const element = React.createElement(AppServer, {
      context,
      store,
      location: this.req.url,
    })

    return ReactDOMServer.renderToString(element)
  }
}

Метод ssr.fetchData() берет нужный компонент и вызывает у него метод fetchData(), т.е. fetchData() у компонента будет вызываться только на сервере при рендеринге. Компонент выбирается с помощью matchRoutes(). Эта функция принимает routes и path. path берем из this.req.url, а routes прописываем:

const routes = [
  {
    component: AppRoot,
    routes: [
      {
        path: '/',
        component: Home,
      },
    ],
  },
]

И вот пример компонента с fetchData():

class Home extends Component {
  static fetchData({ dispatch }) {
    return Promise.all([
      dispatch(UserList.get()),
    ])
  }

  ...
}

Метод fetchData() кроме dispatch может еще принимать match, location и должен возвращать промис. Обычно требуется делать несколько запросов и их можно передавать в Promise.all(). Про этот метод можно подробнее прочитать здесь:
https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Ну и сам ssr.render() из класса SSR вызывает this.fetchData(), рендерит элемент, получившийся HTML код подставляет в шаблон и отправляет страницу пользователю. К ответу добавляется статус (200, 301, 404, ...).

И ещё раз повторюсь. Код можно посмотреть здесь: https://github.com/expert-m/react-ssr-example