Хабрахабр

[Перевод] Учебный курс по React, часть 27: курсовой проект

В этой части перевода учебного курса по React вам предлагается создать генератор мемов.

image

→ Часть 1: обзор курса, причины популярности React, ReactDOM и JSX
→ Часть 2: функциональные компоненты
→ Часть 3: файлы компонентов, структура проектов
→ Часть 4: родительские и дочерние компоненты
→ Часть 5: начало работы над TODO-приложением, основы стилизации
→ Часть 6: о некоторых особенностях курса, JSX и JavaScript
→ Часть 7: встроенные стили
→ Часть 8: продолжение работы над TODO-приложением, знакомство со свойствами компонентов
→ Часть 9: свойства компонентов
→ Часть 10: практикум по работе со свойствами компонентов и стилизации
→ Часть 11: динамическое формирование разметки и метод массивов map
→ Часть 12: практикум, третий этап работы над TODO-приложением
→ Часть 13: компоненты, основанные на классах
→ Часть 14: практикум по компонентам, основанным на классах, состояние компонентов
→ Часть 15: практикумы по работе с состоянием компонентов
→ Часть 16: четвёртый этап работы над TODO-приложением, обработка событий
→ Часть 17: пятый этап работы над TODO-приложением, модификация состояния компонентов
→ Часть 18: шестой этап работы над TODO-приложением
→ Часть 19: методы жизненного цикла компонентов
→ Часть 20: первое занятие по условному рендерингу
→ Часть 21: второе занятие и практикум по условному рендерингу
→ Часть 22: седьмой этап работы над TODO-приложением, загрузка данных из внешних источников
→ Часть 23: первое занятие по работе с формами
→ Часть 24: второе занятие по работе с формами
→ Часть 25: практикум по работе с формами
→ Часть 26: архитектура приложений, паттерн Container/Component
→ Часть 27: курсовой проект

Занятие 45. Курсовой проект. Генератор мемов

→ Оригинал

Займёмся созданием приложения, которое будет генерировать мемы. Вот мы и добрались до курсового проекта. Начнём работу со стандартного проекта create-react-app, созданного с помощью такой команды:

npx create-react-app meme-generator

Здесь можно найти сведения об особенностях её использования.

В стандартном проекте уже есть шаблонный код, находящийся, в частности, в файлах index.js и App.js. В ходе работы над этим проектом вам будет предложено реализовывать некоторые его части самостоятельно, а потом уже читать пояснения о них. Вы вполне можете этот код удалить и попытаться написать его самостоятельно для того чтобы проверить себя в реализации стандартных механизмов React-приложений.

В этом проекте вам предлагается использовать следующие стили:

* { box-sizing: border-box;
} body { margin: 0; background-color: whitesmoke;
} header { height: 100px; display: flex; align-items: center; background: #6441A5; /* fallback for old browsers */ background: -webkit-linear-gradient(to right, #2a0845, #6441A5); /* Chrome 10-25, Safari 5.1-6 */ background: linear-gradient(to right, #2a0845, #6441A5); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
} header > img { height: 80%; margin-left: 10%;
} header > p { font-family: VT323, monospace; color: whitesmoke; font-size: 50px; margin-left: 60px;
} .meme { position: relative; width: 90%; margin: auto;
} .meme > img { width: 100%;
} .meme > h2 { position: absolute; width: 80%; text-align: center; left: 50%; transform: translateX(-50%); margin: 15px 0; padding: 0 5px; font-family: impact, sans-serif; font-size: 2em; text-transform: uppercase; color: white; letter-spacing: 1px; text-shadow: 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000, 2px 2px 5px #000;
} .meme > .bottom { bottom: 0;
} .meme > .top { top: 0;
} .meme-form { width: 90%; margin: 20px auto; display: flex; justify-content: space-between;
} .meme-form > input { width: 45%; height: 40px;
} .meme-form > button { border: none; font-family: VT323, monospace; font-size: 25px; letter-spacing: 1.5px; color: white; background: #6441A5;
} .meme-form > input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ font-family: VT323, monospace; font-size: 25px; text-align: cen
}
.meme-form > input::-moz-placeholder { /* Firefox 19+ */ font-family: VT323, monospace; font-size: 25px; text-align: cen
}
.meme-form > input:-ms-input-placeholder { /* IE 10+ */ font-family: VT323, monospace; font-size: 25px; text-align: cen
}
.meme-form > input:-moz-placeholder { /* Firefox 18- */ font-family: VT323, monospace; font-size: 25px; text-align: cen
}

Эти стили можно включить в уже имеющийся в проекте файл index.css и подключить в файле index.js.

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

