https://habrahabr.ru/post/349064/- Разработка веб-сайтов
- Веб-дизайн
- ReactJS
- Node.JS
- JavaScript
В
прошлой статье рассматривалась библиотека Next.js, которая позволяет разрабатывать универсальные приложения «из коробки». В обсуждении статьи были озвучены существенные недостатки этой библиотеки. Судя по тому, что
https://github.com/zeit/next.js/issues/88 бурно обсуждается с октября 2016 года, решения проблемы в ближайшее время не будет.
Поэтому, предлагаю ознакомится с современным состоянием «экосистемы» React.js, т.к. на сегодняшний день все, что делает Next.js, и даже больше, можно сделать при помощи сравнительно простых приемов. Есть, конечно, и готовые заготовки проектов. Например, мне очень нравится
проект, который, к сожалению, базируется на неактульной версии роутера. И очень актуальный, хотя не такой «заслуженный»
проект.
Использовать готовые проекты с массой плохо документированных возможностей немного страшно, т.к. не знаешь, где споткнешься, и самое главное — как развивать проект. Поэтому для тех, кто хочет разобраться в современном состоянии вопроса (и для себя), я сделал заготовку проекта с разъяснениями. В ней не будет какого-то моего личного эксклюзивного кода. Просто компиляция из примеров документации и большого количества статей.
В
прошлой статье были перечисены задачи которые должно решать универсальное приложение.
1. Асинхронная предзагрузка данных на сервере (React.js как и большинство подобных бибиотек реализует только синхронный рендеринг)и формирование состояния компонента.
2. Серверный рендеринг компонента.
3. Передача состояния компонента на клиент.
4. Воссоздание компонента на клиенте с состоянием, переданным с сервера.
5. «Присоединение» компонента (hydrarte(...)) к полученной с сервера разметке (аналог render(...)).
6. Разбиение кода на оптимальное количество фрагментов (code splitting).
И, конечно, в коде серверной части и клиентской части фронтенда приложения не должно быть различий. Один и тот же компонент должен работать одинаково и при серверном и при клиентском рендеринге.
Начнем с роутинга. В документации React для реализации универсального роутинга предлагается формировать роуты на основании простого объекта. Например так:
// routes.js
module.exports = [
{
path: '/',
exact: true,
// component: Home,
componentName: 'home'
}, {
path: '/users',
exact: true,
// component: UsersList,
componentName: 'components/usersList',
}, {
path: '/users/:id',
exact: true,
// component: User,
componentName: 'components/user',
},
];
Такай форма описания роутов позволяет:
1) сформировать серверный и клиентский роутер на основании единого источника;
2) на сервере сделать предзагрузку данных до создания экземпляра компонента;
3) организовать разбиение кода на оптимальное количество фрагментов (code splitting).
Код серверного роутера очень простой:
import React from 'react';
import { Switch, Route } from 'react-router';
import routes from './routes';
import Layout from './components/layout'
export default (data) => (
<Layout>
<Switch>
{
routes.map(props => {
props.component = require('./' + props.componentName);
if (props.component.default) {
props.component = props.component.default;
}
return <Route key={ props.path } {...props}/>
})
}
</Switch>
</Layout>
);
Отсутсвие возможности использовать полноценный общий
<Layout/>
в Next.js как раз и послужило отправной точкой для написания этой статьи.
Код клиентского роутера немного сложнее:
import React from 'react';
import { Router, Route, Switch} from 'react-router';
import routes from './routes';
import Loadable from 'react-loadable';
import Layout from './components/layout';
export default (data) => (
<Layout>
<Switch>
{
routes.map(props => {
props.component = Loadable({
loader: () => import('./' + props.componentName),
loading: () => null,
delay: () => 0,
timeout: 10000,
});
return <Route key={ props.path } {...props}/>;
})
}
</Switch>
</Layout>
);
Самая интересная часть заключается в фрагменте кода
() => import('./' + props.componentName)
. Функция import() дает команду webpack для реализации code splitting. Если бы на странице была обычная конструкция import или require(), то webpack включил бы код компонента в один результирующий файл. А так код будет загружаться при переходе на роут из отдельного фрагмента кода.
Рассмотрим основную точку входа клиентской части фронтенда:
'use strict'
import React from 'react';
import { hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import {BrowserRouter} from 'react-router-dom';
import Layout from './react/components/layout';
import AppRouter from './react/clientRouter';
import routes from './react/routes';
import createStore from './redux/store';
const preloadedState = window.__PRELOADED_STATE__;
delete window.__PRELOADED_STATE__;
const store = createStore(preloadedState);
const component = hydrate(
<Provider store={store}>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</Provider>,
document.getElementById('app')
);
Все достаточно обычно и описано в документации React. Воссоздается состояние компонента с сервера и компонент «присоединяется» к готовой разметке. Обращаю внимание, что не все библиотеки позволяют сделать такую операцию в одной строчке кода, как это можно сделать в React.js.
И тот же компонент в серверном варианте:
import { matchPath } from 'react-router-dom';
import routes from './react/routes';
import AppRouter from './react/serverRouter';
import stats from '../dist/stats.generated';
...
app.use('/', async function(req, res, next) {
const store = createStore();
const promises = [];
const componentNames = [];
routes.forEach(route => {
const match = matchPath(req.path, route);
if (match) {
let component = require('./react/' + route.componentName);
if (component.default) {
component = component.default;
}
componentNames.push(route.componentName);
if (typeof component.getInitialProps == 'function') {
promises.push(component.getInitialProps({req, res, next, match, store}));
}
}
return match;
})
Promise.all(promises).then(data => {
const context = {data};
const html = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<AppRouter/>
</StaticRouter>
</Provider>
);
if (context.url) {
res.writeHead(301, {
Location: context.url
})
res.end()
} else {
res.write(`
<!doctype html>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(store.getState()).replace(/</g, '\\u003c')}
</script>
<div id="app">${html}</div>
<script src='${assets(stats.common)}'></script>
${componentNames.map(componentName =>
`<script src='${assets(stats[componentName])}'></script>`
)}
`)
res.end()
}
})
});
Наиболее значимая часть — это определение по роуту необходимого компонента:
routes.forEach(route => {
const match = matchPath(req.path, route);
if (match) {
let component = require('./react/' + route.componentName);
if (component.default) {
component = component.default;
}
componentNames.push(route.componentName);
if (typeof component.getInitialProps == 'function') {
promises.push(component.getInitialProps({req, res, next, match, store}));
}
}
return match;
})
После того как мы находим компонент, мы вызываем его асинхронеый статический метод
component.getInitialProps({req, res, next, match, store})
. Статический — потому что экземпляр компонента на сервере еще не создан. Этот метод назван по аналогии с Next.js. Вот как этот метод может выглядеть в компоненте:
class Home extends React.PureComponent {
static async getInitialProps({ req, match, store, dispatch }) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
const action = userActions.login({name: 'John', userAgent});
if (req) {
await store.dispatch(action);
} else {
dispatch(action);
}
return;
}
Для хранения состояния объекта исползуется redux, что в данном случае существенно облегчает доступ к состоянию на сервере Без redux это было бы сделать не просто сложно а очень сложно.
Для удобства разработки нужно обеспечить компиляцию клиентского и серверного кода компонентов «на лету» и обновление браузера. Об этом а также о конфигурациях webpack для работы проекта я планирую рассказатьв следующей статье.
apapacy@gmail.com
14 февраля 2018 года