habrahabr

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

  • четверг, 18 апреля 2019 г. в 00:22:13
https://habr.com/ru/company/ruvds/blog/447136/
  • Блог компании RUVDS.com
  • JavaScript
  • ReactJS
  • Разработка веб-сайтов


В этой части перевода учебного курса по 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, {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="http://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.

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