React基础知识点开发井字游戏

我们一起用 React 开发一个井字棋(tic-tac-toe)。游戏规则,在九宫格内输入棋子“X”或“O”,首先在纵向、横向或斜方向上三个同一棋子连成一线为胜出。 通过这个简单的棋盘游戏我们学习下面这些知识点;

  • 环境搭建 create-react-app

  • 介绍React的基础知识:组件、props和state

  • React开发过程最常用的技术

  • 时间旅行 深刻了解React的独特优势

1、环境搭建

环境搭建流程如下:
1、确保安装了较新版本的Node.js。
2、按照Create React App安装指南创建一个新项目,步骤如下:
2.1 执行指令:npx create-react-app tic-tac-toe(项目名称)
2.2 执行指令:cd tic-tac-toe
2.3 执行指令:npm install
3、在工程项目中src/文件夹下创建index.css文件
4、保留原工程项目中src/文件夹下的index.js文件</pre>

1.1、新建index.css文件,如下

index.css

App {
 text-align: center;
}
​
.App-logo {
 height: 40vmin;
 pointer-events: none;
}
​
@media (prefers-reduced-motion: no-preference) {
 .App-logo {
 animation: App-logo-spin infinite 20s linear;
 }
}
​
.App-header {
 background-color: #282c34;
 min-height: 100vh;
 display: flex;
 flex-direction: column;
 align-items: center;
 justify-content: center;
 font-size: calc(10px + 2vmin);
 color: white;
}
​
.App-link {
 color: #61dafb;
}
​
@keyframes App-logo-spin {
 from {
 transform: rotate(0deg);
 }
 to {
 transform: rotate(360deg);
 }
}

1.2、修改index.js文件

index.js

import React from "react";
import ReactDOM from "react-dom";
​
import App from "./App"; // 将导入的App组件删除
​
const rootElement = document.getElementById("root");
ReactDOM.render(
 <React.StrictMode>
 <App /> {/* 将引入的app组件删除 */}
 </React.StrictMode>,
 rootElement
);

1.3、创建三个新组件

  • Square:渲染单独的一个棋子组件

  • Borad:渲染棋盘组件

  • Game:渲染默认值的一个棋盘

Game.js

import React from 'react';
import Board from './Board';
​
class Game extends React.Component {
 render() {
 return (
 <div className="game">
 <div className="game-board">
 {/* 加载棋盘组件 */}
 <Board />
 </div>
 <div className="game-info">
 <div>{/* status */}</div>
 <ol>{/* TODO */}</ol>
 </div>
 </div>
 );
 }
}
​
export default Game;

Board.js

import React from 'react';
import Square from './Square';
​
class Board extends React.Component {
 // 返回一个组件,React元素
 renderSquare(i) {
 return <Square />;
 }
​
 render() {
 const status = '下一个棋手: X';
​
 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>
 );
 }
}
​
export default Board;

Square.js

import React from 'react';
​
class Square extends React.Component {
 render() {
 return (
 <button className="square">
 {/* TODO */}
 </button>
 );
 }
 }
​
 export default Square;

上面新增加的三个组件,只是初始化的文件,在后面的开发流程中增加新的代码。

对index.js文件做如下修改

import React from "react";
import ReactDOM from "react-dom";
import './index.css';
​
import Game from './Game';
​
const rootElement = document.getElementById("root");
ReactDOM.render(
 <React.StrictMode>
 {/* 加载Game组件 */}
 <Game /> {/* Game组件是组装的React元素 */}
 </React.StrictMode>,
 {/* rootElement 是整个项目唯一的一个挂载点,这对应的单页面工程只有一个页面 页面都是由数据控制 */}
 rootElement
);

关于index.js文件中存在的知识点

1、引入react-dom包的作用: react-dom的 package 提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。
2、ReactDOM.render()就是react-dom包中的方法,它用来渲染一个React元素,并返回对该组件的引用。</pre>

ReactDOM.render() 会控制你传入容器节点里的内容。当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 DOM 差分算法(DOM diffing algorithm)进行高效的更新。

对React做一个简单的总结

React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。</pre>

下面是我们对新增加的这三个组件进行特定业务的编码

2、Square、Board等组件的开发

2.1、单纯测试props属性

在 Board 组件的 renderSquare 方法中,我们将代码改写成下面这样,传递一个名为 value 的 prop 到 Square 当中:

class Board extends React.Component {
 renderSquare(i) {
 // 在父组件中给子组件传递一个props属性value。
 return <Square value={i} />;
 }
}

修改 Square 组件中的 render 方法,把 {/* TODO */} 替换为 {this.props.value},以显示上文中传入的值:

