Я видел, как некоторые люди изо всех сил пытались создать универсальное (ранее изоморфное) приложение React. Либо из-за сложности, которая может быть связана с процессом, либо из-за более страшных стартовых наборов, шаблонов и тому подобного.

Не борись больше! Я здесь, чтобы помочь вам начать!

Прежде чем мы начнем

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

Одним из них является потрясающий Next.js — минималистичный фреймворк для серверных приложений React. Обязательно посмотрите.

Введение

В примере этой статьи мы будем использовать следующие зависимости:

Зависимости

npm install -s babel-register babel-preset-es2015 babel-preset-react react react-dom [email protected] express

Зависимости разработки

npm install -d babel-loader webpack nodemon

Структура папок

Структура нашего приложения будет следующей:

/client
  /components
  /containers
    /App.js
    /Homepage.js
    /About.js
  /index.js
  /routes.js
/server
  /index.js
  /server.js
  /universalRender.js
/static
/.babelrc
/package.json
/webpack.config.js

Это довольно просто!

На /client мы храним все, что связано (как вы уже догадались) с клиентской частью. /client/components будет содержать все повторно используемые компоненты, /client/containers будет содержать все компоненты, не подлежащие повторному использованию (представьте, что это похоже на страницы нашего приложения).

На стороне сервера у нас будет три файла (для целей нашей миссии). /server/index.js — наш основной файл, отвечающий за вызов babel-register и запуск сервера. /server/server.js будет содержать код самого сервера (экспресс и т. д.), а /server/universalRender.js — место, где происходит волшебство в отношении рендеринга на стороне сервера.

Конфигурация

Теперь убедитесь, что ваши .babelrc , package.json и webpack.config.js выглядят следующим образом:

.babelrc

{
  "presets": [
    "es2015",
    "react"
  ]
}

пакет.json

{
  "main": "server/index.js",
  "scripts": {
    "start": "NODE_ENV=production node server/index.js",
    "build": "webpack",
    "dev": "webpack -w & nodemon server/index.js"
  },
  "devDependencies": {
    "babel-loader": "^6.4.1",
    "nodemon": "^1.11.0",
    "webpack": "^2.3.2"
  },
  "dependencies": {
    "babel-preset-es2015": "^6.24.0",
    "babel-preset-react": "^6.23.0",
    "babel-register": "^6.24.0",
    "express": "^4.15.2",
    "react": "^15.4.2",
    "react-dom": "^15.4.2",
    "react-router-dom": "^4.0.0"
  }
}

webpack.config.js

const path = require('path');
module.exports = {
  entry: {
    bundle: [
      __dirname + '/client/index.js'
    ]
  },
  output: {
    path: __dirname + '/static/js',
    filename: '[name].js'
  },
  devtool: 'source-map',
  resolve: {
    extensions: ['.js', '.json']
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  }
};

Создание наших компонентов

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

App.js

Этот компонент является основным компонентом приложения, который будет обертывать все остальные (например, компонент макета или что-то подобное).

// client/containers/App.js
import React from 'react';
export default class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Universal React App</h1>
        <hr />
        <ul>
          <li><Link to='/'>Home</Link></li>
          <li><Link to='/about'>About</Link></li>
        </ul>
        <hr />
        <div>
          {this.props.children}
        </div>
      </div>
    );
  }
}

Homepage.js и About.js

Эти компоненты являются нашими главными страницами… Домашняя страница:

// client/containers/Homepage.js
import React from 'react';
export default class Homepage extends React.Component {
  render() {
    return (
      <div>
        <h2>Homepage!</h2>
      </div>
    );
  }
}

И страница «О нас»:

// client/containers/About.js
import React from 'react';
export default class About extends React.Component {
  render() {
    return (
      <div>
        <h2>About!</h2>
      </div>
    );
  }
}

Ничего особенного!

Создание маршрутов

Как упоминалось ранее, в этом примере мы используем react-router@4. Обратите внимание, что в новой версии пакет разделен на разные компоненты. react-router содержит основные компоненты пакета, а также другие для различных функций (собственные, DOM). В нашем случае мы будем использовать react-router-dom — пакет, который экспортирует компоненты, поддерживающие DOM (те, которые используются в веб-приложениях). Вместо того, чтобы использовать оба пакета, мы можем придерживаться react-router-dom, так как он также экспортирует основные компоненты, которые нам понадобятся.

Файл маршрутов:

// client/routes.js
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { App, About, Homepage } from './containers';
export default (
  <App>
    <Switch>
      <Route exact path='/' component={ Homepage } />
      <Route path='/about' component={ About } />
    </Switch>
  </App>
);

Если вы еще не знакомы с новой версией React Router, рекомендую посетить документацию, так как в этой статье я не буду вдаваться в подробности.

