در بخش اول آموزش ساخت بازی XO با استفاده از React، کامپوننت های Board و Square را ایجاد کردیم و دیدیم که چطور با استفاده از کامپوننت ها می توانیم UI مورد نظر خود را به قسمت های مختلف و مجزا از هم تقسیم کنیم و آن ها را توسعه دهیم. مباحث State و Props را شروع و بررسی کردیم که داده ها بین کامپوننت ها به چه صورتی در جریان هستند و کاربرد هر یک از آنها را مشاهده کردید.
حالا باید ویژیگی های بیشتری را به بازی اضافه و آن را تکمیل کنیم.
در ادامه مطلب همراه من باشید.
اعلام برنده
در قسمت قبل بررسی کردیم چطور نوبت بازیکن بعدی مشخص می شود، حالا باید برنده بازی را تعیین کنیم و پس از آن دیگر حرکتی در بازی انجام نشود. می توانیم با استفاده از تابع زیر که به انتهای کامپوننت Board اضافه می کنیم برنده بازی را مشخص کنیم.
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
ما تابع calculateWinner(squares)
را برای بررسی اینکه کدام بازیکن برنده است در متد render کامپوننت Board فراخوانی می کنیم. اگر بازیکنی برنده باشد متن “Winner: X” یا “Winner: O” به نمایش در می آید. در متد render کامپوننت Board تغییرات زیر را اعمال می کنیم.
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// the rest has not changed
همچنین متد handleClick در کامپوننت Board را به صورت زیر تغییر می دهیم. اگر برنده بازی مشخص شده باشد یا خانه ای که بازیکن روی آن کلیک کرده است پر شده باشد با استفاده از به کارگیری return ادامه کار تابع متوقف می شود.
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
ذخیره تاریخچه حرکات بازی
به عنوان آخرین ویژیگی قصد داریم تا بازگشت به حرکات قبل را در بازی پیاده سازی کنیم.
اگر ما با آرایه squares به صورت تغییر پذیر کار می کردیم و تغییرات خود را به صورت مستقیم در آن اعمال می کردیم، پیاده سازی Time travel پیچیده تر بود. تا اینجا ما با استفاده از تابع slice پس از هر حرکت در بازی، یک کپی از آرایه squares ایجاد کردیم و به صورت تغییر ناپذیر با آن کار کردیم که به ما این امکان را می دهد تا نسخه های قبلی آرایه squares را ذخیره و از آنها برای بازگشت به حرکت قبل استفاده کنیم.
در اینجا باید آرایه squares را در آرایه ای دیگر به نام history قرار دهیم. آرایه history همه state های کامپوننت Board را از ابتدا تا انتها در خود نگهداری می کند و شکل آرایه هم به صورت زیر خواهد بود:
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
حالا زمان تصمیم گیری در مورد محل نگهداری آرایه history است و اینکه این آرایه باید در کدام کامپوننت قرار بگیرد.
انتقال State به سطح بالاتر
کامپوننت Game را برای نمایش لیست حرکات قبلی ایجاد می کنیم و برای انجام این کار نیاز به آرایه history خواهیم داشت بنابراین ما آرایه history را در بالاترین سطح یعنی کامپوننت Game قرار می دهیم.
با قرار گیری آرایه history در کامپوننت Game می توانیم آرایه squares را از کامپوننت فرزندش یعنی Board حذف کنیم. همانند ارسال State از کامپوننت Square به کامپوننت Board، حالا State را از Board به سطح بالاتر یعنی کامپوننت Game منتقل می کنیم و State را از Board حذف می کنیم. این کار باعث می شود تا کامپوننت Game کنترل کاملی بر داده های کامپوننت Board داشته باشد و این امکان را به کامپوننت Game می دهد تا از طریق کامپوننت Board، حرکات قبلی را از history خوانده و آن را render مجدد کند.
ابتدا در متد constructor کامپوننت Game، state اولیه را تنظیم می کنیم:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
در حال حاضر کامپوننت Board، prop های squares و onClick را از کامپوننت Game دریافت می کند. به دلیل اینکه ما یک click handler برای Square ها داریم، باید موقعیت هر Square را به onClick ارسال کنیم تا مشخص کنیم که کدامیک از Square ها کلیک شده است. در اینجا مراحل لازم برای تغییر کامپوننت Board را بررسی می کنیم:
-
حذف متد constructor از کامپوننت Board
-
جایگزین کردن
this.state.squares[i]
به جایthis.props.squares[i]
در متد renderSquare کامپوننت Board -
جایگزین کردن
this.props.onClick(i)
به جایthis.handleClick(i)
در متد renderSquare کامپوننت Board
پس از اعمال تغییرات، کامپوننت Borad به صورت زیر خواهد بود:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
برای استفاده از تاریخچه جهت تشخیص و نمایش وضعیت بازی، متد render کامپوننت Game را به صورت زیر تغییر می دهیم:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
پس از انتقال نمایش وضعیت بازی به متد render در کامپوننت ،Game می توانیم کد نمایش وضعیت بازی را از متد render کامپوننت Board حذف کنیم. پس از این کار متد render کامپوننت Board به صورت زیر خواهد بود:
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
در آخر ما متد handleClick را از کامپوننت Board به کامپوننت Game منتقل می کنیم. همچنین باید تغییراتی را در متد handleClick اعمال کنیم زیرا ساختار state در کامپوننت Game تغییر کرده است. در متد handleClick کامپوننت Game تاریخچه جدید را با استفاده از تابع concat به آرایه history الصاق می کنیم.
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
نکته:
به جای استفاده از تابع push() از تابع concat() استفاده می کنیم تا آرایه مورد نظر تغییر ناپذیر باقی بماند و به صورت مستقیم داده های آرایه تغییر نکند.
تا اینجا متد های renderSquare و render در کامپوننت Board و state بازی و متد handleClick در کامپوننت Game قرار دارد.
نمایش حرکات قبلی
با توجه به اینکه تاریخچه بازی را ثبت کردیم، می توانیم لیست حرکات انجام شده بازیکن ها را نمایش دهیم.
پیش تر بررسی کردیم که می توان با عناصر React به صورت یک شی جاوااسکریپت برخورد کرد و از آن در سرتاسر اپلیکیشن استفاده کرد. برای render کردن چندین آیتم در React می توانیم از یک آرایه که شامل عناصر React می باشد استفاده کنیم.
آرایه ها در جاوااسکریپت دارای یک متد به نام map هستند که معمولا برای تبدیل کردن یک فرم داده به فرم دیگر استفاده می شود. به طور مثال:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
در اینجا می توانیم با استفاده از متد map، تاریخچه تغییرات را به button هایی که وظیفه آنها برگشت حالت بازی به مراحل قبل تر است تبدیل کنیم.
حالا آرایه history را در متد render کامپوننت Game، map می کنیم:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
برای هر حرکت در تاریخچه بازی ما یک <li>
که شامل یک <button>
است ایجاد کردیم. هر button دارای یک onClick handler است که متد this.jumpsTo()
را فراخوانی می کند که ما هنوز این متد را پیاده سازی نکردیم. در حال حاضر ما باید یک لیست از حرکت های انجام شده در بازی را مشاهده کنیم. البته یک اخطار با این مضمون که هر عنصر موجود در آرایه باید دارای کلید باشد در قسمت developer tools ایجاد می شود و در ادامه در مورد این اخطار بیشتر بحث می کنیم.
انتخاب کلید
زمانی که یک لیست را render می کنیم، React اطلاعاتی را در ارتباط با آیتم های لیست render شده ذخیره می کند. زمانی که یک لیست را به روز رسانی می کنیم، React نیاز دارد تا تشخیص دهد چه تغییری رخ داده است. در نظر بگیرید که کاربر می تواند آیتم های لیست را اضافه، حذف، به روزرسانی یا ترتیب آن را تغییر دهد.
به طور مثال تغییر زیر را در نظر بگیرید:
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
به
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
علاوه بر به روزرسانی شمارنده، شاید کاربر بخواهد ترتیب Alexa و Ben را جا به جا کند و Claudia را بین این دو قرار دهد. با این حال React یک برنامه کامپیوتری است و چیزی در ارتباط با هدف ما نمی داند. ما باید یک کلید برای هر آیتم لیست تعریف کنیم تا از یک دیگر متمایز باشند. یک گزینه می تواند استفاده از رشته های Alexa، Ben و Claudia باشد. اگر داده ها را از database بخوانیم id های Alexa، Ben و Claudia می تواند به عنوان کلید در نظر گرفته شوند.
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
زمانی که یک لیست render مجدد می شود، React کلید هر آیتم لیست را دریافت می کند و آیتم های لیست قبلی را برای تطابق کلید جست و جو می کند. اگر لیست جاری دارای کلیدی باشد که قبلا وجود نداشته، React یک آیتم جدید ایجاد می کند. اگر لیست جاری فاقد کلیدی باشد که در لیست قبلی موجود بوده، React، آیتم قبلی را حذف می کند. اگر دو کلید با هم تطابق داشته باشند، آیتم مربوطه منتقل می شود. کلید ها باعث می شوند تا هر آیتم هویت مشخص داشته باشد و این به React امکان می دهد تا حالت را بین render های مجدد حفظ کند.اکر کلید آیتم تغییر کند، آیتم حذف می شود و با یک state جدید ایجاد می شود.
Key یک ویژیگی رزرو شده در React است (مانند ref که یک ویژیگی پیشرفته تر است). React زمانی که یک عنصر ایجاد می شود، ویژگی key را دریافت می کند و به طور مستقیم به عنصر اختصاص می دهد. هر چند که به نظر می رسد key از props قابل دسترسی است اما key نمی تواند به صورت this.props.key فراخوانی شود. React به صورت خودکار از key برای تصمیم گیری در مورد اینکه کدام کامپوننت ها باید به روز رسانی شود استفاده می کند. یک کامپوننت از کلید اختصاص داده شده به خودش اطلاعی ندارد.
پیشنهاد می کنیم در زمان ساخت لیست های پویا از کلید های مناسب استفاده کنید.
اگر هیچ کلیدی مشخص نشود، React اخطار می دهد و به طور پیش فرض از index آرایه به عنوان کلید استفاده می کند. استفاده از index آرایه زمانی که می خواهیم آرایه را مجدد مرتب کنیم یا آیتمی را اضافه یا حذف کنیم باعث ایجاد مشکل می شود. اگر هم کلید را به صورت key={i}
مشخص کنیم با اینکه اخطاری ایجاد نمی شود اما باعث ایجاد مشکل های مشابه با تعیین index آرایه به عنوان کلید می شود و در اغلب موارد توصیه نمی شود.
کلید ها نیازی به اینکه به صورت کلی منحصر به فرد باشند ندارند، آنها فقط باید در سطحی که هستند (به عنوان مثال در یک آرایه) منحصر به فرد باشند.
پیاده سازی تاریخچه بازی
در تاریخچه بازی XO هر حرکتی که انجام شده است دارای یک ID منحصر به فرد و مرتبط با خودش است که در واقع عدد متوالی حرکت ها می باشد. حرکات هیچ وقت حذف یا در میانه لیست اضافه یا مجدد مرتب نمی شوند بنابراین استفاده از index حرکت به عنوان کلید قابل اطمینان می باشد.
در متد render کامپوننت Game می توانیم کلید را به صورت <li key={move}>
اضافه کنیم و اخطار ایجاد شده توسط React را برطرف کنیم.
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
با کلیک روی آیتم های button لیست یک خطا مبنی بر اینکه متد jumpTo تعریف نشده است نمایش داده می شود. اما قبل از پیاده سازی jumpTo ما stepNumber را به state کامپوننت Game اضافه می کنیم تا مشخص کنیم در حال حاضر در کدام گام قرار داریم.
ابتدا stepNumber : 0
را به state اولیه در constructor کامپوننت Game اضافه می کنیم:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
سپس متد jumpTo را برای بروزرسانی stepNumber در کامپوننت Game تعریف می کنیم. ما همچنین مقدار xIsNext را با توجه به مقدار stepNumber تغییر می دهیم به این صورت که اگر مقدار stepNumber زوج بود xIsNext برابر با true و در غیر اینصورت برابر با false خواهد بود.
handleClick(i) {
// this method has not changed
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
// this method has not changed
}
حالا باید چند تغییر در متد handleClick کامپوننت Game که زمان کلیک کاربر روی Square اجرا می شود اعمال کنیم.
stepNumber که در state اضافه کردیم تعداد حرکات را به کاربر نشان می دهد. پس از اینکه یک حرکت جدید انجام شد، نیاز داریم تا stepNumber را که بخشی از this.setState می باشد با اضافه کردن stepNumber: history.length
به روز رسانی کنیم.
همچنین خواندن history را که به صورت this.state.history
به this.state.history.slice(0, this.state.stepNumber + 1)
تغییر می دهیم:
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
در آخر متد render کامپوننت Game را از حالتی که همیشه آخرین حرکت را render می کند به حالتی که تغییر انتخابی را طبق stepNumber، render کند تغییر می دهیم:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
// the rest has not changed
اگر روی هر گام از تاریخچه بازی کلیک کنیم، Board بازی باید بلافاصله به شکلی که در آن گام بوده است به روز شود.
می توانید فایل های آموزش را از این لینک دریافت کنید.
منبع: reactjs.org
React | Javascript |