Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
React.js – замечательный фреймворк для фронтенд-разработки. У него удобный интерфейс и большие возможности, которые лучше всего реализуются при создании одностраничных приложений. Сегодня мы запилим небольшое Single Page Application и параллельно разберёмся в некоторых принципах React.
MPA vs SPA
Если в 2020 году вы ещё не знаете, чем SPA отличается от обычных многостраничных сайтов, пора исправить недоразумение. Если знаете – пролистайте раздел и переходите к практике.
MPA. Возьмём среднестатистический сайт, например, воображаемый интернет-магазин плюшевых игрушек MimimiShop. Вы заходите на главную страницу mimimi-shop.ru и видите много-много карточек с мягкими мишками. Чтобы вы смогли их увидеть, браузер послал запрос на сервер и получил файл index.php
.
Один мишка очень вам нравится, и вы по нему кликаете. Ссылка меняется на mimimi-shop.ru/teddy-bears/92, страница перезагружается, браузер получает другой файл – product.php
. Здесь совершенно другая информация – мишка только один, зато подробно описан.
В классическом Multi Page Application каждый url – это отдельный запрос к серверу с получением нового шаблона.
SPA. Перепишем MimimiShop как SPA. Вы заходите на главную страницу mimimi-shop.ru, и браузер загружает всего один файл index.html
и большой скрипт mimimi.spa.js
. После загрузки скрипт выводит на страницу компонент каталога – множество карточек с мягкими игрушками. Когда вы кликаете на понравившегося мишку, компонент каталога реактивно заменяется на компонент одного товара. Страница при этом не перезагружается, всё происходит красиво и плавно.
В итоге браузер отдал только одну страницу – index.html
.
Сложно ли сделать SPA?
Несложно. Если вы используете инструменты из экосистемы React, то большая часть рутинной работы уже сделана за вас. Сосредоточьтесь на логике приложения, а скучное обновление DOM-дерева оставьте библиотеке.
Что нужно знать? Чтобы разобраться в React, достаточно знать на базовом уровне JavaScript версии ES6. Если неуверены в скиллах, то чекните их в нашей статье Больше JS, чем React: как фреймворк использует возможности языка.
Основы работы React
DOM. Загружаясь на страницу, скрипт берёт под контроль DOM-элемент, который вы ему предоставили. Теперь внутри этого элемента безраздельно властвует React. Он может отслеживать действия пользователя и менять дерево элементов на любом уровне. Для этого используется быстрый виртуальный DOM. В этот элемент может выводиться любой компонент (или дерево компонентов) в зависимости от текущего состояния приложения. Могут меняться какие-то мелочи или даже весь контент целиком.
JSX. Привычной MVC-модели в React нет, так как в компоненте объединяется и представление (рендер), и логика. Это значит, что в компоненте, например, селекта, вы определяете и его внешний вид, и поведение. Для удобства используется специфический JSX-синтаксис (JavaScript XML). Он похож на HTML, но на самом деле это чистой воды JavaScript. Убедиться в этом и поиграться вы можете в онлайн-компиляторе Babel.

