[Программирование, Java, ReactJS] YAHW на React
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Да, это еще один хелло-ворлд на React, которых уже много на сети. Зачем еще один? Здесь я попытался рассказать о создании простого приложения так, как хотел бы прочитать об этом в то время когда делал первые шаги на React, т.е. совсем недавно. Обратить внимание на то, что мне нужно было узнать сначала. Надеюсь начинающим пригодится, а продолжающие дадут свои замечания.Создание первого приложения Здесь все максимально просто, как это часто бывает с созданием хелловорлдов. Все (почти) сделают за нас. Перед тем как начать убеждаемся в том, что у нас есть необходимый инструментарий.
node --version
v10.24.0
npm --version
6.14.11
npx --version
10.2.2
Собственно создание приложения выполняется простой командой
npx create-react-app hw-app
где hw-app (helloworld-application) -- имя приложения.В текущей папке будет создана папка с именем hw-app, содержащая все необходимое для запуска React приложения. Чтобы проверить его работу нужно зайти внутрь (cd hw-app) и запустить приложение. npm startРезультатом работы команды будет являться не только текст на экране
You can now view hw-app in the browser.
http://localhost:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
но и возможность проверить его правдивость. Нацелим свой браузер на указанный адрес и увидим работающее приложение.
Что бы остановить приложение (если захотим) нажмем Ctr-C. Посмотрим на содержимое папки приложения-проекта.
ls
README.md node_modules package-lock.json package.json public src
На данном этапе нас будут интересовать папки public и src.
ls public
favicon.ico index.html logo192.png logo512.png manifest.json robots.txt
ls src
App.css App.js App.test.js index.css index.js logo.svg reportWebVitals.js setupTests.js</pre></code>
В папке public лежит index.html, который и будет отдаваться dev-сервером в ответ на запрос браузера. В свою очередь в index.html есть div элемент с id='root', в который React "отрисует" приложение, как это указано в файле src/index.jsReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);и отрисует он там компонент App. Чтобы рассмотреть немного ближе, как это работает изменим файлы index.html и App.js. Пусть браузeр в заголовке показывает название именно нашего приложения. Изменим соответствующую строку было<title>React App</title>стало<title>My First React App</title>А компонент App пусть покажет наш контент (содержимое файла App.js) было:import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>);} стало:function App() {
return (
<div>
<h1>Hello React!</h1>
</div>
);
}
export default App;также из папки src можно удалить все неиспользуемые файлы в данный момент файлы. Содержимое папки должно быть таким
ls src/
App.js index.js
По необходимости мы будем добавлять нужные файлы сами и узнаем зачем мы это делаем. А пока посмотрим на результат.
Выглядит как настоящий HelloWorld, но останавливаться мы на этом не будем и рассмотрим простые случаи взаимодействия с пользователем, чтобы наш "Hello" не улетел в пустоту. В React мы работаем с компонентами. В данном и самом простом случае компонент - это функция написанная на JavaScript и возвращающая код, похожий на разметку html. Похожий, но являющийся на деле кодом JSX из которого html получается в результате компиляции. Мы не будем вносить изменения в файл App.js пусть он остается корневым компонентом нашего в будущем интерактивного приложения, в котором мы расположим написанные нами компоненты.Не забудем убрать лишние строки из src/index.js, он примет вид import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);Следует сказать, что компоненты могут быть функциональными -- просто функция, возвращающая представление компонента и "классовыми" -- это компонент описываемый JS классом, имеющий внутреннее состояние и дополнительный функционал. Сказанное не означает, что функциональные компоненты не могут иметь внутреннего состояния, но об этом позже. Добавим в наше приложение два компонента, один функциональный и один в виде класса. Оба будут иметь состояния, что-то о себе помнить и взаимодействовать с пользователем. Первый компонент, назовем его ClickCounter, будет считать сколько раз по нему кликнули. Второй - Machine будет представлять собой интерфейс с какой-то машине, которую можно включать выключать, нажимая на кнопку. ClickCounter - функцияПервая итерация. Это содержимое файла ClickCounter.jsconst clickCounter = () => {
const clickTimes = 0;
return (
<div>
<p>Я счетчик кликов</p>
<p>Кликнуто {clickTimes} раз</p>
</div>
);
}
export default clickCounter;А вот так изменился App.js.import ClickCounter from './ClickCounter';
function App() {
return (
<div>
<h1>Hello React!</h1>
<ClickCounter />
</div>
);
}
export default App; В первой строке импортируем наш ClickCounter и добавляем его в отрисовку после тега "Hello React!". И да, новый компонент будет выглядеть в коде как еще один новый тэг. Тут наверное может возникнуть вопрос об именовании компонентов, в файле ClickCounter.js экспортируем clickCounter, в App.js импортируем ClickCounter, что за дела? Мы можем импортировать хоть MySupperPupperClickCounter из './ClickCounter' и использовать его как <MySupperPupperClickCounter /> , но получим все равно вывод функции clickCounter(), которая экспортируется по дефолту. (попробуйте) Итого: после слова import стоит имя компонента которое мы будем использовать далее в файле (в данном случае в файле App.js) после слова from стоит имя файла с относительным путем, но без расширения '.js'. Что же написано в СlickCounter.js? Определена константа с именем clickCounter, которой присваивается '=' функция без параметров '()' выполняющая код написанный в теле '{}', там же определяется переменная clickTimes. Значение этой переменной будет появляться в строке <p>Кликнуто {clickTimes} раз</p>где имя переменной обернуто в фигурные скобки. Помним, что это JSX и после компиляции мы увидим "Кликнуто 0 раз" как на рисунке.
Пока еще ничего интересного не происходит, т.к. нет самого подсчета кликов. Реализуем его. Здесь очень подробно и хорошо описана работа с хуками состояния. Я может быть немного повторю, но еще раз про строку
const [clickTimes, clickIncrement] = useState(0)
useState(0) - принимает в качестве параметра инициирующее значение для переменной состояния clickTimes его же и возвращает как первый элемент массива.
clickUpdater - функция, которая будет обновлять значение переменной состояния своим параметром.Я вынес логику в функцию clickIncrementer(), чтобы показать, что сложную логику (в нашем случае она, конечно, простая) можно описать в отдельной функции и вернуть состояние оттуда.Таким образом файл ClickCounter.js становится таким:import React, { useState } from 'react';
const ClickCounter = () => {
const [clickTimes, clickUpdater] = useState(0);
const clickIncrementer = () => {
return clickTimes + 1;
}
return (
<div onClick={ () => clickUpdater(clickIncrementer)}>
<p>Я счетчик кликов</p>
<p>Кликнуто {clickTimes} раз</p>
</div>
);
}
export default ClickCounter;Обратите внимание, ClickCounter мы теперь пишем с большой буквы -- это требование к именованию функциональных компонентов. Теперь, кликая по тексту элемента будем наблюдать возрастающее число кликов.Machine - classПоехали дальше. На очереди компонент класс. Назовем этот класс Machine и опишем в файле Machine.js. Класс компонента обязан реализовать функцию render(), которая будет вызываться для отображения компонента в браузере. Сначала просто нарисуем нужные нам элементы с нужными значениями. Вот полный текст файла. import React from 'react';
class Machine extends React.Component {
state = {
machineState: 'STOPPED',
machineStarted: 0
}
render () {
return (
<div>
<p>Я интерфейс машины.</p>
<p>Состояние машины: {this.state.machineState}.
<br/>
<button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button>
</p>
Машину запускали {this.state.machineStartCount} раз.
</div>
);
}
}
export default Machine;Как видим, render() возвращает не что иное, как JS код, и весь этот код должен быть обрамлен одним <div>(<span> или даже <>), для функционального компонента требование то же. В файл App.js добавим строку для импорта
import Machine from './Machine';
и строку для отображения
<Machine />
Полный текст файлаimport ClickCounter from './ClickCounter';
import Machine from './Machine';
function App() {
return (
<div>
<h1>Hello React!</h1>
<ClickCounter />
<Machine />
</div>
);
}
export default App;Что увидим в браузере показано на рисунке. Пока, конечно -- каша. Но совсем скоро мы ее поправим.
А пока обсудим содержимое файла Machine.js. В первой строке import React. Обратите внимание, мы не использовали никаких выражений в фигурных скобках. Именно поэтому мы пишем extends React.Component. Если бы написалиimport React, { Component } from 'react' , то можно было бы сказать extends Component. Далее, появилось объявление и инициализация объекта state. Именно в нем будет храниться изменяемая информация (состояние), связанная с объектом класса. Обращаться к этому объекту нужно с использованием указателя this. Понятное дело, что объект state может быть объемнее и сложнее, чем в нашем случае. Теперь у нас есть привязанное к компоненту состояние, которое нужно менять в зависимости от действия пользователя. Действий не много -- нажатие кнопки. В зависимости от того, в каком состоянии находилась машина в момент нажатия кнопки мы изменим состояние таким образом:
Состояние машины -- STOPPED, на кнопке написано START.
Состояние машины -- STARTED, на кнопке написано STOP.
Счетчик стартов будет считать количество нажатий на кнопку с надписью START.
При работе с состоянием state надо знать, изменять состояние нужно только специальной функцией setState(), при этом фактически будет создано и сохранено новое состояние с новыми значениями. Попробуем на практике, при нажатии на кнопку поменяем соответствующие надписи. Для этого напишем функцию clickButtonHandler. clickButtonHandler = () => {
switch (this.state.machineState) {
case 'STOPPED':
const cnt = this.state.machineStartCount;
this.setState({machineState: 'STARTED',
buttonLabel: 'STOP',
machineStartCount: cnt + 1});
break;
case 'STARTED':
this.setState({machineState: 'STOPPED',
buttonLabel: 'START'});
break;
default:
break;
}
}Код простой и пояснений не требует, кроме наверное одного нюанса (я на этом застрял). Речь идет о наличии константы const cnt = this.state.machineStartCount. Почему бы не написать просто machineStartCount: this.state.machineStartCount + 1? Нельзя. Нельзя использовать непосредственно состояние компонента для создания нового состояния. Просто. Также нужно обратить внимание, что в одном случае мы создаем полностью новое состояние, а в другом, только только его части, не обновляем значение счетчика. setState() правильно обработает ситуацию, она обновит указанные поля и оставит не тронутыми те, о которых умолчали.Теперь добавим эту функцию в обработчик клика для кнопки.<button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button>Все работает, но выглядит все еще так себе.Добавим стиляГромко сказано, просто отделим компоненты друг от друга и покажем их границы. Все описание запишем в файл src/App.css и импортируем его в src/App.js строкойimport './App.css';А это содержимое файла App.css :
.Component {
margin: 20px;
border: 1px solid #eee;
box-shadow: 5px 5px 10px #ccc;
padding: 10px;
}
Чтобы применить стиль к компоненту надо в тэге div добавить className вот так:
<div className='Component'>
Посмотрим, что получилось.
"Пропсы"Не могу понять, почему не использовать 'свойства' или 'параметры', ведь очень похоже на передачу параметров при создании компонента. Может быть, при создании нового сообщества React-истов(еров) потребовался новый слэнг? В общем, следуем официальному сайту. Мы рассмотрели state - структуру, которая хранит информацию о компоненте и которую мы можем изменять в жизненном цикле компонента, изменения инициируются компонентом. Теперь мы рассмотрим props.Это данные, которые родитель компонента может передать дочернему компоненту. "Пропсы" (вроде как устоявшийся термин) не изменяются в течение срока жизни компонента самим компонентом, для него это "константы", но которые компонент может прочитать. Поcмотрим как это работает.Представим себе, что нам нужно создать две машины, два компонента Machine, которые будут отличаться именем-обозначением, в остальном экземпляры компонента будут одинаковы. Добавим дополнительную информацию с использованием props. Создадим в App две машины с именами "Первая машина" и "Вторая машина" следующим образом<Machine name='Первая машина' />
<Machine name='Вторая машина' />name и будет тем самым пропсом. Теперь изменим функцию render() компонента.render () {
return (
<div className='Component'>
{this.props.name}
<p>Я интерфейс машины.</p>
<p>Состояние машины: {this.state.machineState}.
<br/>
<button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button>
</p>
Машину запускали {this.state.machineStartCount} раз.
</div>
);
}Обратим внимание на строку после div. Теперь мы имеем на странице два независимых однотипных компонента с разными состояниями. Смотрим на рисунок.
Это самый простой случай использования пропса. В игре "Сапер" рассмотрим еще. Промежуточный итог таков:
- мы научились создавать простое приложение React;
- мы научились создавать компоненты React;
- мы научились взаимодействовать с компонентами
- узнали что такое state и props
На очереди -- создание игры "Сапер". Рассмотрим создание более сложных компонентов, посмотрим как можно устроить взаимодействие между компонентами. Для реализации логики игры сначала поработаем с состояниями компонентов, во второй итерации посмотрим как хранить состояние всего приложения и работать с ним с использованием redux. Приступим.Игра "Сапер" Все знают игру "Сапер", нужно на поле размера M x N найти X мин. Сделаем такую. Интерфейс игры показан на рисунке.
Здесь есть два "больших" компонента:
- компонент с "измерительными приборами" (счетчик мин и секундомер) ControlPanel
- компонент "игровое поле" с множеством кликабельных компонентов, квадратов с минами или безMineField
Оговорим правила, может быть еще раз.
- перед игроком поле m-строк, n-колонок
- игрок кликает по любой ячейке левой кнопкой мыши
- если это первый клик за игру, то запускается секундомер
- если ячейка пустая и рядом в соседних ячейках нет мин, то ячейка открывается, открываются все соседние ячейки, и если у открывающейся ячейки есть соседи с минами, то их количество появляется на ячейке. Ячейки открываются автоматически последовательно, пока не откроются все соседние пустые ячейки без минных соседей
- если у открытой ячейки есть соседи с минами, то она просто покажет их кол-во
- если игрок кликнет по ячейке с миной, таймер останавливается, показываются все мины (их места расположения), игра окончена
- игрок кликает по закрытой ячейке правой кнопкой мыши - ячейка помечается как заминированная, повторный клик снимает метку, при этом увеличивается или уменьшается счетчик мин
- если откроются все свободные ячейки - секундомер останавливается, игра окончена
Родительский компонент GameПро этот компонент я не упоминал, но он является главным, родительским компонентом для минного поля и панели отображения. Что входит в функции данного компонента:
- конечно создание тех двух
- запуск счетчика секунд после первого клика пользователя по ячейке
- остановка его после "нахождения" мины (проигрыш) или после открытия всех свободных от мин ячеек (выигрыш)
- оповещение пользователя о завершении игры.
Game хранит свое состояние вот в таком объекте:state = {
flagCnt: 0,
seconds: 0
};Здесь: flagCnt -- счетчик флажков на минном поле, seconds -- прошло секунд с начала игры. Но ведь счетчик флажков и прошедших секунд отображается на компоненте ControlPnael , как же происходит передача данных? Посмотрим. Компонент ControlPnaelВот так Game создает ControlPanel<ControlPanel
flagCnt={this.state.flagCnt}
seconds={this.state.seconds}
/>А вот так будет выглядеть код компонента ControlPanel. Он совсем короткий и я привожу его весь. const zeroPad = (num, places) => String(num).padStart(places, '0');
const controlPanel = (props) => {
const min = Math.floor(props.seconds / 60);
const secs = props.seconds % 60;
return (
<div className='Control'
style={{color: '#adadad'}}
>
Flag count:{zeroPad(props.flagCnt)} Time:{zeroPad(min, 2)}:{zeroPad(secs, 2)}
</div>
);
}
export default controlPanel;Функциональный компонент создается с параметром props, Game в пропсах указывает имена переменных (своих переменных) this.state.flagCnt и this.state.seconds, а ControlPanel использует при рендеринге имена пропсов flagCnt и seconds. Просто? Сам компонент не изменяет значения пропсов, за него это делает родительский компонент, причем делает это с использованием setState() (помним, что эта функция используется для изменения state компонента). А так как setState инициирует перерисовку самого компонента и его дочек, то мы увидим изменяющиеся значения на ControlPanel.Как данные спускаются от родительского компонента в дочерний мы увидели, теперь посмотрим как данные "поднимаются" от дочернего компонента к родительскому. Такой фокус происходит при взаимодействии Game и MineField . Взаимодействие Game и MineFieldПри создании компонента MineField используется следующий код в Game :<MineField
rows='8'
cols='8'
mines='10'
gameStarted={this.startGame}
gameOver={this.stopGame}
changeFlagCount={this.setFlag}
/>Имена пропсов, наверное, говорящие: rows -- количество строк ячеек, cols -- количество колонок, mines -- количество мин, с этими значения MineField построит минное поле. Теперь о передаче данных "наверх". Как Game узнАет о том, что игра началась, что был уже клик по ячейке? Просто, MineField вызовет в своем коде функцию gameSarted -- это имя пропса, но выполнится код функции startGame в пространстве класса Game -- это его функция. Это приведет по цепочке к ежесекундному запуску функции tick(), изменяющей значение seconds, до тех пор пока MineField не вызовет функцию gameOver(true|false), при этом вызовется функция stopGame в классе Game. Game покажет alert с сообщением о выигрыше или проигрыше в зависимости от переданного (поднятого?)из MineField параметра. Вот часть кода Game (собственно, почти весь код): start = () => {
this.timerID = setInterval(() => this.tick(), 1000);
}
stop = () => {
clearInterval(this.timerID)
}
tick() {
const oldsec = this.state.seconds;
this.setState({seconds: oldsec + 1});
}
startGame = () => {
this.start();
}
stopGame = (isGameWon) => {
if (isGameWon) {
alert("You win");
} else {
alert("You lose");
}
this.stop();
}Создание ячеек поля и их взаимодействие с полемФункционал ячеек прост и описывается он в классе с оригинальным названием Cell. Первое и основное, что должна сделать ячейка -- это отрисовать себя в соответствии со своим состоянием: закрыта, открыта, помечена флагом. Тут нужно добавить, что состояние ячейки не является хранимым в ячейке состоянием, а опять же передается ей через пропс от MineField. render () {
const cellSize = 40; //px
var width = cellSize + 'px';
var height = cellSize + 'px';
var left = this.props.col (cellSize + 4) + 'px'; var top = this.props.row (cellSize + 4) + 'px';
let backgroundColor = this.props.opened ? '#adadad' : '#501b1d';
var rendstate = () => {
if (this.props.checked) {
return (
<img className='flag' src={flag} alt=''/>
)
}
if (this.props.opened) {
return (
this.props.hasBomb ? <img className='bomb' src={bomb} alt=''/> : (this.props.bombNbr > 0 ? this.props.bombNbr : ''));
}
}
return (
<div className='Cell'
style={{width, height, left, top, backgroundColor}}
onClick={this.leftClickHandler}
onContextMenu={this.rightClickHandler}
>
{rendstate()}
</div>
);
}Да, в этой функции много "магических" чисел (40, 4, 4), наверное код можно было бы сделать чище. Но по именам переменных наверное все понятно: cellSize -- длина стороны ячейки, чтобы хоть как-то уменьшить количество безымянных чисел. width, height -- высота ширина top, left -- координаты верхнего левого угла ячейки на поле, вычисляются в зависимости от номера строки и колонки, передаваемых от MineField в пропсах. bomb, flag -- импортированные из файлов рисунки. Итого, чтобы ячейка себя правильно отрисовала MineField передает ей следующие пропсы:
- col -- колонка
- row -- строка
- checked -- помечена флагом
- opened -- открыта
- bombNbr -- сколько мин (бомб) в соседних ячейках
И конечно, нужно реагировать на нажатия левой и правой кнопки мыши. За это отвечают следующие две функции, обратите внимание, они указаны в тэге div : leftClickHandler = () => {
this.props.clickLeft(this.props.row, this.props.col);
}
rightClickHandler = (e) => {
e.preventDefault();
this.props.clickRight(this.props.row, this.props.col);
}Как видите, вся работа этих функций заключается только в том, чтобы передать "наверх" координаты ячейки, про которой кликнули. После клика, вызываются переданные в пропсах функции, работа которых произойдет в классе MineField и после обработки результат спустится вниз, назад в ячейку в виде измененного пропса opened или checked.Класс MineFieldЭто самый "функционально насыщенный и сложный" класс, весь код составляет немногим более 200 строк. Класс имеет конструктор, в котором создается массив размера rows x cols содержащий элементы типа cellData . Вот описание cellData.class cellData {
constructor (row, col) {
this.row = row;
this.col = col;
this.hasBomb = false;
this.checked = false;
this.opened = false;
this.bombNbr = 0;
this.nbrs = [...Array(0)];
}
}Эти данные полностью описываю состояние ячейки минного поля и именно они передаются в качестве пропсов при создании ячейки. Я про них уже писал. constructor(props) {
super(props);
this.closedCells = props.rows * props.cols - props.mines;
this.flagCount = 0;
this.state = {
field: this.createMap(this.props.rows, this.props.cols, this.props.mines),
gameState: 'waiting',
}
}В поле состояния state.field мы сохраняем карту минного поля, создаваемого функцией createMap(this.props.rows, this.props.cols, this.props.mines) . Если посмотреть на код (ссылка на полный код игры в конце статьи) создания минного поля, то можно увидеть может быть не совсем оптимальное наполнение поля минами и расчет соседей с минами и заполнение списка соседей, в общем, несколько проходов по одному и тому же массиву, но читается легко (наверное). Что же, вот и код отрисовки компонента MineField и создания при этом компонентов ячеек с нужными пропсами, считаем при этом что все данные для ячеек лежат в соответствующем массиве: render() {
return (
<div className='MineField'>
{
this.state.field.map(function(row){
return row.map(function (cell) {
return (
<Cell
row={cell.row}
col={cell.col}
hasBomb={cell.hasBomb}
bombNbr={cell.bombNbr}
key={cell.row + "-" + cell.col}
checked={cell.checked}
opened={cell.opened}
clickLeft={this.cellLeftClicked}
clickRight={this.cellRightClicked}
/>
)
}, this);
}, this)
}
</div>
);
}В конструкции map не забываем передать указатель на класс, иначе будут недоступны функции класса. Все, игровое поле создано и готово принимать клики мышкой. При клике на ячейку, как мы помним, в конечном итоге вызывается функция из класса MineField , производит манипуляции с минным полем, тем самым массивом, потом обновляем состояние (setState()) , и наблюдаем изменение внешнего вида ячеек, пропсы ведь поменялись, а так же смотрим на запуск счетчика секунд и флажков. Вот не очень сложный код функций обработки кликов: cellLeftClicked = (row, col) => {
switch (this.state.gameState) {
case 'waiting':
this.props.gameStarted();
this.setState({gameState: 'started'});
/* falls through */
case 'started':
let newField = [...this.state.field];
this.openCell(newField, row, col);
this.setState({field: newField});
break;
case 'finished':
break;
default:
break;
}
} cellRightClicked = (row, col) => {
switch (this.state.gameState) {
case 'waiting':
break;
case 'started':
if (this.state.field[row][col].opened) {
break;
}
let newField = [...this.state.field];
let flagCntDiff = newField[row][col].checked ? -1 : 1;
if ((this.flagCount + flagCntDiff) < 0 || (this.flagCount + flagCntDiff) > this.props.mines) {
break;
}
this.flagCount += flagCntDiff;
this.props.changeFlagCount(flagCntDiff);
newField[row][col].checked = !newField[row][col].checked;
this.setState({field: newField});
break;
case 'finished':
break;
default:
break;
}
}Как видно, игра перемещается из состояния в состояние waiting -> started -> finished . waiting -- это состояние сразу после загрузки страницы. В состояние started мы перемещаемся после открытия первой ячейки, и в состояние finished после открытия ячейки с миной или открытия всех свободных ячеек. За открытие ячеек отвечает функция openCell() , она рекурсивно вызывает себя для открытия соседних ячеек, которые не граничат заминированными ячейками. В состоянии finished мы перестаем реагировать на действия пользователя, игру (страницу) нужно перезагрузить.Еще раз хотелось бы обратить внимание на то, как происходит работа с минным полем -- state.field . В обработчике клика мышкой мы создаём копию поля. Производим с ним необходимые манипуляции и потом, с помощью setState() устанавливаем новое состояние с новым, обновленным полем.Вот и всеПолный код приложений можно найти по ссылке.В этой статье я хотел показать способы взаимодействия, обмена данными
между компонентами в приложении React, надеюсь получилось. Хотя могло получиться и так, что какие-то вещи уже очевидные для себя сейчас не рассказал. Я также писал здесь в тексте, что расскажу на примере этой же игры про применение redux , но наверное она (игра) того не стоит. Если статья вызовет интерес, сделаем какую-нибудь инфографику с биржи, поучимся вместе использовать графические библиотеки и вот тут дойдет время для redux , будет к месту, наверное. А теперь, всем всего хорошего!
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, C++] C++ 14 с точки зрения олимпиадного программирования
- [Программирование, C++] Multithreading
- [Программирование, C++, Системы сборки] Кроссплатформенный C++ с vcpkg и зависимостями
- [Open source, Программирование, GitHub, Управление разработкой] Это не работает (перевод)
- [Мессенджеры, Программирование, Разработка мобильных приложений, API] Twilio vs Sendbird vs CONTUS MirrorFly Feature Comparsion | Twilio vs Competitors
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Свою квалификацию программиста можно повысить, если разбираться в деталях разных технологий (перевод)
- [JavaScript, Разработка под Arduino, Разработка на Raspberry Pi, DIY или Сделай сам] Умная квартира на JavaScript. От светодиода до распознавания лица в камере домофона
- [JavaScript, Серверная оптимизация, Node.JS] Профилирование Node.js. Доклад Яндекса
- [Я пиарюсь, Системное программирование, Программирование микроконтроллеров] All-Hardware стартовал конкурс прошивок для отладок STM и NXP
- [PostgreSQL, Java, Тестирование веб-сервисов] История о PostgreSQL 13, Testcontainers и багах
Теги для поиска: #_programmirovanie (Программирование), #_java, #_reactjs, #_react, #_component, #_properties, #_programmirovanie (
Программирование
), #_java, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:09
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Да, это еще один хелло-ворлд на React, которых уже много на сети. Зачем еще один? Здесь я попытался рассказать о создании простого приложения так, как хотел бы прочитать об этом в то время когда делал первые шаги на React, т.е. совсем недавно. Обратить внимание на то, что мне нужно было узнать сначала. Надеюсь начинающим пригодится, а продолжающие дадут свои замечания.Создание первого приложения Здесь все максимально просто, как это часто бывает с созданием хелловорлдов. Все (почти) сделают за нас. Перед тем как начать убеждаемся в том, что у нас есть необходимый инструментарий. node --version
v10.24.0 npm --version 6.14.11 npx --version 10.2.2 npx create-react-app hw-app
You can now view hw-app in the browser.
http://localhost:3000 Note that the development build is not optimized. To create a production build, use npm run build. Что бы остановить приложение (если захотим) нажмем Ctr-C. Посмотрим на содержимое папки приложения-проекта. ls
README.md node_modules package-lock.json package.json public src ls public
favicon.ico index.html logo192.png logo512.png manifest.json robots.txt ls src App.css App.js App.test.js index.css index.js logo.svg reportWebVitals.js setupTests.js</pre></code> <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );и отрисует он там компонент App. Чтобы рассмотреть немного ближе, как это работает изменим файлы index.html и App.js. Пусть браузeр в заголовке показывает название именно нашего приложения. Изменим соответствующую строку было<title>React App</title>стало<title>My First React App</title>А компонент App пусть покажет наш контент (содержимое файла App.js) было:import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div>);} стало:function App() { return ( <div> <h1>Hello React!</h1> </div> ); } export default App;также из папки src можно удалить все неиспользуемые файлы в данный момент файлы. Содержимое папки должно быть таким ls src/
App.js index.js Выглядит как настоящий HelloWorld, но останавливаться мы на этом не будем и рассмотрим простые случаи взаимодействия с пользователем, чтобы наш "Hello" не улетел в пустоту. В React мы работаем с компонентами. В данном и самом простом случае компонент - это функция написанная на JavaScript и возвращающая код, похожий на разметку html. Похожий, но являющийся на деле кодом JSX из которого html получается в результате компиляции. Мы не будем вносить изменения в файл App.js пусть он остается корневым компонентом нашего в будущем интерактивного приложения, в котором мы расположим написанные нами компоненты.Не забудем убрать лишние строки из src/index.js, он примет вид import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );Следует сказать, что компоненты могут быть функциональными -- просто функция, возвращающая представление компонента и "классовыми" -- это компонент описываемый JS классом, имеющий внутреннее состояние и дополнительный функционал. Сказанное не означает, что функциональные компоненты не могут иметь внутреннего состояния, но об этом позже. Добавим в наше приложение два компонента, один функциональный и один в виде класса. Оба будут иметь состояния, что-то о себе помнить и взаимодействовать с пользователем. Первый компонент, назовем его ClickCounter, будет считать сколько раз по нему кликнули. Второй - Machine будет представлять собой интерфейс с какой-то машине, которую можно включать выключать, нажимая на кнопку. ClickCounter - функцияПервая итерация. Это содержимое файла ClickCounter.jsconst clickCounter = () => { const clickTimes = 0; return ( <div> <p>Я счетчик кликов</p> <p>Кликнуто {clickTimes} раз</p> </div> ); } export default clickCounter;А вот так изменился App.js.import ClickCounter from './ClickCounter'; function App() { return ( <div> <h1>Hello React!</h1> <ClickCounter /> </div> ); } export default App; В первой строке импортируем наш ClickCounter и добавляем его в отрисовку после тега "Hello React!". И да, новый компонент будет выглядеть в коде как еще один новый тэг. Тут наверное может возникнуть вопрос об именовании компонентов, в файле ClickCounter.js экспортируем clickCounter, в App.js импортируем ClickCounter, что за дела? Мы можем импортировать хоть MySupperPupperClickCounter из './ClickCounter' и использовать его как <MySupperPupperClickCounter /> , но получим все равно вывод функции clickCounter(), которая экспортируется по дефолту. (попробуйте) Итого: после слова import стоит имя компонента которое мы будем использовать далее в файле (в данном случае в файле App.js) после слова from стоит имя файла с относительным путем, но без расширения '.js'. Что же написано в СlickCounter.js? Определена константа с именем clickCounter, которой присваивается '=' функция без параметров '()' выполняющая код написанный в теле '{}', там же определяется переменная clickTimes. Значение этой переменной будет появляться в строке <p>Кликнуто {clickTimes} раз</p>где имя переменной обернуто в фигурные скобки. Помним, что это JSX и после компиляции мы увидим "Кликнуто 0 раз" как на рисунке. Пока еще ничего интересного не происходит, т.к. нет самого подсчета кликов. Реализуем его. Здесь очень подробно и хорошо описана работа с хуками состояния. Я может быть немного повторю, но еще раз про строку const [clickTimes, clickIncrement] = useState(0)
clickUpdater - функция, которая будет обновлять значение переменной состояния своим параметром.Я вынес логику в функцию clickIncrementer(), чтобы показать, что сложную логику (в нашем случае она, конечно, простая) можно описать в отдельной функции и вернуть состояние оттуда.Таким образом файл ClickCounter.js становится таким:import React, { useState } from 'react'; const ClickCounter = () => { const [clickTimes, clickUpdater] = useState(0); const clickIncrementer = () => { return clickTimes + 1; } return ( <div onClick={ () => clickUpdater(clickIncrementer)}> <p>Я счетчик кликов</p> <p>Кликнуто {clickTimes} раз</p> </div> ); } export default ClickCounter;Обратите внимание, ClickCounter мы теперь пишем с большой буквы -- это требование к именованию функциональных компонентов. Теперь, кликая по тексту элемента будем наблюдать возрастающее число кликов.Machine - classПоехали дальше. На очереди компонент класс. Назовем этот класс Machine и опишем в файле Machine.js. Класс компонента обязан реализовать функцию render(), которая будет вызываться для отображения компонента в браузере. Сначала просто нарисуем нужные нам элементы с нужными значениями. Вот полный текст файла. import React from 'react'; class Machine extends React.Component { state = { machineState: 'STOPPED', machineStarted: 0 } render () { return ( <div> <p>Я интерфейс машины.</p> <p>Состояние машины: {this.state.machineState}. <br/> <button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button> </p> Машину запускали {this.state.machineStartCount} раз. </div> ); } } export default Machine;Как видим, render() возвращает не что иное, как JS код, и весь этот код должен быть обрамлен одним <div>(<span> или даже <>), для функционального компонента требование то же. В файл App.js добавим строку для импорта import Machine from './Machine';
<Machine />
import Machine from './Machine'; function App() { return ( <div> <h1>Hello React!</h1> <ClickCounter /> <Machine /> </div> ); } export default App;Что увидим в браузере показано на рисунке. Пока, конечно -- каша. Но совсем скоро мы ее поправим. А пока обсудим содержимое файла Machine.js. В первой строке import React. Обратите внимание, мы не использовали никаких выражений в фигурных скобках. Именно поэтому мы пишем extends React.Component. Если бы написалиimport React, { Component } from 'react' , то можно было бы сказать extends Component. Далее, появилось объявление и инициализация объекта state. Именно в нем будет храниться изменяемая информация (состояние), связанная с объектом класса. Обращаться к этому объекту нужно с использованием указателя this. Понятное дело, что объект state может быть объемнее и сложнее, чем в нашем случае. Теперь у нас есть привязанное к компоненту состояние, которое нужно менять в зависимости от действия пользователя. Действий не много -- нажатие кнопки. В зависимости от того, в каком состоянии находилась машина в момент нажатия кнопки мы изменим состояние таким образом: Состояние машины -- STOPPED, на кнопке написано START.
Состояние машины -- STARTED, на кнопке написано STOP. Счетчик стартов будет считать количество нажатий на кнопку с надписью START. switch (this.state.machineState) { case 'STOPPED': const cnt = this.state.machineStartCount; this.setState({machineState: 'STARTED', buttonLabel: 'STOP', machineStartCount: cnt + 1}); break; case 'STARTED': this.setState({machineState: 'STOPPED', buttonLabel: 'START'}); break; default: break; } }Код простой и пояснений не требует, кроме наверное одного нюанса (я на этом застрял). Речь идет о наличии константы const cnt = this.state.machineStartCount. Почему бы не написать просто machineStartCount: this.state.machineStartCount + 1? Нельзя. Нельзя использовать непосредственно состояние компонента для создания нового состояния. Просто. Также нужно обратить внимание, что в одном случае мы создаем полностью новое состояние, а в другом, только только его части, не обновляем значение счетчика. setState() правильно обработает ситуацию, она обновит указанные поля и оставит не тронутыми те, о которых умолчали.Теперь добавим эту функцию в обработчик клика для кнопки.<button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button>Все работает, но выглядит все еще так себе.Добавим стиляГромко сказано, просто отделим компоненты друг от друга и покажем их границы. Все описание запишем в файл src/App.css и импортируем его в src/App.js строкойimport './App.css';А это содержимое файла App.css : .Component {
margin: 20px; border: 1px solid #eee; box-shadow: 5px 5px 10px #ccc; padding: 10px; } <div className='Component'>
"Пропсы"Не могу понять, почему не использовать 'свойства' или 'параметры', ведь очень похоже на передачу параметров при создании компонента. Может быть, при создании нового сообщества React-истов(еров) потребовался новый слэнг? В общем, следуем официальному сайту. Мы рассмотрели state - структуру, которая хранит информацию о компоненте и которую мы можем изменять в жизненном цикле компонента, изменения инициируются компонентом. Теперь мы рассмотрим props.Это данные, которые родитель компонента может передать дочернему компоненту. "Пропсы" (вроде как устоявшийся термин) не изменяются в течение срока жизни компонента самим компонентом, для него это "константы", но которые компонент может прочитать. Поcмотрим как это работает.Представим себе, что нам нужно создать две машины, два компонента Machine, которые будут отличаться именем-обозначением, в остальном экземпляры компонента будут одинаковы. Добавим дополнительную информацию с использованием props. Создадим в App две машины с именами "Первая машина" и "Вторая машина" следующим образом<Machine name='Первая машина' /> <Machine name='Вторая машина' />name и будет тем самым пропсом. Теперь изменим функцию render() компонента.render () { return ( <div className='Component'> {this.props.name} <p>Я интерфейс машины.</p> <p>Состояние машины: {this.state.machineState}. <br/> <button onClick={this.clickButtonHandler}>{this.state.buttonLabel}</button> </p> Машину запускали {this.state.machineStartCount} раз. </div> ); }Обратим внимание на строку после div. Теперь мы имеем на странице два независимых однотипных компонента с разными состояниями. Смотрим на рисунок. Это самый простой случай использования пропса. В игре "Сапер" рассмотрим еще. Промежуточный итог таков:
Здесь есть два "больших" компонента:
flagCnt: 0, seconds: 0 };Здесь: flagCnt -- счетчик флажков на минном поле, seconds -- прошло секунд с начала игры. Но ведь счетчик флажков и прошедших секунд отображается на компоненте ControlPnael , как же происходит передача данных? Посмотрим. Компонент ControlPnaelВот так Game создает ControlPanel<ControlPanel flagCnt={this.state.flagCnt} seconds={this.state.seconds} />А вот так будет выглядеть код компонента ControlPanel. Он совсем короткий и я привожу его весь. const zeroPad = (num, places) => String(num).padStart(places, '0'); const controlPanel = (props) => { const min = Math.floor(props.seconds / 60); const secs = props.seconds % 60; return ( <div className='Control' style={{color: '#adadad'}} > Flag count:{zeroPad(props.flagCnt)} Time:{zeroPad(min, 2)}:{zeroPad(secs, 2)} </div> ); } export default controlPanel;Функциональный компонент создается с параметром props, Game в пропсах указывает имена переменных (своих переменных) this.state.flagCnt и this.state.seconds, а ControlPanel использует при рендеринге имена пропсов flagCnt и seconds. Просто? Сам компонент не изменяет значения пропсов, за него это делает родительский компонент, причем делает это с использованием setState() (помним, что эта функция используется для изменения state компонента). А так как setState инициирует перерисовку самого компонента и его дочек, то мы увидим изменяющиеся значения на ControlPanel.Как данные спускаются от родительского компонента в дочерний мы увидели, теперь посмотрим как данные "поднимаются" от дочернего компонента к родительскому. Такой фокус происходит при взаимодействии Game и MineField . Взаимодействие Game и MineFieldПри создании компонента MineField используется следующий код в Game :<MineField rows='8' cols='8' mines='10' gameStarted={this.startGame} gameOver={this.stopGame} changeFlagCount={this.setFlag} />Имена пропсов, наверное, говорящие: rows -- количество строк ячеек, cols -- количество колонок, mines -- количество мин, с этими значения MineField построит минное поле. Теперь о передаче данных "наверх". Как Game узнАет о том, что игра началась, что был уже клик по ячейке? Просто, MineField вызовет в своем коде функцию gameSarted -- это имя пропса, но выполнится код функции startGame в пространстве класса Game -- это его функция. Это приведет по цепочке к ежесекундному запуску функции tick(), изменяющей значение seconds, до тех пор пока MineField не вызовет функцию gameOver(true|false), при этом вызовется функция stopGame в классе Game. Game покажет alert с сообщением о выигрыше или проигрыше в зависимости от переданного (поднятого?)из MineField параметра. Вот часть кода Game (собственно, почти весь код): start = () => { this.timerID = setInterval(() => this.tick(), 1000); } stop = () => { clearInterval(this.timerID) } tick() { const oldsec = this.state.seconds; this.setState({seconds: oldsec + 1}); } startGame = () => { this.start(); } stopGame = (isGameWon) => { if (isGameWon) { alert("You win"); } else { alert("You lose"); } this.stop(); }Создание ячеек поля и их взаимодействие с полемФункционал ячеек прост и описывается он в классе с оригинальным названием Cell. Первое и основное, что должна сделать ячейка -- это отрисовать себя в соответствии со своим состоянием: закрыта, открыта, помечена флагом. Тут нужно добавить, что состояние ячейки не является хранимым в ячейке состоянием, а опять же передается ей через пропс от MineField. render () { const cellSize = 40; //px var width = cellSize + 'px'; var height = cellSize + 'px'; var left = this.props.col (cellSize + 4) + 'px'; var top = this.props.row (cellSize + 4) + 'px'; let backgroundColor = this.props.opened ? '#adadad' : '#501b1d'; var rendstate = () => { if (this.props.checked) { return ( <img className='flag' src={flag} alt=''/> ) } if (this.props.opened) { return ( this.props.hasBomb ? <img className='bomb' src={bomb} alt=''/> : (this.props.bombNbr > 0 ? this.props.bombNbr : '')); } } return ( <div className='Cell' style={{width, height, left, top, backgroundColor}} onClick={this.leftClickHandler} onContextMenu={this.rightClickHandler} > {rendstate()} </div> ); }Да, в этой функции много "магических" чисел (40, 4, 4), наверное код можно было бы сделать чище. Но по именам переменных наверное все понятно: cellSize -- длина стороны ячейки, чтобы хоть как-то уменьшить количество безымянных чисел. width, height -- высота ширина top, left -- координаты верхнего левого угла ячейки на поле, вычисляются в зависимости от номера строки и колонки, передаваемых от MineField в пропсах. bomb, flag -- импортированные из файлов рисунки. Итого, чтобы ячейка себя правильно отрисовала MineField передает ей следующие пропсы:
this.props.clickLeft(this.props.row, this.props.col); } rightClickHandler = (e) => { e.preventDefault(); this.props.clickRight(this.props.row, this.props.col); }Как видите, вся работа этих функций заключается только в том, чтобы передать "наверх" координаты ячейки, про которой кликнули. После клика, вызываются переданные в пропсах функции, работа которых произойдет в классе MineField и после обработки результат спустится вниз, назад в ячейку в виде измененного пропса opened или checked.Класс MineFieldЭто самый "функционально насыщенный и сложный" класс, весь код составляет немногим более 200 строк. Класс имеет конструктор, в котором создается массив размера rows x cols содержащий элементы типа cellData . Вот описание cellData.class cellData { constructor (row, col) { this.row = row; this.col = col; this.hasBomb = false; this.checked = false; this.opened = false; this.bombNbr = 0; this.nbrs = [...Array(0)]; } }Эти данные полностью описываю состояние ячейки минного поля и именно они передаются в качестве пропсов при создании ячейки. Я про них уже писал. constructor(props) { super(props); this.closedCells = props.rows * props.cols - props.mines; this.flagCount = 0; this.state = { field: this.createMap(this.props.rows, this.props.cols, this.props.mines), gameState: 'waiting', } }В поле состояния state.field мы сохраняем карту минного поля, создаваемого функцией createMap(this.props.rows, this.props.cols, this.props.mines) . Если посмотреть на код (ссылка на полный код игры в конце статьи) создания минного поля, то можно увидеть может быть не совсем оптимальное наполнение поля минами и расчет соседей с минами и заполнение списка соседей, в общем, несколько проходов по одному и тому же массиву, но читается легко (наверное). Что же, вот и код отрисовки компонента MineField и создания при этом компонентов ячеек с нужными пропсами, считаем при этом что все данные для ячеек лежат в соответствующем массиве: render() { return ( <div className='MineField'> { this.state.field.map(function(row){ return row.map(function (cell) { return ( <Cell row={cell.row} col={cell.col} hasBomb={cell.hasBomb} bombNbr={cell.bombNbr} key={cell.row + "-" + cell.col} checked={cell.checked} opened={cell.opened} clickLeft={this.cellLeftClicked} clickRight={this.cellRightClicked} /> ) }, this); }, this) } </div> ); }В конструкции map не забываем передать указатель на класс, иначе будут недоступны функции класса. Все, игровое поле создано и готово принимать клики мышкой. При клике на ячейку, как мы помним, в конечном итоге вызывается функция из класса MineField , производит манипуляции с минным полем, тем самым массивом, потом обновляем состояние (setState()) , и наблюдаем изменение внешнего вида ячеек, пропсы ведь поменялись, а так же смотрим на запуск счетчика секунд и флажков. Вот не очень сложный код функций обработки кликов: cellLeftClicked = (row, col) => { switch (this.state.gameState) { case 'waiting': this.props.gameStarted(); this.setState({gameState: 'started'}); /* falls through */ case 'started': let newField = [...this.state.field]; this.openCell(newField, row, col); this.setState({field: newField}); break; case 'finished': break; default: break; } } cellRightClicked = (row, col) => { switch (this.state.gameState) { case 'waiting': break; case 'started': if (this.state.field[row][col].opened) { break; } let newField = [...this.state.field]; let flagCntDiff = newField[row][col].checked ? -1 : 1; if ((this.flagCount + flagCntDiff) < 0 || (this.flagCount + flagCntDiff) > this.props.mines) { break; } this.flagCount += flagCntDiff; this.props.changeFlagCount(flagCntDiff); newField[row][col].checked = !newField[row][col].checked; this.setState({field: newField}); break; case 'finished': break; default: break; } }Как видно, игра перемещается из состояния в состояние waiting -> started -> finished . waiting -- это состояние сразу после загрузки страницы. В состояние started мы перемещаемся после открытия первой ячейки, и в состояние finished после открытия ячейки с миной или открытия всех свободных ячеек. За открытие ячеек отвечает функция openCell() , она рекурсивно вызывает себя для открытия соседних ячеек, которые не граничат заминированными ячейками. В состоянии finished мы перестаем реагировать на действия пользователя, игру (страницу) нужно перезагрузить.Еще раз хотелось бы обратить внимание на то, как происходит работа с минным полем -- state.field . В обработчике клика мышкой мы создаём копию поля. Производим с ним необходимые манипуляции и потом, с помощью setState() устанавливаем новое состояние с новым, обновленным полем.Вот и всеПолный код приложений можно найти по ссылке.В этой статье я хотел показать способы взаимодействия, обмена данными между компонентами в приложении React, надеюсь получилось. Хотя могло получиться и так, что какие-то вещи уже очевидные для себя сейчас не рассказал. Я также писал здесь в тексте, что расскажу на примере этой же игры про применение redux , но наверное она (игра) того не стоит. Если статья вызовет интерес, сделаем какую-нибудь инфографику с биржи, поучимся вместе использовать графические библиотеки и вот тут дойдет время для redux , будет к месту, наверное. А теперь, всем всего хорошего! =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_java, #_reactjs |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:09
Часовой пояс: UTC + 5