Маршруты определены в отдельном файле не только для простоты управления маршрутами во время разработки, но и, в данном конкретном случае, чтобы позволить использовать одни и те же маршруты как в клиентском, так и в серверном рендеринге, без необходимости их повторного объявления дважды.

Рендеринг на стороне клиента

Рендеринг приложения React на стороне клиента — это то, что вы, вероятно, уже делали раньше.

Чтобы отобразить приложение на стороне клиента, нам нужно сделать следующее:

// client/index.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from './routes';
render(
  <BrowserRouter>
    {routes}
  </BrowserRouter>,
  document.getElementById('root')
);

Как видите, это не какая-то непонятная вещь (как и рендеринг на стороне сервера). Мы используем react-dom для рендеринга компонента и присоединения к некоторому элементу с идентификатором root. Отрисовываемый компонент — это конкретный Router для клиентской стороны. С react-router@4 это BrowserRouter (или HashRouter, в зависимости от того, какого поведения вы хотите добиться). Внутри вам нужно только пройти ранее определенные маршруты.

Переход на сторону сервера

Прежде всего, мы просто реализуем babel, чтобы иметь возможность преобразовывать код React.

// server/index.js
require('babel-register')();
require('./server');

Как вы хорошо заметили, мы не даем никакой конфигурации babel-register, потому что мы уже указали ее в /.babelrc, чтобы мы могли использовать конфигурацию как в babel-register, так и в babel-loader (Webpack).

Теперь нам нужно создать какой-нибудь сервер. Для этого воспользуемся Express:

// server/server.js
import path from 'path';
import express from 'express';
import universalRender from './universalRender';
// If running in production, PORT env var must be specified
const PORT = process.env.NODE_ENV === 'production' ? process.env.PORT : 3000;
let app = express();
// Make public assets available
app.use('/static', express.static(path.join(__dirname, '../static')));
// Server-Side rendering
app.use(universalRender);
// Start app
app.listen(PORT, err => {
  if (err) {
    throw err;
  }
  console.log('> Ready on http://localhost:%s', PORT);
});

Это всего лишь простой сервер, созданный с помощью Express. Единственное, что здесь особое, — это использование промежуточного программного обеспечения universalRender, которое будет отображать наше приложение на стороне сервера.

Итак, наконец, мы должны определить это промежуточное ПО:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import routes from '../client/routes';
const renderPage = html => {
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Universal React App</title>
        <link rel="shortcut icon" href="/favicon.ico" />
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/static/js/bundle.js"></script>
      </body>
    </html>
  `;
};
// Server Side Rendering based on routes matched by React Router.
const universalRender = (req, res, next) => {
  const context = {};
  const initialView = renderToString(
    <StaticRouter location={ req.url } context={ context }>
      {routes}
    </StaticRouter>
  );
  if (context.url) {
    return res.redirect(context.url);
  }
  return res.status(200).send(renderPage(initialView));
};
export default universalRender;

По сути, мы объявляем функцию для рендеринга страницы и самого промежуточного программного обеспечения. Функция renderPage(html) получает визуализированный компонент, визуализированный методом react-dom/server renderToString() (аналогично ранее использовавшемуся render() на стороне клиента). Мы передаем ему Router, который в данном случае равен StaticRouter, и внутри вы размещаете маршруты, определенные в /client/routes.js.

Наконец, мы отправляем весь HTML-код клиенту, и у него будет красивое приложение React, отрендеренное сервером, ожидая, когда клиент вступит во владение.

Тестирование

Сначала запустите сервер, введя npm run dev.

Чтобы убедиться, что все работает правильно (приложение отображается как на стороне клиента, так и на стороне сервера), мы можем протестировать его следующим образом:

На стороне сервера

Сделайте запрос с помощью такого инструмента, как cURL, и он должен вернуть всю страницу, например:

curl http://localhost:3000 # should return homepage content
curl http://localhost:3000/about # should return about page content

На стороне клиента

В браузере перейдите на http://localhost:3000 (должна отобразиться домашняя страница), а оттуда попробуйте перейти на страницу "О странице", используя ссылки на странице (должна отобразиться страница "О программе").

Вывод

Как видите, создать универсальное приложение React не так уж и сложно. Хотя в этой статье не рассматриваются некоторые другие важные вещи, такие как использование Webpack Dev Server для упрощения разработки, в ней рассматриваются основы, которые должны позволить вам быстро приступить к работе и совершенствоваться.

Надеюсь, это помогло вам как-то!

Если у вас есть какие-либо вопросы, предложения или вы просто хотите поболтать, не стесняйтесь делать это ниже. Я ценю любую обратную связь!