class Square extends React.Component {
 render() {
 return (
 <button className="square">
 {/* 在子组件中直接取props对象的属性值,就可以得到父组件传递的数据 */}
 {this.props.value}
 </button>
 );
 }
}

经过上面代码的修改,页面修改前后展现的对比。如下图:

修改前:
1583560819929.png

修改后:
1583560863857.png

成功地把一个 prop 从父组件 Board “传递”给了子组件 Square。在 React 应用中,数据通过 props 的传递,从父组件流向子组件。

2.2、给组件添加交互功能

棋盘上的每一个格子对一个就是一个"Square"组件,给Square组件增加一个点击事件。当点击小格子时,页面会弹出一个信息提示框。

2.2.1、给Square组件增加点击事件

修改 Square.js文件

import React from 'react';
​
class Square extends React.Component {
 render() {
 return (
 <button className="square" onClick={()=> {alert('棋盘中的小格子被触发')}}>
 {/* 在子组件中直接取props对象的属性值,就可以得到父组件传递的数据 */}
 { this.props.value }
 </button>
 );
 }
 }
​
 export default Square;
2.2.2、点击Square组件,格子显示“X”

我们希望 Square 组件可以“记住”它被点击过,然后用 “X” 来填充对应的方格。我们用 state 来实现所谓“记忆”的功能。

可以通过在 React 组件的构造函数中设置 this.state 来初始化 state。this.state 应该被视为一个组件的私有属性。我们在 this.state 中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。</pre>

state是组件内部的属性。组件本身是一个状态机,它可以在constructor中通过this.state直接定义它的值,然后根据这些值来渲染不同的UI。当state的值发生改变时,可通过this.setState方法让组件再次调用render方法,来渲染新的UI。

修改 Square.js文件

import React from 'react';
​
class Square extends React.Component {
 constructor(props){
 super(props);
 // 初始化棋盘格子中要显示的值“value”
 this.state = {
 value: null
 };
 }
​
 render() {
 return (
 // state是组件内部的“状态”属性,从外部不能改变,只能通过setState方法更新
 <button className="square" onClick={()=> this.setState({value: 'X'}) }>
 { this.state.value }
 </button>
 );
 }
 }
​
 export default Square;

现在再点击棋盘中的格子,在格子里就会展示“X”,如图所示:

1583572520960.png

3、增加游戏规则

井字棋游戏规则:两位选手,各执祺子为“X”或“O”。在九宫格内,任一选手的棋子在纵向、横向或斜方向上能排成一条直线,即为胜方。

剩下的功能就是需要交替在棋盘上放置“X”或“O”,并且判断出胜者。

Square为格子组件,Board为棋盘组件,Board组件是Square组件的父组件。棋子Square上的数据都通过state保存到Board上。

3.1、将棋子的数据保存到Board组件中state上

为Board组件添加构造函数,将Board组件的初始状态设置为长度为9的空数组。

class Board extends React.Component {
 constructor(props){
 super(props);
 this.state = {
 // fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素
 squares: Array.fill(null)
 }
 }
​
 // 返回一个组件,React元素
 renderSquare(i) {
 // 在父组件中给子组件传递一个props属性value。
 return <Square value={this.state.squares[i]} />;
 }
 // ....省略其它源码
}

让我们再一次使用 prop 的传递机制。我们通过修改 Board 来指示每一个 Square 的当前值('X', 'O', 或者 null)。我们在 Board 的构造函数中已经定义好了 squares 数组,这样,我们就可以通过修改 Board 的 renderSquare 方法来读取这些值了。

renderSquare(i) {
 return <Square value={this.state.squares[i]} />;
 }

这样,每个 Square 就都能接收到一个 value prop 了,这个 prop 的值可以是 'X''O'

上面的代码,在Board组件中对Square组件进行初始化,用Board组件来维护那些被填充的格子。 我们需要想办法让 Square 去更新 Board 的 state。由于 state 对于每个组件来说是私有的,因此我们不能直接通过 Square 来更新 Board 的 state。

state是组件的内部状态,对外是私有,从外部是不能改变state的状态,只能在组件内部使用setState方法来更新

在Board组件中向Square组件传递一个props的函数,当Square组件被点击时就调用这个函数。

 renderSquare(i) {
 return (
 <Square
 value={this.state.squares[i]}
 onClick={() => this.handleClick(i)}
 />
 );
 }

现在我们从 Board 组件向 Square 组件中传递两个 props 参数:valueonClickonClick prop 是一个 Square 组件点击事件监听函数。接下来,我们需要修改 Square 的代码:

Square.js