Вот что должно оказаться в index.js:

import React from "react"
import ReactDOM from "react-dom"
import './index.css'
import App from "./App" ReactDOM.render(<App />, document.getElementById("root"))

Здесь мы импортируем React и ReactDOM, импортируем стили из index.css и компонент App. После этого, с помощью метода ReactDOM.render(), выводим то, что формирует компонент App, в элемент страницы index.html с идентификатором root (<div id="root"></div>).

Вот как может выглядеть файл App.js:

import React from "react" function App() { return ( <h1>Hello world!</h1> )
} export default App

Тут сейчас представлен простейший функциональный компонент.

На данном этапе работы проект выглядит так, как показано ниже.

Приложение в браузере

Теперь создайте два новых компонента, в двух файлах, имена которых соответствуют именам компонентов:

  • Компонент Header, который будет использоваться для вывода заголовка приложения.
  • Компонент MemeGenerator, в котором будут решаться основные задачи, возлагаемые на приложение. А именно, здесь будут выполняться обращения к API. Здесь же будут храниться данные приложения.

Учитывая то, какие функции возлагаются на эти компоненты, подумайте о том, какими они должны быть.

Вот содержимое файла Header.js:

import React from "react" function Header() { return ( <h1>HEADER</h1> )
} export default Header

Так как этот компонент будет использоваться только для вывода заголовка приложения, его мы оформили в виде функционального компонента.

Вот код файла MemeGenerator.js:

import React, from "react" class MemeGenerator extends Component { constructor() { super() this.state ={} } render() { return ( <h1>MEME GENERATOR SECTION</h1> ) }
} export default MemeGenerator

Тут мы, учитывая задачи, которые предполагается решать средствами компонента MemeGenerator, будем использовать компонент, основанный на классе. Здесь имеется конструктор, в котором мы инициализируем состояние пустым объектом.

В нашем случае это — тег <div>. Создав эти файлы, импортируем их в App.js и возвратим из функционального компонента App разметку, в которой используются экземпляры этих компонентов, не забывая о том, что, если функциональный компонент возвращает несколько элементов, их нужно во что-то обернуть. Вот обновлённый код App.js:

import React from "react"
import Header from "./Header"
import MemeGenerator from "./MemeGenerator" function App() { return ( <div> <Header /> <MemeGenerator /> </div> )
} export default App

Проверим внешний вид приложения.

Приложение в браузере

Здесь мы воспользуемся семантическим элементом HTML5 <header>. Теперь поработаем над компонентом Header. Теперь код файла Header.js будет выглядеть так: В этом теге будет размещено изображение и текст.

import React from "react" function Header() { return ( <header> <img src="http://www.pngall.com/wp-content/uploads/2016/05/Trollface.png" alt="Problem?" /> <p>Meme Generator</p> </header> )
} export default Header

Вот как изменится внешний вид приложения.

Приложение в браузере

Работа над компонентом Header на этом завершена. Заголовок приложения оформлен в соответствии с ранее подключёнными в index.js стилями.

Сейчас вам предлагается самостоятельно инициализировать состояние этого компонента, записав в него следующие данные: Продолжим заниматься компонентом MemeGenerator.

  • Текст, выводимый в верхней части мема (свойство topText).
  • Текст, выводимый в нижней части мема (свойство bottomText).
  • Случайное изображение (свойство randomImage, которое нужно инициализировать ссылкой http://i.imgflip.com/1bij.jpg).

Вот каким будет код MemeGenerator.js после инициализации состояния:

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg" } } render() { return ( <h1>MEME GENERATOR SECTION</h1> ) }
} export default MemeGenerator

Сейчас на внешний вид приложения это не повлияет.

На данном этапе работы над проектом вам предлагается реализовать в компоненте MemeGenerator следующий функционал: Мы будем использовать обращения к API, которое возвращает массив объектов, содержащих ссылки на изображения, на основе которых можно создавать мемы.

  • Выполните обращение к API https://api.imgflip.com/get_memes/.
  • Сохраните данные, доступные в ответе в виде массива response.data.memes, в новом свойстве состояния (allMemeImgs).

Вот, чтобы было понятнее, фрагмент JSON-данных, возвращаемых при обращении к этому API:

{ "success":true, "data":{ "memes":[ { "id":"112126428", "name":"Distracted Boyfriend", "url":"https:\/\/i.imgflip.com\/1ur9b0.jpg", "width":1200, "height":800, "box_count":3 }, { "id":"87743020", "name":"Two Buttons", "url":"https:\/\/i.imgflip.com\/1g8my4.jpg", "width":600, "height":908, "box_count":2 }, { "id":"129242436", "name":"Change My Mind", "url":"https:\/\/i.imgflip.com\/24y43o.jpg", "width":482, "height":361, "box_count":2 }, …. ] }
}

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

