В этой части перевода учебного курса по React вам предлагается создать генератор мемов.
→ Часть 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, {Component} from "react"
class MemeGenerator extends Component {
constructor() {
super()
this.state ={}
}
render() {
return (
<h1>MEME GENERATOR SECTION</h1>
)
}
}
export default MemeGenerator
Тут мы, учитывая задачи, которые предполагается решать средствами компонента MemeGenerator
, будем использовать компонент, основанный на классе. Здесь имеется конструктор, в котором мы инициализируем состояние пустым объектом.
Создав эти файлы, импортируем их в App.js
и возвратим из функционального компонента App
разметку, в которой используются экземпляры этих компонентов, не забывая о том, что, если функциональный компонент возвращает несколько элементов, их нужно во что-то обернуть. В нашем случае это — тег <div>
. Вот обновлённый код App.js
:
import React from "react"
import Header from "./Header"
import MemeGenerator from "./MemeGenerator"
function App() {
return (
<div>
<Header />
<MemeGenerator />
</div>
)
}
export default App
Проверим внешний вид приложения.
Приложение в браузере
Теперь поработаем над компонентом Header
. Здесь мы воспользуемся семантическим элементом HTML5 <header>
. В этом теге будет размещено изображение и текст. Теперь код файла Header.js
будет выглядеть так:
import React from "react"
function Header() {
return (
<header>
<img
src="https://www.pngall.com/wp-content/uploads/2016/05/Trollface.png"
alt="Problem?"
/>
<p>Meme Generator</p>
</header>
)
}
export default Header
Вот как изменится внешний вид приложения.
Приложение в браузере
Заголовок приложения оформлен в соответствии с ранее подключёнными в index.js
стилями. Работа над компонентом Header
на этом завершена.
Продолжим заниматься компонентом 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
Сейчас на внешний вид приложения это не повлияет.
Мы будем использовать обращения к API, которое возвращает массив объектов, содержащих ссылки на изображения, на основе которых можно создавать мемы. На данном этапе работы над проектом вам предлагается реализовать в компоненте MemeGenerator
следующий функционал:
- Выполните обращение к 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
},
….
]
}
}
Решая задачу, поставленную выше, нужно учитывать то, что речь идёт о данных, которые нужны компоненту в самом начале работы приложения.
Поэтому для их загрузки мы прибегнем к методу жизненного цикла компонента componentDidMount()
. Здесь мы, воспользовавшись стандартным методом fetch()
, выполним обращение к API. Оно возвращает промис. После загрузки данных нам будет доступен объект ответа, из него мы извлекаем массив 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
, случайным образом выбирать из этого массива изображение и формировать мем.
Вот обновлённый код компонента MemeGenerator
. Здесь, в методе render()
, ниже кода описания формы, имеется элемент <div>
, включающий в себя элемент <img>
, выводящий изображение, и пару элементов <h2>
, которые выводят надписи. Элементы <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
, после чего выполнять действия, которые позволяют вывести это изображение в элементе <img>
, расположенном под полями ввода текста. Учитывайте то, что в allMemeImgs
хранится массив объектов, описывающих изображения, и то, что у каждого объекта из этого массива есть свойство url
.
Вот код, в котором приведено решение этой задачи:
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.
Уважаемые читатели! Столкнулись ли вы с какими-нибудь сложностями, выполняя этот курсовой проект?
Автор: ru_vds