class Square extends React.Component {
 render() {
 return (
 <button
 className="square"
 onClick={() => this.props.onClick()}
 >
 {this.props.value}
 </button>
 );
 }
}

1、将Square组件的render方法中的this.state.value替换为this.props.value;
2、将Square组件的render方法中的this.setState替换为this.props.onClic();
3、删掉Square组件中的构造函数constructor,因为该组件不再保存游戏的state;</pre>

Board组件和Square组件间的交互流程如下:

每一个 Square 被点击时,Board 提供的 onClick 函数就会触发。我们回顾一下这是怎么实现的:
1、向 DOM 内置元素 <button> 添加 onClick prop,让 React 开启对点击事件的监听。
2、当 button 被点击时,React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
3、事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
4、由于 Board 把 onClick={() => this.handleClick(i)} 传递给了 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 this.handleClick(i) 方法。</pre>

给Board组件中增加handleClick()方法

import React from 'react';
import Square from './Square';
​
class Board extends React.Component {
 constructor(props){
 super(props);
 this.state = {
 squares: Array(9).fill(null)
 };
 }
​
 HandleClick(i){
 // .slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。
 const squares = this.state.squares.slice();
 squares[i] = "X";
 this.setState({
 squares: squares
 });
 }
​
 // 返回一个组件,React元素
 renderSquare(i) {
 // 在父组件中给子组件传递一个props属性value。
 return <Square value={this.state.squares[i]} onClick={() => this.HandleClick(i)} />;
 }
 // 代码未贴完....

基于上面的组件交互,每当Board的state发生变化时,这些Square组件就会渲染一次。把所有Square组件中的state保存到Board组件中可以让我们在将来判断出游戏的胜利者。

3.2、选手轮流落子

现在棋盘上还只能展示一个棋子的标识(“X”),现在要把“O”棋子也加入到棋盘中。我们先把“X"棋子设置为先手棋。我们可以通过修改Board组件的构造函数中的初始state来设置默认的第一步棋子;在state中设置一个默认属性xIsNext来标识棋子是“X”或“O”。

代码如下: Board.js

class Board extends React.Component {
 constructor(props) {
 super(props);
 this.state = {
 squares: Array(9).fill(null),
 xIsNext: true,
 };
 }

棋子没移动一步,也就是Square组件点击一次,Square组件的事件会触发父组件Board中的state的变化,也就是说页面会重新渲染一次。同样,我们也可以将xIsNext的值进行修改。xIsNext的值的变化来决定落入棋盘的棋子。

代码如下: Board.js

 handleClick(i) {
 const squares = this.state.squares.slice();
 squares[i] = this.state.xIsNext ? 'X' : 'O';
 this.setState({
 squares: squares,
 xIsNext: !this.state.xIsNext,
 });
 }

现在页面上的棋盘效果如图所示:

1583589214923.png

页面中对两棋手落子的判断开发如下

代码如下 Board.js

 render() {
 const status = '下一个棋手: ' + (this.state.xIsNext ? 'X' : 'O');
 // 后面代码未贴。。。

4、判断游戏的胜负

在两选手的轮流落子开发完毕后,我们就要对游戏的胜负裁决进行业务逻辑编码。判断游戏胜出的编码如下:

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;
}

对游戏胜败的判断逻辑如下,基于棋子落入棋盘中能形成胜出的位置,以数据的形式写入到一个数组中,然后去判断两个棋子落入棋盘的位置。能否和数组中的数据匹配上。如果匹配上就返回胜出的棋子,如果匹配不上就返回一个null值。

代码如下 Board.js

 render() {
 // 根据判断函数来返回胜出的棋子
 const winner = calculateWinner(this.state.squares);
 // 声明变量status 来标识游戏当前的状态
 let status;
 if(winner) {
 status = "胜出者为:" + winner;
 }else {
 status = '下一个棋手: ' + (this.state.xIsNext ? 'X' : 'O');
 }
 // 其它代码没有变化

最后,修改 handleClick 事件,当有玩家胜出时,或者某个 Square 已经被填充时,该函数不做任何处理直接返回。

代码如下 Board.js

 HandleClick(i){
 // .slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。
 const squares = this.state.squares.slice();
 // 如果棋手胜出,或有棋子填充Square,则不作任何处理直接返回
 if (calculateWinner(squares) || squares[i]) {
 return;
 }
 squares[i] = this.state.xIsNext ? "X" : "O";
 this.setState({
 squares: squares,
 xIsNext: !this.state.xIsNext
 });
 }

至此,井字游戏开发完毕,还有高级功能未在此添加,有待后续追加。效果如图所示:

1583590550224.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容