Здесь мы, воспользовавшись стандартным методом fetch(), выполним обращение к API. Поэтому для их загрузки мы прибегнем к методу жизненного цикла компонента componentDidMount(). После загрузки данных нам будет доступен объект ответа, из него мы извлекаем массив memes и помещаем его в новое свойство состояния allMemeImgs, инициализированное пустым массивом. Оно возвращает промис. Так как эти данные пока не используются для формирования чего-то такого, что выводится на экран, мы, для проверки правильности работы механизма загрузки данных, выведем первый элемент массива в консоль.

Вот как выглядит код компонента MemeGenerator на данном этапе работы:

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg", allMemeImgs: [] } } componentDidMount() { fetch("https://api.imgflip.com/get_memes") .then(response => response.json()) .then(response => { const {memes} = response.data console.log(memes[0]) this.setState({ allMemeImgs: memes }) }) } render() { return ( <h1>MEME GENERATOR SECTION</h1> ) }
} export default MemeGenerator

Вот что попадает в консоль после успешной загрузки данных.

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

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

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

Генератор мемов

Сейчас вам предлагается, взяв за основу показанный ниже обновлённый код компонента MemeGenerator, который отличается от вышеприведённого кода этого компонента тем, что сюда добавлена заготовка формы, самостоятельно создать пару текстовых полей, topText и bottomText. В частности, в его интерфейсе имеется пара полей для ввода текста, который будет выводиться в верхней и нижней частях изображения. Добавьте к ним необходимые атрибуты. Учитывайте то, что это должны быть управляемые компоненты. Создайте обработчик событий onChange этих полей, в котором нужно, по мере ввода текста в них, обновлять соответствующие свойства состояния.

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg", allMemeImgs: [] } } componentDidMount() { fetch("https://api.imgflip.com/get_memes") .then(response => response.json()) .then(response => { const {memes} = response.data this.setState({ allMemeImgs: memes }) }) } render() { return ( <div> <form className="meme-form"> { // Здесь должны быть текстовые поля } <button>Gen</button> </form> </div> ) }
} export default MemeGenerator

Кстати, обратите внимание на то, что для того чтобы включить комментарий в код, возвращаемый методом render(), мы заключили его в фигурные скобки для того чтобы указать системе на то, что данный фрагмент она должна воспринимать как JavaScript-код.

Вот что у вас должно получиться на данном этапе работы над приложением:

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg", allMemeImgs: [] } this.handleChange = this.handleChange.bind(this) } componentDidMount() { fetch("https://api.imgflip.com/get_memes") .then(response => response.json()) .then(response => { const {memes} = response.data this.setState({ allMemeImgs: memes }) }) } handleChange(event) { const {name, value} = event.target this.setState({ [name]: value }) } render() { return ( <div> <form className="meme-form"> <input type="text" name="topText" placeholder="Top Text" value={this.state.topText} onChange={this.handleChange} /> <input type="text" name="bottomText" placeholder="Bottom Text" value={this.state.bottomText} onChange={this.handleChange} /> <button>Gen</button> </form> </div> ) }
} export default MemeGenerator

Теперь страница приложения будет выглядеть так, как показано ниже.

Приложение в браузере

Для того чтобы проверить правильность работы реализованных здесь механизмов, вы можете воспользоваться командой console.log(). Пока на экран выводятся лишь поля с текстом подсказок, ввод данных в них не приводит к изменениям интерфейса.

Вспомним о том, что сейчас у нас имеется массив, содержащий сведения об изображениях, которые планируется использовать в качестве основы мемов. Теперь поработаем над той частью приложения, которая ответственна за вывод на экран изображения-мема. Приложение должно, по нажатию на кнопку Gen, случайным образом выбирать из этого массива изображение и формировать мем.