Мы не будем подробно разбираться в тонкостях JSX. Если вы с ним незнакомы, то загляните сначала в Подробное руководство по JSX в React, а потом продолжайте чтение.
Запуск проекта
Настройка webpack. Настраивать всю среду разработки с чистого листа было бы долго и скучно. Воспользуемся готовым решением – утилитой create-react-app. Она подготовит начальную структуру проекта, скачает необходимые зависимости и настроит webpack. Если вы плохо знакомы с принципами сборки проекта, загляните во вводное руководство по webpack (и перестаньте уже его бояться).
Откройте рабочую директорию, запустите терминал – начинаем работать.
Устанавливаем утилиту:
npm i create-react-app
Создаём проект:
npx create-react-app react-spa
// или
npm init react-app react-spa
В директории появилась новая папка react-spa
. Заходим в папку, запускаем команду:
npm start
Запустится сервер для локальной разработки, и в браузере откроется новая вкладка с текущим проектом.
Исходники. Все интересующие исходники лежат в папке src
:
src/index.js
– входная точка сборки. Здесь указывается корневой DOM-элемент, в который помещается главный компонентApp
, представляющий собой наше React-приложение.src/App.js
– это собственно код компонентаApp
.
А другие файлы? Помимо этих файлов, в папке src
лежат стили, тесты, несколько служебных скриптов и svg-файл логотипа – всё это нам сейчас неважно. Основные боевые действия будут разворачиваться внутри App.js
.
В документации React подробно описано, что такое компонент и как с ним работать. Нам же сейчас достаточно понимать, что компонент – просто функция, которая принимает извне некоторые свойства (props
), а возвращает разметку для рендера в формате JSX.
Первая страница
Наше SPA будет представлять собой портфолио React-разработчика.
Откройте файл src/App.js
, удалите всё ненужное, и разместите следующий код:
import React from 'react';
import './App.css';
import works from './works';
function App() {
return (
<div className='app'>
<header className='header'>
<div className='container'>
<div className='header-brand'>Иван Иванов</div>
</div>
</header>
<main className='main'>
<div className='about'>
<div className='about__bg'></div>
<div className='container'>
<h1 className='about__title'>
React-разработчик Иван Иванов
</h1>
<div className='about__description'>
<p>
Разрабатываю на самом крутом в мире фреймворке
<br />
самые крутые в мире SPA!
</p>
<p>
С удовольствием и вам что-нибудь разработаю ;)
</p>
</div>
</div>
</div>
<div className='portfolio'>
<div className='container'>
{works.map((work, index) => (
<a
href={work.link}
className='portfolio-item'
key={index}
>
<img
className='portfolio-item__screenshot'
src={work.screenshot}
alt={work.title}
/>
<div className='portfolio-item__title'>
{work.title}
</div>
</a>
))}
</div>
</div>
</main>
</div>
);
}
export default App;
Стили можете написать свои или взять готовые:
.app .container {
width: 1000px;
padding: 0 20px;
margin: 0 auto;
}
.header {
height: 60px;
background-color: darkcyan;
color: white;
}
.header .container {
height: 100%;
display: flex;
align-items: center;
}
.header-brand {
font-size: 24px;
font-weight: bold;
}
.about {
position: relative;
padding: 100px 0;
text-align: center;
font-weight: bold;
color: white;
}
.about__title {
font-size: 50px;
margin-bottom: 30px;
}
.about__description p {
margin: 20px 0;
font-size: 24px;
}
.about__bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
background-image: url(./img/react.jpeg);
background-position: center;
background-size: cover;
background-attachment: fixed;
filter: brightness(0.3);
}
.portfolio {
padding: 40px 0;
}
.portfolio .container {
display: flex;
flex-wrap: wrap;
}
.portfolio-item {
box-sizing: border-box;
flex-basis: 33.33%;
padding: 10px;
text-decoration: none;
color: inherit;
}
.portfolio-item {
display: block;
}
.portfolio-item img {
max-width: 100%;
}
.portfolio-item__title {
margin: 10px 0;
text-align: center;
font-size: 16px;
font-weight: bold;
}
Используя JSX-синтаксис и удобные фичи типа циклов, мы создаём разметку простой страницы с шапкой, блоком приветствия и списком выполненных проектов.
На что обратить внимание:
- Вместо атрибута
class
используетсяclassName
; - Все теги, включая
img
, должны быть закрыты; - Для вывода массива элементов используется метод
map
; - Каждый элемент массива, создаваемого методом
map
, должен иметь уникальный ключkey
.
Структура проекта и компоненты
Заботимся о структуре. Если продолжить писать всё в одном файле, SPA очень быстро разрастётся, и мы запутаемся в коде. Нужно разделить проект на отдельные компоненты.
Сейчас у нас наметились несколько самостоятельных блоков:
Header
– шапка с брендом;About
– блок с приветствием;PortfolioItem
– карточка одного проекта.
Создадим директорию src/components
, а внутри – отдельные папки для каждого компонента. Код и стили блоком переносим практически без изменений:
import React from 'react';
import './Header.css';
function Header(props) {
return (
<header className='header'>
<div className='container'>
<div className='header-brand'>{props.brand}</div>
</div>
</header>
);
}
export default Header;
import React from 'react';
import './About.css';
function About(props) {
return (
<div className='about'>
<div className='about__bg'></div>
<div className='container'>
<h1 className='about__title'>{props.title}</h1>
<div className='about__description'>{props.children}</div>
</div>
</div>
);
}
export default About;
import React from 'react';
import './PortfolioItem.css';
function PortfolioItem({ work }) {
return (
<a href={work.link} className='portfolio-item'>
<img
className='portfolio-item__screenshot'
src={work.screenshot}
alt={work.title}
/>
<div className='portfolio-item__title'>{work.title}</div>
</a>
);
}
export default PortfolioItem;
Подключаем компоненты внутри App:
function App() {
return (
<div className='app'>
<Header brand='Иван Иванов'></Header>
<main className='main'>
<About title='React-разработчик Иван Иванов'>
<p>
Разрабатываю на самом крутом в мире фреймворке
<br />
самые крутые в мире SPA!
</p>
<p>С удовольствием и вам что-нибудь разработаю ;)</p>
</About>
<div className='portfolio'>
<div className='container'>
{works.map(work => (
<PortfolioItem key={work.id} work={work} />
))}
</div>
</div>
</main>
</div>
);
}
Разметка. Каждый компонент создаёт определённую разметку и заполняет её данными, полученными из родительского компонента через props
. Обратите внимание, что компонент может получать также вложенную в него разметку – через свойство props.children
.
Что изменилось? В качестве уникального ключа для элементов массива мы теперь используем id проекта, а не порядковый индекс элемента. Эта практика лучше, так как позволяет React оптимизировать рендер.
Директория src
теперь выглядит так:
Все исходники можно посмотреть здесь. Визуально ничего не изменилось.
Чем больше проект, тем важнее становится его структура. Загляните в большое руководство по структурированию react-проектов, чтобы создавать удобные и легко поддерживаемые приложения.
Взаимодействие с пользователем
Добавляем формы. Теперь нужно научить наше SPA отслеживать действия пользователя и реагировать на них. Для демонстрации лучше всего подходят формы, поэтому мы добавим форму обратной связи в наше портфолио. Вот так это будет выглядеть на странице.
Форма обратной связи. Добавляем новый компонент ContactForm
.
import React, { Component } from 'react';
import './ContactForm.css';
class ContactForm extends Component {
state = {
email: '',
emailError: null,
offer: '',
offerError: null
};
emailChangeHandler = event => {
const email = event.target.value;
this.setState({
email,
emailError: !email
});
};
offerChangeHandler = event => {
const offer = event.target.value;
this.setState({
offer,
offerError: !offer
});
};
submitHandler = event => {
event.preventDefault();
const { email, offer } = this.state;
if (email && offer) {
this.setState({
email: '',
emailError: false,
offer: '',
offerError: false
});
this.props.onSubmit();
return;
}
this.setState({
emailError: !email,
offerError: !offer
});
};
render() {
const { email, emailError, offer, offerError } = this.state;
return (
<form className='contact-form' onSubmit={this.submitHandler}>
<div className='contact-form__field'>
<input
value={email}
onChange={this.emailChangeHandler}
placeholder='Email для связи'
/>
{emailError ? (
<div className='error'>Заполните поле</div>
) : null}
</div>
<div className='contact-form__field'>
<textarea
rows='10'
value={offer}
onChange={this.offerChangeHandler}
placeholder='Ваше предложение'
></textarea>
{offerError ? (
<div className='error'>Заполните поле</div>
) : null}
</div>
<button className='button' type='submit'>
Отправить
</button>
</form>
);
}
}
export default ContactForm;
Этот компонент имеет состояние: он хранит значения двух полей, а также флаги, указывающие, нужно ли показывать ошибку валидации. Поэтому при его создании мы используем класс, наследующий от React.Component
. Весь рендер при этом переносится в метод render
.
Обработчики событий. У ContactForm
есть также методы-обработчики событий onSubmit
(отправка формы) и onChange
(изменение значений полей). Поля ввода полностью контролируются React – мы отслеживаем ввод и указываем, что выводить. Чтобы лучше понять, что такое контролируемые компоненты и как происходит изменение данных в формах, загляните в документацию React.
Храним состояние формы. Теперь у компонента App появляется состояние: форма может быть закрытой или открытой. Чтобы хранить этот параметр переделаем App из функционального компонента (stateless) в классовый и добавим ему поле state
.
Внизу разместим кнопку, клики по которой будем отслеживать с помощью атрибута onClick
.
class App extends React.Component {
state = {
closed: true
};
openForm() {
this.setState({
closed: false
});
}
closeForm() {
this.setState({
closed: true
});
}
render() {
return (
<div className='app'>
<Header brand='Иван Иванов'></Header>
<main className='main'>
<About title='React-разработчик Иван Иванов'>
<p>
Разрабатываю на самом крутом в мире фреймворке
<br />
самые крутые в мире SPA!
</p>
<p>С удовольствием и вам что-нибудь разработаю ;)</p>
</About>
<div className='portfolio'>
<div className='container'>
{works.map(work => (
<PortfolioItem key={work.id} work={work} />
))}
</div>
</div>
<div className='contacts'>
<div className='container'>
{this.state.closed ? (
<button
className='button'
onClick={() => this.openForm()}
>
Напишите мне
</button>
) : (
<div>
<hr />
<ContactForm
onSubmit={() => this.closeForm()}
/>
</div>
)}
</div>
</div>
</main>
</div>
);
}
}
Все исходники здесь.
Обратите внимание: Для изменения состояния компонента предназначен метод this.setState
, который принимает новый объект состояния. Подробнее о состоянии вы можете прочитать в документации.
Страница проекта
Добавим в приложение страницы. Хоть мы и используем React, до сих пор в нашем проекте не было ничего особенного. Мы работаем в пределах одной страницы, отслеживаем события и меняем DOM. Чтобы прикоснуться к магии Single Page Application, нужно в наше приложение добавить страниц. Например, было бы неплохо показывать детальную информацию о выполненном проекте по адресу /projects/{идентификатор_проекта}
.
Требования к маршрутизации SPA:
- Переход между страницами должен происходить без перезагрузки.
- Каждый переход должен сохраняться в истории браузера (кнопки Назад/Вперед должны функционировать).
- При вводе в адресную строку некоторого маршрута должен выводиться тот контент, который этому маршруту соответствует, как в мультистраничном приложении.
Пакет react-router-dom. Не переживайте, нам не придётся заниматься этим вручную. Честно говоря, нам вообще мало что придётся делать – всё уже сделано. Пакет react-router инкапсулирует логику маршрутизации, а react-router-dom обеспечивает его взаимодействие с DOM в браузере.
Устанавливаем модуль:
npm i -D react-router-dom
Теперь откройте файл src/index.js
и оберните всё приложение в компонент BrowserRouter
.
import { BrowserRouter } from 'react-router-dom';
const app = (
<BrowserRouter>
<App />
</BrowserRouter>
);
ReactDOM.render(app, document.getElementById('root'));
Страница проекта будет иметь тот же хедер, что и главная, но другой контент. Чтобы логически разделить их, создадим в проекте директорию src/pages
с двумя папками: home
и project
. В home/index.js
перенесем имеющийся код из компонента App
, а в project/index.js
создадим разметку для новой страницы проекта.

