В настоящее время я изучаю React и в качестве первого проекта пробовал создать пользовательский интерфейс базовой игры-тральщика. Моей целью было написать простые компоненты, которые можно использовать повторно. Я рассмотрю каждый из них и сделаю все возможное, чтобы объяснить их обоснование, а затем объясню, как все они связаны между собой в основном App
составная часть. Пожалуйста, дайте мне знать, можно ли что-то из написанного мной сделать более эффективным или идиоматическим способом.
Как было сказано ранее, я построил UI при использовании React логика игры отделена от кода React. Весь проект можно увидеть на github и в эту игру можно играть на моем сайт neocities. И последнее уточнение: для удобства чтения следующее jsx код.
Компонент Picker
Компонент выбора используется для создания группы помеченных переключателей. Он имеет функцию checkedRadio(groupName)
который возвращает html value
атрибут выбранного варианта. Обратите внимание, что я визуализирую коллекцию элементов без использования key
, вот почему:
- каждая радиокнопка имеет
id
атрибут, чтобы он работал с htmllabel
, Я хочу добавить уникальныйkey
будет лишним - список переключателей не должен меняться с течением времени, что означает отсутствие повторного рендеринга и, следовательно, отсутствие необходимости в
key
/ * Компонент выбора: * Возвращает группу переключателей. * Ожидаемые свойства: * - name: имя html для группы переключателей * - descriptions: последовательность строк, каждая из которых описывает параметр в * группе * - defaultPick: индекс описания для выбранного параметра по умолчанию * - onChange: функция, вызываемая при включении переключателя, изменяется, эта * функция может принимать в качестве аргумента объект формы * {description, indexOfDescription} * - radioClass: имя класса для каждой опции средства выбора * Дополнительные реквизиты будут прикреплены в оборачивающий div. * / const Picker = function ({name, descriptions, defaultPick, onChange, radioClass, ... rest}) {return ( {descriptions.map ((description, i) => {return ( onChange ({description, i})} /> );}}; )} ); } / * Возвращает значение переключателя, отмеченного в группе переключателей * с именем groupName. * Выдает ошибку, если такая группа не найдена. * / const checkedRadio = function (groupName) {const grp = document.querySelectorAll (`input[name=${groupName}`);
for (const node of grp)
if (node.checked)
return node.value;
throw new Error(`no radio group of name ${ groupName }`);
}
The Matrix component
This component is used to render a rectangular 2D array of data. It uses a Row
component to render lines of cells. How each cell is render is up to the user, and a cellComponent
should be provided for both of these components to work as intended. Here as well, I am not using any key
, it is up to the user to give a key
(or not) to their cells. Given that the user can make their cells have key
s, I figured that Row
elements don’t need any key
of their own.
/* Matrix component:
* Return a table-like element.
* Expected props:
* - array: the 2D array that must be translated into a pseudo-table
* - rowClass: the class name for every row in the Matrix
* - onClick, onContextMenu: functions to be called when a cell of the
* Matrix is respectively left-clicked or right-clicked
* - cellComponent: a component to be instanciated for every cell of the
* given 2D array. Such a component should accept props of the form
* { cell, x, y, onClick, onContextMenu }
* Additional props will be attached to the wrapping div.
*/
const Matrix = function({array, rowClass, onClick, onContextMenu, cellComponent, ...rest}) {
return (
<div {...rest}>
{array.map((row, y) => {
return (
<Row
className={rowClass}
row={row}
y={y}
onClick={onClick}
onContextMenu={onContextMenu}
cellComponent={cellComponent} />
);
})}
</div>
);
}
/* Row component:
* Return a flat sequence of cells. This component is meant to be instanciated
* by the Matrix component.
* Expected props:
* - row: the flat (1D) array that must be translated into a Row
* - y: the number of the Row in the Matrix
* - onClick, onContextMenu: functions to be called when a cell of the
* Row is respectively left-clicked or right-clicked
* - cellComponent: a component to be instanciated for every cell of the
* given 2D array. Such a component should accept props of the form
* { cell, x, y, onClick, onContextMenu }
* Additional props will be attached to the wrapping div.
*/
const Row = function({row, y, onClick, onContextMenu, cellComponent, ...rest}) {
return (
<div {...rest}>
{row.map((cell, x) => cellComponent({x, y, onClick, onContextMenu, cell}))}
</div>
);
}
My own cellComponent
: the mineCell component
This is the cellComponent
that is meant to be called by the Matrix
component. It is meant to represent cells in a minesweeper game, and therefore relies more on the logic part (which isn’t covered here). mines.UNREV
, mines.BOMB
and mines.FLAG
are constants defined in the logic part that are used to differentiate between kinds of cell, here they are used to apply different styles to different cells. Still in the logic part, each cell can be represented be a single character, which is the actual cell
argument passed to the mineCell
component.
// Map to determine the CSS class of a cell based on its value
const cellClasses = new Map(
[[mines.UNREV, 'tile-unrevealed'],
[mines.BOMB, 'tile-bomb'],
[mines.FLAG, 'tile-flag']]); // Компонент ячейки для игры «Сапер» const mineCell = function ({x, y, onClick, onContextMenu, cell}) {let className = cellClasses.get (cell); return ( onClick (x, y)} onContextMenu = {(evt) => {evt.preventDefault (); onContextMenu (x, y)}}> {ячейка} ); }
Где все связано: компонент приложения
Давайте посмотрим на render
функция. Он использует Picker
элемент (с трудностями, определенными в логической части как descriptions
), чтобы пользователь мог выбрать уровень сложности и Matrix
элемент для рендеринга всех ячеек игры.
// labels for the difficulty settings
const descriptions = Array.from(mines.difficulties.keys());
// Map to determine the reset button css class depending on the minefield state
const resetClasses = new Map(
[[mines.WON, 'reset-won'],
[mines.LOST, 'reset-lost'],
[mines.PLAYING, 'reset-normal']]
);
App.prototype.render = function() {
const minefield = this.state.minefield;
let resetButtonClass = resetClasses.get(minefield.state);
return (
<div id='app'>
<Picker
id='picker'
descriptions={descriptions}
name="difficulty"
defaultPick={1}
onChange={() => this.changeDifficulty()}
radioClass="radio-button"/>
<button
id='reset-button'
className={resetButtonClass}
onClick={() => this.resetGame()}
alt="button to reset the game">
</button>
<Matrix
id='game'
array={minefield.view}
rowClass="mines-row"
onClick={(x, y) => this.mineviewLeftClick(x, y)}
onContextMenu={(x, y) => this.mineviewRightClick(x, y)}
cellComponent={mineCell}/>
</div>
);
}
Вот как App
компонент определен. Выбранную сложность можно восстановить в любой момент, выяснив, какой вариант отмечен в сложности. Picker
. В state
только содержит Minefield
пример. (Быстрая точность о Minefield
конструктор, определенный в логической части: он требует позиции для создания экземпляра любого объекта, чтобы первая щелкнутая ячейка не была бомбой. По этой причине все, что отображается перед первым щелчком в игре, на самом деле является пустым минным полем.)
// Dummy minefield used when the game hasn't started yet
const DUMMY = 'du';
const dummyMinefield = function([width, height, _]) {
return {
view: Array(height).fill(Array(width).fill(mines.UNREV)),
state: DUMMY
};
}
const App = function(props) {
React.Component.call(this, props);
Object.defineProperty(this, 'difficulty', {
enumerable: true,
get: function() { return checkedRadio('difficulty'); }
});
this.state = {
minefield: dummyMinefield(mines.difficulties.get(descriptions[1])),
};
}
App.prototype = Object.create(React.Component.prototype);
App.prototype.constructor = App;
Остальное — это разные функции: изменение сложности, запуск игры заново, нажатие на ячейку, установка флажка на ячейку.
// Change the difficulty picked
App.prototype.changeDifficulty = function() {
this.setState({
minefield: dummyMinefield(mines.difficulties.get(this.difficulty)),
});
}
// Reset the on-going game of minesweeper
App.prototype.resetGame = function() {
this.setState({
minefield: dummyMinefield(mines.difficulties.get(this.difficulty)),
});
}
// Handle click events on the Matrix component that represents the minefield
/* Handle left click events
* Reveal the minefield's cell at coordinates x, y if the minefield is not a dummy.
* Create a minefield otherwise
*/
App.prototype.mineviewLeftClick = function(x, y) {
if (this.state.minefield.state===DUMMY) {
const [width, height, bombs] = mines.difficulties.get(this.difficulty);
const minefield = new mines.Minefield(width, height, [x, y], bombs);
this.setState({minefield});
}
else {
const minefield = this.state.minefield.reveal([x, y]);
this.setState({minefield});
}
}
/* Handle right click events
* Flag the minefield's cell at coordinates x, y if the minefield is not a dummy.
*/
App.prototype.mineviewRightClick = function(x, y) {
if (this.state.minefield.state!==DUMMY) {
const minefield = this.state.minefield.flag([x, y]);
this.setState({minefield});
}
}
Вот и все, я надеюсь, что он не будет длинным или слишком расплывчатым, и я с нетерпением жду совета о том, как написать более идиоматический код React.
1 ответ
Использовать class
является вместо создания function
это вручную наследуется от React.Component
. Это также позволит вам более точно определять свойства и методы. Например:
class App extends React.Component {
state = {
minefield: dummyMinefield(mines.difficulties.get(descriptions[1])),
};
get difficulty() {
return checkedRadio('difficulty');
}
changeDifficulty() {
this.setState({
minefield: dummyMinefield(mines.difficulties.get(this.difficulty)),
});
}
// ...
Классы намного лучше подходят для организации кода, чем function
s и присвоение .prototype
.
Или еще лучше:
Рассмотрим функциональные компоненты — Реагировать рекомендует попробовать функциональные компоненты и хуки вместо компонентов класса в новом коде. Для меня работа и жизненный цикл функциональных компонентов более интуитивно понятны, чем для компонентов классов. Если вы попробуете их, то обнаружите, что они упрощают обслуживание.
Избегайте собственных методов DOM когда возможно — с React, когда вы видите использование собственного метода DOM, например querySelector
(или же .children
или же .style
и т. д.), сделайте шаг назад и подумайте, есть ли другой способ. Как правило, использования таких методов можно избежать, используя вместо них методы состояния и рендеринга React.
Единственная часть этой проблемы связана с выбором сложности. В идеале checkedRadio
функция будет полностью удалена. (Если вы все равно решите сохранить его, по крайней мере, измените имя на что-нибудь вроде getCheckedRadioName
— переменная не держать установленный переключатель, это возвращается проверенное название радио)
Вместо этого поместите сложность в состояние приложения и вместо этого сделайте Picker управляемым компонентом (чтобы изменения выбора переходили в состояние). Передайте значение сложности и установите средство выбора. Что-то вроде этого:
const App = () => {
// these get passed as selectedDescription and setSelectedDescription
const [difficulty, setDifficulty] = useState('EASY');
const Picker = function({name, descriptions, selectedDescription, setSelectedDescription, radioClass, ...rest}) {
return (
<div {...rest}>
{descriptions.map((description) => {
return (
<span className={radioClass}>
<input type="radio"
name={name}
value={description}
id={`${ name }-${ description }`}
checked={description === selectedDescription}
onChange={() => setSelectedDescription(description)} />
<label htmlFor={`${ name }-${ description }`}>{description}</label>
</span>);
})}
</div>
);
}
Остальные параметры В компоненте очень много реквизита. Вместо того, чтобы допускать столько дополнительных свойств, сколько хочет вызывающий, вы можете рассмотреть возможность использования не замужем опора вместо rest
, и дайте ему информативное имя. Может быть:
, radioClass, pickerContainerProps = {}}) {
return (
<div {...pickerContainerProps}>
имя Я не считать то name
prop необходимо передать — это деталь реализации, о которой, в конце концов, заботится только Picker и только для целей группировки радиостанций. Может, вместо этого Пикер придумал имя?
const inputNameRef = useRef();
if (!inputNameRef.current) {
inputNameRef.current = makeRandomString();
}
// proceed to use `inputNameRef.current` instead of `name`
Вы также можете избежать id
s и htmlFor
сделав <input>
дитя <label>
.
JSDoc Стандартный способ документирования функций в JS — это JSDoc или аналогичный формат. Напротив, используя что-то вроде
/* Picker component:
* Return a group of radio buttons.
* Expected props:
* - name: the html name for the group of radio buttons
в то время как конечно полезный, не совсем соответствует ожидаемому формату для хороших IDE, чтобы воспользоваться им. Статья с примерами.
Вот небольшой пример того, как VSCode может информировать разработчика об ожидаемых реквизитах, помещая ваши комментарии в формат JSDoc:
Рассмотрите возможность использования простых стрелочных функций везде вместо function
s — стрелочные функции — гораздо более распространенное соглашение в React. Они немного более краткие из-за отсутствия function
и их способность неявно возвращаться.
Распространение реквизита и используйте сокращенные свойства, если хотите — когда вам нужно только перечислить каждую опору один раз при рендеринге, а не дважды, это на один меньше возможностей для проявления проблемы, связанной с опечаткой. Например, это:
<Row
className={rowClass}
row={row}
y={y}
onClick={onClick}
onContextMenu={onContextMenu}
cellComponent={cellComponent}
/>
может быть:
<Row
className={rowClass}
{...{
row,
y,
onClick,
onContextMenu,
cellComponent
}}
/>
(вы также можете использовать объектный отдых, чтобы собрать все это, но className
в один объект в списке параметров Matrix
)
Использовать const
вместо let
— видеть Вот. Особенно в React, где функциональное программирование является предпочтительным (а иногда и необходимым), полезно указать читателю кода, что даже возможность переназначения невозможно. И то и другое let
использование, которое я вижу, можно заменить на const
.
Разрушение, если хочешь. Надеюсь, вы будете использовать функциональные компоненты вместо компонентов класса, но если нет, вы можете изменить что-то вроде этого:
const minefield = this.state.minefield;
к
const { minefield } = this.state;
Текст ячейки Когда в ячейке есть видимая мина, текст ячейки становится «X». Когда на ячейке есть видимый флаг, текст ячейки становится «P». Эти тексты, обычно невидимые, становятся видимыми, если доска выделена. То же самое для непосещенных ячеек, в которых .
с. Подумайте о том, чтобы не отображать эти тексты вообще — отображайте текст только в том случае, если текст является числом.
Подсказка лица У вас есть
alt="button to reset the game"
Некоторые люди могут быть не знакомы с пользовательским интерфейсом Minesweeper. Также подумайте о добавлении всплывающей подсказки, чтобы кто-нибудь, наведя курсор на лицо, мог видеть, что на нее можно нажать. Может быть, добавить :hover
эффект тоже.
Если вы хотите сделать что-то необычное, вы также можете изменить лицо на нерешительное лицо «O», когда мышь нажимается на ячейку, но еще не отпускается.
Сделать перетаскивание сложнее случайно Очень и очень легко случайно перетащить основной интерфейс, щелкнув вниз в любом месте внутри интерфейса, а затем перетаскивая. Это делает невозможным пересмотр своего выбора, если человек нажимает на ячейку, но еще не отпускает мышь. Рассмотрите возможность перетаскивания только с .close-bar
на вершине.