Здесь, в методе render(), ниже кода описания формы, имеется элемент <div>, включающий в себя элемент <img>, выводящий изображение, и пару элементов <h2>, которые выводят надписи. Вот обновлённый код компонента MemeGenerator. Элементы <div> и <h2> оформлены с использованием стилей, которые мы добавляли в проект в самом начале работы над ним.

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg", allMemeImgs: [] } this.handleChange = this.handleChange.bind(this) } componentDidMount() { fetch("https://api.imgflip.com/get_memes") .then(response => response.json()) .then(response => { const {memes} = response.data this.setState({ allMemeImgs: memes }) }) } handleChange(event) { const {name, value} = event.target this.setState({ [name]: value }) } render() { return ( <div> <form className="meme-form"> <input type="text" name="topText" placeholder="Top Text" value={this.state.topText} onChange={this.handleChange} /> <input type="text" name="bottomText" placeholder="Bottom Text" value={this.state.bottomText} onChange={this.handleChange} /> <button>Gen</button> </form> <div className="meme"> <img align="center" src={this.state.randomImg} alt="" /> <h2 className="top">{this.state.topText}</h2> <h2 className="bottom">{this.state.bottomText}</h2> </div> </div> ) }
} export default MemeGenerator

Вот как приложение выглядит теперь.

Приложение в браузере

Мы пока не пользуемся изображениями, которые хранятся в свойстве состояния allMemeImgs. Обратите внимание на то, что здесь выводится то изображение, которым инициализировано состояние. Попробуем ввести что-нибудь в текстовые поля.

Приложение в браузере

Теперь осталось лишь сделать так, чтобы по нажатию на кнопку Gen из массива с данными изображений выбиралось бы случайное изображение и загружалось бы в элемент <img>, присутствующий на странице ниже полей для ввода текста. Как видно, подсистемы приложения, ответственные за работу с текстом, функционируют так, как ожидается.

Создайте метод, который срабатывает при нажатии на кнопку Gen. Для того чтобы оснастить приложение этой возможностью — выполните следующее задание. Учитывайте то, что в allMemeImgs хранится массив объектов, описывающих изображения, и то, что у каждого объекта из этого массива есть свойство url. Этот метод должен выбирать одно из изображений, сведения о которых хранятся в свойстве состояния allMemeImgs, после чего выполнять действия, которые позволяют вывести это изображение в элементе <img>, расположенном под полями ввода текста.

Вот код, в котором приведено решение этой задачи:

import React, {Component} from "react" class MemeGenerator extends Component { constructor() { super() this.state = { topText: "", bottomText: "", randomImg: "http://i.imgflip.com/1bij.jpg", allMemeImgs: [] } this.handleChange = this.handleChange.bind(this) this.handleSubmit = this.handleSubmit.bind(this) } componentDidMount() { fetch("https://api.imgflip.com/get_memes") .then(response => response.json()) .then(response => { const {memes} = response.data this.setState({ allMemeImgs: memes }) }) } handleChange(event) { const {name, value} = event.target this.setState({ [name]: value }) } handleSubmit(event) { event.preventDefault() const randNum = Math.floor(Math.random() * this.state.allMemeImgs.length) const randMemeImg = this.state.allMemeImgs[randNum].url this.setState({ randomImg: randMemeImg }) } render() { return ( <div> <form className="meme-form" onSubmit={this.handleSubmit}> <input type="text" name="topText" placeholder="Top Text" value={this.state.topText} onChange={this.handleChange} /> <input type="text" name="bottomText" placeholder="Bottom Text" value={this.state.bottomText} onChange={this.handleChange} /> <button>Gen</button> </form> <div className="meme"> <img align="center" src={this.state.randomImg} alt="" /> <h2 className="top">{this.state.topText}</h2> <h2 className="bottom">{this.state.bottomText}</h2> </div> </div> ) }
} export default MemeGenerator

Кнопке Gen можно назначить обработчик события, возникающего при щелчке по ней, как это делается при работе с любыми другими кнопками. Однако, учитывая то, что эта кнопка используется для отправки формы, лучше будет воспользоваться обработчиком события onSubmit формы. В этом обработчике, handleSubmit(), мы вызываем метод поступающего в него события event.preventDefault() для того, чтобы отменить стандартную процедуру отправки формы, в ходе которой выполняется перезагрузка страницы. Далее, мы получаем случайное число в диапазоне от 0 до значения, соответствующего индексу последнего элемента массива allMemeImgs и используем это число для обращения к элементу с соответствующим индексом. Обратившись к элементу, являющемуся объектом, мы получаем свойство этого объекта url и записываем его в свойство состояния randomImg. После этого выполняется повторный рендеринг компонента и внешний вид страницы меняется.

Страница приложения в браузере

Курсовой проект завершён.

Итоги

На этом занятии вы создали приложение, в котором использовано то, чему вы учились, осваивая React. В следующий раз мы поговорим о разработке современных React-приложений и обсудим идеи проектов, реализуя которые вы можете попрактиковаться в работе с React.

Уважаемые читатели! Столкнулись ли вы с какими-нибудь сложностями, выполняя этот курсовой проект?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть