前言
在之前实现的 Rax.js+Ts+ESlint「旅游官网」实战项目 的基础上,利用 React 官方项目例子 GoBang 井字棋小游戏 来进一步熟悉 Rax 框架。
该井字棋小游戏例子在官方文档中实例已经写得很清楚详细,本文就不稍加介绍了嘿,主要是将该小游戏以 Rax 进行重构,并且使用 function 组件和 hooks 的方式进行项目案例的更新替代。
实现功能效果如下:
- tic-tac-toe(三连棋)游戏的所有功能
- 能够判定玩家何时获胜
- 能够记录游戏进程
- 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态
而本篇文章的目标是进阶优化完善升级小游戏项目的功能点:
- 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
- 在历史记录列表中加粗显示当前选择的项目。
- 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
- 添加一个可以升序或降序显示历史记录的按钮。
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
- 当无人获胜时,显示一个平局的消息。
项目
Gobang 小游戏嵌入旅游项目
重点:利用 Rax 的路由进行跳转。
路由配置
/src/app.json
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
},
{
"path": "/gobang",
"source": "pages/Gobang/index"
}
],
"window": {
"title": "柃木🎈"
}
}
旅游导航
/src/components/Header.tsx
import { history } from 'rax-app';
// ...代码省略
// 在 {/* navMenu */} 中加入 Gobang 导航
<div className={styles.navLink} key="gobang" onClick={() => history.push('gobang')}>
Gobang
</div>
Gobang 导航
Gobang 返回旅游页面
/src/pages/Gobang/index.tsx
import { history as router } from 'rax-app';
// ... 省略
// 添加返回旅游页面代码
<div style={{ textAlign: 'center' }} className="button buttonPrimary buttonBig buttonNoRound" onClick={() => router.push('/')}>
返回旅游页面
</div>
添加棋子坐标
重点:利用棋盘点击事件上的 i 来确定当前棋子坐标。
Gobang 页面
/src/pages/Gobang/index.tsx
// 给 history 对象状态添加 nowKey 属性,记录当前坐标。
const [history, setHistory] = useState([
{
squares: Array(9).fill(null),
nowKey: '',
},
]);
// 在 handleClick 事件中根据 `i` 计算出当前棋子的坐标,并调用 effect 事件更新记录。
const handleClick = (i: string | number) => {
const nowKey = [(+i % 3) + 1, Math.floor((+i / 3)) + 1].join(',');
// ...省略代码
const historyTemp = [...curHistory, { squares, nowKey }];
setHistory(historyTemp);
}
// 在游戏历史记录列表显示每一步棋的坐标
const moves = history.map((step, move) => {
const desc = move ? `Go to move #${move} \n Chess position: (${step.nowKey})` : 'Go to game start';
// ...省略代码
});
需要更新一个 li 的外边距以及 li > button 的宽高样式。
显示当前选择的历史项目
重点:历史项目上样式的 focus,伪元素和伪类是可以直接套用添加。
Gobang 样式
/src/pages/Gobang/index.module.css
li > button {
font-size: 16px;
width: 160px;
height: 100%;
position: relative;
}
li > button:focus:before {
content: "";
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 4px solid gold;
transition: all 0.5s;
animation: clipPath 3s infinite linear;
}
@keyframes clipPath {
0%,
100% {
clip-path: inset(0 0 95% 0);
}
25% {
clip-path: inset(0 95% 0 0);
}
50% {
clip-path: inset(95% 0 0 0);
}
75% {
clip-path: inset(0 0 0 95%);
}
}
循环渲染格子
重点:Array.map() 和对应的 key 值
Board 棋盘
/src/components/Board/index.tsx
// 省略代码
// 给格子添加 key 值
<Square key={i} value={props.squares[i]} onClick={() => props.onClick(i)} />;
<View>
// 使用两个循环来渲染出棋盘的格子
{[0, 1, 2].map((v) => {
return (
<div key={v} className={styles.boardRow}>
{[NaN, NaN, NaN].map((v2, i) => {
return (renderSquare(v * 3 + i));
})};
</div>
);
})}
</View>
- 注意不能使用
Array(3)来初始化空数组,当空数组时不能被渲染出来- key 值不能是 index 索引
- 注意传入方法
renderSquare的值与外层循环的索引值的关系
排序历史记录
重点:设置 sort 状态,并且调用 Array.reverse() 调转历史记录数组
Gobang 页面
/src/pages/Gobang/index.tsx
// 设置 sort 状态
const [sort, setSort] = useState(false);
// 判断是否需要调转历史记录数组
sort && moves.reverse();
// ... 省略代码
// 排序按钮
<div
style={{ textAlign: 'center', width: '100px' }}
className="button buttonPrimary buttonNoBig buttonRound"
onClick={() => setSort(!sort)}
>
排序
</div>
标识获胜棋子
重点:获胜时,获取到获胜棋子的编号,通过 prop 传递到棋子 Square 判断改变棋子样式
样式
/src/global.css
/* 设置获胜样式 */
.winner {
background: firebrick !important;
color: white;
}
注意需要
!important否则优先级样式被覆盖。
获胜棋子编号
/src/utils/index.ts
// 判断获胜棋子方法中
export function calculateWinner(squares: string[]) {
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]) {
// 更改这一步,返回一个对象,包含获取棋子的编号 square
return { winner: squares[a], square: lines[i] };
}
}
return null;
}
/src/pages/Gobang/index.tsx
更改判断 winner 的条件和传递获胜棋子数组
- if (calculateWinner(squares) || squares[i]) {
+ if (calculateWinner(squares)?.winner || squares[i]) {
return;
}
// 当前棋盘
const current = history[stepNumber];
- const winner = calculateWinner(current.squares);
+ const calculate = calculateWinner(current.squares);
+ const winner = calculate?.winner;
+ let winSquare: number[] | undefined;
// 判断 winner 条件
if (winner) {
status = `Winner: ${winner}`;
+ winSquare = calculate?.square;
// ... 省略代码
// 传递 props
<div className={styles.gameBoard}>
- <Board squares={current.squares} onClick={(i) => handleClick(i)} />
+ <Board squares={current.squares} winner={winSquare} onClick={(i) => handleClick(i)} />
</div>
棋盘判断获胜
/src/components/Board/index.tsx
- function Board(props: { squares: { [x: string]: any }; onClick: (arg0: any) => any }) {
+ function Board(props: { squares: { [x: string]: any }; winner?: number[], onClick: (arg0: any) => any }) {
const renderSquare = (i: number) => {
- return <Square key={i} value={props.squares[i]} onClick={() => props.onClick(i)} />;
+ const winner = props.winner?.includes(i);
+ return <Square key={i} winner={winner} value={props.squares[i]} onClick={() => props.onClick(i)} />;
};
// ... 省略代码
棋格渲染
/src/components/Square/index.tsx
- function Square(props: { value: number, onClick: () => void; }) {
+ function Square(props: { value: number, winner?: boolean, onClick: () => void; }) {
return (
- <button className={styles.square} onClick={() => props.onClick()}>
+ <button className={`${props.winner ? 'winner' : ''} ${styles.square}`} onClick={() => props.onClick()}>
{props.value}
</button>
);
}
平局
重点:很简单,判断一下历史条目的长度,改变显示内容
Gobang 页面
/src/pages/Gobang/index.tsx
if (winner) {
status = `Winner: ${winner}`;
winSquare = calculate?.square;
// 判断长度,显示平局内容。
} else if (moves.length === 10) {
status = 'Draw, start over game';
} else {
status = `Next player: ${xIsNext ? 'X' : 'O'}`;
}
项目总览
以上就是 Rax.js 重构 React 官方项目例子 GoBang 井字棋小游戏 并且实现进阶功能和优化项目的具体内容。下面简单进行项目总览:
后言
目前个人对于 React.js 和 Rax.js 的基础知识学习暂时到这里,后边在实际项目中成长,争取学习后继续与小友们总结分享~
若学习过程有改进的地方,希望有大佬能给我指点一二。