import React from 'react';
import About from '../../components/About/About';
import PortfolioItem from '../../components/PortfolioItem/PortfolioItem';
import ContactForm from '../../components/ContactForm/ContactForm';
import works from '../../works';
class HomePage extends React.Component {
state = {
closed: true,
};
openForm() {
this.setState({
closed: false,
});
}
closeForm() {
this.setState({
closed: true,
});
}
render() {
return (
<div>
<About title='React-разработчик Иван Иванов'>
<p>
Разрабатываю на самом крутом в мире фреймворке
<br />
самые крутые в мире SPA!
</p>
<p>С удовольствием и вам что-нибудь разработаю ;)</p>
</About>
<div className='portfolio'>
<div className='container'>
{works.map((work) => (
<PortfolioItem key={work.id} work={work} />
))}
</div>
</div>
<div className='contacts'>
<div className='container'>
{this.state.closed ? (
<button
className='button'
onClick={() => this.openForm()}
>
Напишите мне
</button>
) : (
<div>
<hr />
<ContactForm
onSubmit={() => this.closeForm()}
/>
</div>
)}
</div>
</div>
</div>
);
}
}
export default HomePage;
import React from 'react';
import works from '../../works';
import './index.css';
class ProjectPage extends React.Component {
state = {
project: null,
error: false
};
componentDidMount() {
const id = this.props.match.params.id;
setTimeout(() => {
const project = works.find(work => work.id === id);
this.setState({
project: project,
error: !project
});
}, 1000);
}
render() {
const { project, error } = this.state;
if (error)
return <div className='container'>Что-то пошло не так...</div>;
if (!project) return <div className='container'>Loading...</div>;
return (
<div className='project'>
<div className='container'>
<img
className='project__screenshot'
src={project.screenshot}
alt={project.title}
/>
<h1 className='project__title'>{project.title}</h1>
<p className='project__description'>
{project.description}
</p>
<div className='project__stack'>
{project.stack.join(', ')}
</div>
<div>
<a href={project.link} className='project__link'>
Ссылка на проект
</a>
</div>
</div>
</div>
);
}
}
export default ProjectPage;
Сами маршруты мы определим прямо в компоненте App, но в реальном приложении, вы, вероятно, захотите указать их в отдельном файле для удобства.
class App extends React.Component {
render() {
return (
<div className='app'>
<Header brand='Иван Иванов'></Header>
<main className='main'>
<Switch>
<Route path='/project/:id' component={ProjectPage} />
<Route exact path='/' component={HomePage} />
<Redirect to='/' />
</Switch>
</main>
</div>
);
}
}
Все исходники вы можете найти здесь. Демонстрационная версия приложения имеет ограниченную функциональность – нет редиректа на главную при вводе несуществующего маршрута.
На что обратить внимание:
- Идентификатор конкретного проекта передается прямо в url как параметр
:id
. - Извлечь его в компоненте можно из
this.props.match.params.id
. - В методе
componentDidMount
компонентаProjectPage
можно сделать запрос к серверу для получения данных проекта. В коде задержку запроса имитирует методsetTimeout
. - Все внутренние ссылки (в компонентах
Header
иProjectPreview
) заменены на компонентLink
, это обеспечивает переход между маршрутами без перезагрузки. - Компонент
App
больше не имеет состояния – оно перешло в компонентHomePage
– поэтому его снова можно сделать функциональным.
Чтобы глубже разобраться в работе роутера, загляните в этот туториал по использованию React Router.
Резюмируем: основы создания SPA
Итак, что требуется для создания Single Page Application на React?
- Разделить код проекта на компоненты, которые можно заменять.
- Отслеживать действия пользователя для изменения состояния приложения.
- Настроить маршрутизацию без перезагрузки, передавая при необходимости параметры маршрутам для более тонкой настройки.
Ваше первое простое SPA на React готово!
Не останавливайтесь на достигнутом, продолжайте развиваться, учиться и создавать самые крутые приложения.
Комментарии