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