В данной статье расскажу как настраивал 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