最近开始学习React了,为了学习过程更有趣味性,选择了一个简单有趣的桌游宝石商人来开发,边玩边开发边学习!用的是最基础的技术栈 create-react-app + react-redux + react-router-dom + antd,桌游界面的设计主要是参考【哇哦桌游】的,不过做的是单机版嘛,所以有一些改进。主要是想分享一下好玩的桌游和自己的设计,记录一些开发中遇到的问题,大概包括这些:
- 游戏流程 & 界面组件设计
- 数据设计 react-redux
- react中数据状态存储的方法和区别
- 为什么在setInterval中获取不到最新的数据
- 设置key={index}的问题
- …
目前就先想到这么多,突然记起来啥再加上来吧~
1. 游戏流程 & 界面组件设计
宝石商人这个游戏大致流程就是2个玩家(其实可以多个但是我只做了2个)轮流换取宝石或者是卡牌,谁先凑齐15分谁就赢了。
- 获取分数有2个渠道,一是兑换有分数的卡牌(在卡牌的右上角标明的是分数),二是获取贵族的青睐(满足贵族所要求的卡牌数量即可获得青睐)。
- 每个回合可以选择3种操作,(1)拿取宝石,只能拿白色、蓝色、绿色、红色和褐色宝石,不同颜色的至多3个,相同颜色的至多2个;(2)预约卡牌,预约后这个卡牌不能被另一个玩家抢走,且会附赠一个黄色的宝石(可以当做任何颜色使用),下个回个就可以选择兑换预约的卡牌了,一共只有5个黄色宝石,所以2个玩家最多一共预约5个卡牌;(3)兑换卡牌,满足相应卡牌的颜色即可完成兑换。
- 兑换得到的卡牌有颜色,这个颜色的卡牌相当于永久的同颜色的宝石。
- 用户选择的内容会实时显示,当用户确认选择后,才会提交至用户信息中进行保存。
根据上述游戏流程,就可以进行界面和组件的设计啦~
整个界面为一个父组件Game,包括User组件用于表示用户,显示当前用户得分、卡牌数量、宝石数量、预约的卡牌和倒计时,Card组件用于表示中间可选择的卡牌,Noble组件显示贵族青睐所需的卡牌,Gem组件用于显示当前剩余的宝石数量,Round组件用于显示当前回合用户选择的内容。
2. 数据设计 react-redux
- Q1:关于为什么使用redux
其实一开始我是考虑直接把所有数据都放在父组件Game上,再通过props传递给子组件数据和修改数据的方法,这样子组件可以拿到所有的数据并可以修改数据,但是最主要的问题是,这样数据不方便进行规范化的修改和管理,子组件修改的数据很容易出错。所以最后还是选用react-redux进行了数据管理,在reducer中定义了修改每个数据的方法,这样都可以对数据进行规范化的修改,开发中遇到的问题会更少。不知道这样的理解是否正确诶
- Q2:是一个action和reducer管理一个数据吗?
使用react-redux的时候,在教程中,是着重强调了给每个组件定义一个reducer文件,但是我觉得应该是给每个数据定义一个reducer文件,文件内记录对于每个数据的操作方法,如果是每个组件一个reducer的话,那两个组件都要用一个数据怎么办?我觉得好像是教程有问题,我是按照每个数据一个reducer进行开发的!
一、系统数据:
系统数据是游戏初始化的数据,随机从以下数据中选择,需要选择3个贵族以及4张一级卡牌、4张二级卡牌和4张三级卡牌,选择的贵族和卡牌之间不能重复。
- nobles 贵族的青睐,表示需要什么要求的卡牌才能够获得贵族的青睐,+3分
[
{
blue: 4,
white: 4,
},
// ……
]
复制代码
- 一级、二级、三级卡牌,一级卡牌20张,二级卡牌30张,三级卡牌40张。
{
level3: [
{
score: 5,
color: 'blue',
needs: {
white: 7,
blue: 3,
},
},
// ……
],
level2: [
{
score: 3,
color: 'blue',
needs: {
blue: 6,
},
},
// ……
],
level1: [
{
score: 1,
color: 'blue',
needs: {
red: 4,
},
},
// ……
]
}
复制代码
二、redux管理的数据一共包括:
- remainGem:存储剩余宝石的数量
{ white: 5, blue: 5, green: 5, red: 5, brown: 5, gold: 5 }
复制代码
- round:存储当前是第几个回合,以及当前的用户
{
number: 1, // 回合数
userIndex: 0, // 用户id
}
复制代码
- userList:存储当前的用户信息
[{
name: '玩家1',
score: 0, // 得分
gem: { white: 0, blue: 0, green: 0, red: 0, brown: 0, gold: 0 }, //宝石
card: { white: 0, blue: 0, green: 0, red: 0, brown: 0 }, // 卡牌
order: [], // 预定的卡牌
},
{
name: '玩家2',
score: 0,
gem: { white: 0, blue: 0, green: 0, red: 0, brown: 0, gold: 0 },
card: { white: 0, blue: 0, green: 0, red: 0, brown: 0 },
order: [],
}]
复制代码
- selectedContent:当前回合用户选择的内容,分为4种类型。
(1)用户选择的为 宝石
{
type: 'gem',
content: {
gemList: ['white', 'blue', 'yellow'],
},
};
复制代码
(2)用户选择的为 卡牌
{
type: 'card',
content: {
card: {
score: 3,
needs: { red: 3, blue: 3, green: 3, white: 3 }
}
},
};
复制代码
(3)用户选择的为 预定
{
type: 'order',
content: {
card: {
score: 3,
needs: { red: 3, blue: 3, green: 3, white: 3 }
},
gemList: ['gold'] // 预定默认获得一个金色的万能宝石
},
};
复制代码
(3)用户选择的为 该用户之前预定的卡牌
{
type: 'order-card',
content: {
card: {
score: 3,
needs: { red: 3, blue: 3, green: 3, white: 3 }
},
},
};
复制代码
修改每种数据的方法如下:
数据 | 方法 | 含义 |
---|---|---|
remainGem | changeRemainGem | 修改剩余的宝石数量,type选择+1/-1,gemList选择需要操作的宝石是哪些 |
round | changeRound | 修改当前回合为下一个回合 |
userList | addUser | 初始化增加一个玩家 |
changeUserSelectedGem | 修改玩家已有的宝石数量 | |
changeUserSelectedCard | 修改玩家已有的卡牌数量 | |
changeUserScore | 修改玩家得分 | |
changeUserOrderList | 修改玩家的预定内容 | |
changeUserWin | 改变玩家的状态为赢得游戏(可能某个玩家赢了,也可能平局) | |
selectedContent | addSelectedGem | 增加当前用户选择的宝石 |
reduceSelectedGem | 减少当前用户选择的宝石 | |
changeSelectedCard | 修改当前用户的选择内容为某卡牌 | |
changeSelectedOrder | 修改当前用户的选择内容为预定某卡牌 | |
changeSelectedOrderCard | 修改当前用户的选择内容为该用户已经预定的某卡牌 |
其中,之所以需要设计selectedContent这个数据,是因为,用户选择的内容是显示在Round组件内的,并且当前用户在结束回合前,都可以修改选择的内容,只有选择结束回合后,才会将选择的内容提交至User组件显示。
3. react中数据状态存储的方法和区别
在一个类组件中,数据状态有如下的方式进行存储:
- 在this实例上
- 在this.state中
- static静态属性
class Components extends React.Component {
this.test = 1; // 方式 1:在this实例上
constructor (props){
super(props);
this.state = {
test: 1 // 方式 2:在this.state中
}
}
}
Components.test = 1; // 方式 3:静态属性
复制代码
这3种方式的区别在于:
- 在this.state中存储的是和视图相关的响应式数据,当state中相关的数据变化后,需要通过setState更新数据,从而使得react帮助我们更新视图。
- 在this实例上存储的数据,该组件的每个实例都存在test属性,且每个实例的test属性之间互不相干,一般存储实例需要的、和视图不直接相关的数据。
- 静态属性存储的是和实例无关的数据,是每个组件实例共享的数据,通过【组件名.属性名】访问。
同样的,在一个函数组件中,数据状态存在如下4种类型存储:
- useState
- useRef
- 静态属性
- var/let/const一个变量
function Component(){
const [test, setTest] = useState(1); // 方式 1
const testRef = useRef(1) // 方式 2
var test1 = 1; // 方式 3
// ……
return <div></div>;
}
Component.test = 1; // 方式 3:静态属性
复制代码
这4种方式的区别在于:
- 方式1是响应式数据,是和视图直接相关的数据,需要通过setTest方式更改数据。
- 方式2不是响应式的,通过testRef.current = 2的方式更改数据。
- 方式3是静态属性,是通过Component.test访问的,各组件示例之间可以共享。
- 方式4中的test1变量是一次性的,每次视图更新调用Component函数获取最新的render内的时候,都会重新定义一个test1变量,并赋值为1,所以test1只能参与一些一次性的运算。
4. 为什么在setInterval中获取不到最新的数据
之所以会关注3的数据存储方式,是因为我在开发过程中遇到了这样一个问题:
- 场景:我采用的函数组件进行开发,由于需要用户操作的倒计时,我在User组件中定义了一个定时器,每0.1秒都会改变用户的剩余时间,从而使得进度条发生变化。
- 数据:我是使用的useRef方法存储的定时器,因为定时器和响应式视图不直接相关,但是视图更新后是需要获取到之前的定时器的,这样使得定时器不会重复定义、方便清除定时器,而和视图相关的剩余时间我是使用useState定义的,因为它与视图直接相关。
function User(props){
const [selectRemainTime, setSelectRemainTime] = useState(120); // 剩余时间
const taskRef = useRef(null); // 定时器
if (task.current === null && props.index === props.round.userIndex) {
// 当前没有定义定时器,且当前用户是本组件实例的时候
taskRef.current = setInterval(() => {
setSelectRemainTime(v => v - 1); // 更新时间
// 有一些条件需要清除定时器
// !!!
// 这里的selectRemainTime一直是初始值
if ( selectRemainTime < 0 ) {
clearInterval(taskRef.current);
// !!!
// 这里需要从props上获取react-redux更新的数据,做出一些操作
// 但是获取不到最新的prosp
}
})
}
}
复制代码
为什么在setInterval内获取不到最新的selectRemainTime和props数据呢?这主要是闭包导致的,当前函数内没有selectRemainTime和props,所以只能去父作用域内找,找到的selectRemainTime和props是初始化时候的值,不是更新后的值。
所以解决的办法包括:
- 使用useEffect,在useEffect的依赖数据数据中增加selectRemainTime。
- 使用useRef,增加对于selectRemainTime和props的引用,每次都使用useRef更新当前的值。
- 使用官方提供的useCallback。
注意,使用useEffect解决props的变化是没有用的,因为props的属性值并未变化,变化的是深层次的对象,所以react-redux相关的数据的变化useEffect的监听不到的。
最后,我是采用了这样的方式解决selectRemainTime和props数据更新的问题:
function User(props){
const [selectRemainTime, setSelectRemainTime] = useState(120); // 剩余时间
const taskRef = useRef(null); // 定时器
const propsRef = useRef(props); // props的引用
propsRef.current = props; // 更新引用
if (task.current === null && props.index === props.round.userIndex) {
// 当前没有定义定时器,且当前用户是本组件实例的时候
let count = selectRemainTime; // 形成一个闭包
taskRef.current = setInterval(() => {
setSelectRemainTime(--count); // 更新时间和count变量
if ( count < 0 ) {
clearInterval(taskRef.current);
// propsRef.current上获取最新的数据
}
})
}
}
复制代码
selectRemainTime采用了闭包解决,props采用了useRef解决。
5. 设置key={index}的问题
很多数据都是采用的Object类型来存储,在jsx语法的时候都需要写成
Object.keys(data).map(e => (<div></div>))
复制代码
的方式,但是一般控制台会出现缺少key的warning,在我开发过程中倒是因为缺少key没有出现什么错误,但是为了一个干干净净的控制台~ 还是解决一下这个问题吧!
这个其实和react的渲染有关(和vue的原理是一样的),就是在通过diff算法比较新的和旧的虚拟dom树的时候,会根据key来判断,这个dom元素是否需要更新,在vue中如果没有设置,默认为undefined,而在react中,没有设置默认为index。这都会使得渲染的效率下降,例如插入新元素的时候,如果不合理的设置key,会导致多更新一个元素。
如果数据只需要渲染,而不存在更新,则设置key={index}是没有问题的,因为不需要更新不存在diff;而如果涉及到更新,但是没有input等输入型组件的增删改,设置key={index}也是没有问题的,但是可能会使得渲染效率下降;如果涉及到input输入组件的增删改,则使用key={index}很有可能会出现问题。
可以作为key的数据:
- 数据的唯一值,例如id值等等;
- 在数据初始化的时候,增加一个id值,可以使用时间戳、随机数等等。
最近领了个腾讯云免费的服务器,部署了一下,好像4月十几号就到期啦,地址是:
http://42.192.152.124:5277/splendor/
,不知道现在还有没有bug,我改了好几版了 T T