Skip to content

Repository files navigation

react-memory

基于 nodejs + bower + react 的文字记忆游戏。

前言

现在最热门的前端框架,毫无疑问是 React,React 是由 Facebook 出品的 JavaScript 框架,由于该框架比较新,比较少中文的资料。这几天看了很多篇关于 react 的英文文章,不得不说 React 是创建大型、快速的 Web 应用的最好方式。在本文中,我们将通过一步一步的创建一个简单的文字记忆游戏,来体验 React 的思想和强大之处。

PS: 由于对 React 的学习也是皮毛,但是在这里,我希望这个小游戏能够成为学习 React 的最佳开发结构,并且随着自己的不断学习,将会继续改进和完善这里的代码。假如您有任何的建议和反馈,请给我留言,谢谢!

在开始之前我们先来看看我们的 demo,游戏非常简单,输入想要记忆的文字,然后点击开始记忆即可。试玩了我们的游戏之后,那么现在就开始吧。

先来看看我们的目录结构,你可以在 GitHub 上找到相对应的源码

├── bower_components
│ ├── bootstrap
│ └── jquery
├── node_modules
│ ├── browserify
│ ├── lodash
│ ├── react
│ ├── reactify
│ └── watchify
├── docs
│ ├── component.dot
│ └── component.png
├── build
│ └── app.js
├── css
│ └── style.css
├── index.html
├── js
│ ├── app.js
│ ├── board.js
│ ├── game.js
│ ├── status.js
│ ├── tile.js
│ └── word-form.js
├── bower.json
├── package.json
└── README.md
  • bower_components 和 bower.json 是安装 bower 组件的目录和配置信息。
  • node_modules 和 package.json 是安装 npm 模块的目录和配置信息。
  • docs 用于存放我们的文档信息。
  • css 和 js 用于存放样式和 JavaScript 源码。
  • build 用于存放最后编译的 css 和 js 文件。
  • index.html 是我们游戏的主页面,也就是 React 的入口。

初始化

  • 首先,创建 npm 模块的配置文件 package.json
{
"name": "react-memory",
"version": "1.0.0",
"description": "基于 nodejs + bower + react 的文字记忆游戏。",
"browserify": {
"transform": [
["reactify"]
]
},
"author": "wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)",
"license": "MIT"
}
  • 接着,创建 bower 组件的配置文件 bower.json
{
"name": "react-memory",
"version": "1.0.0",
"authors": [
"zhixin <wenzhixin2010@gmail.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}
  • 安装所需要的依赖包
# 运行游戏时需要的依赖包
npm install --save react lodash
bower install --save bootstrap
# 编译游戏时需要的依赖包
npm install --save-dev browserify watchify reactify
# 全局命令行工具
npm install -g browserify watchify http-server

可以看到,我们安装了运行游戏时所需要的依赖包:react,lodash 模块,以及 bootstrap 组件,lodash 是一个非常实用的工具库,游戏中我们使用到了好多它所提供的操作 array 的简单方法,react 和 bootstrap 的话就不用说了。

React 组件依赖层次

React 中都是以组件的方式来体现的,从上往下,我们切割成非常小、功能单一的组件,分别是:

  • Game:游戏组件
  • WordForm:文字输入组价
  • Board:游戏面板组件
  • Status:游戏状态组价
  • Tile:单个卡片组件

组件模板

由于我们使用了 nodejs 的开发方式以及 React 独有的 JSX 语法,我们组件的模板为:

varReact=require('react'),// 加载 react 模块_=require('lodash'),// 加载 lodash 模块OtherComponent=require('./other-component');// 加载其他自定义 React 模块varComponent=React.createClass({// 定义组件所需要的 properties 属性propTypes: {prop1: React.PropTypes.string.isRequired,func1: React.PropTypes.func.isRequired},// 初始化组件的状态,并非所有组件都需要 stategetInitialState: function(){return{};},// 渲染我们的界面render: function(){return(<OtherComponentprop1={this.props.prop1}func1={{this.props.func1}}/>
);
}});module.exports=Component;

对于各行代码的意思,已加了详细的注释说明,在下面的 js 代码中,也是一样在代码中做了详细注释,由于我们只关注组件的核心部分,与模板相同的地方,我们就不做解释了。

Game

创建文件:js/game.js

varReact=require('react'),_=require('lodash'),Board=require('./board'),WordForm=require('./word-form');varGame=React.createClass({// 初始化 state,这里我们使用了 words 数组,用于保存输入的文字getInitialState: function(){return{words: undefined};},// 开始游戏startGame: function(words){this.setState({// 组合并打乱输入的文字words: _.shuffle(words.concat(words))});},// 结束游戏,设置 words 为 undefinedendGame: function(){this.setState({words: undefined});},// 根据 words 来显示我们自定义的组件render: function(){return(this.state.words ?
<BoardonEndGame={this.endGame}words={this.state.words}/> :
<WordFormonWordsEntered={this.startGame}/>);}});module.exports=Game;

WordForm

  • 新建文件:js/word-form.js
varReact=require('react'),_=require('lodash');varWordForm=React.createClass({// 需要提供 onWordsEntered 方法,用于触发提交方法,在 Game 中我们使用了 startGamepropTypes: {onWordsEntered: React.PropTypes.func.isRequired},// 初始化 error 状态getInitialState: function(){return{error: undefined};},// 显示错误信息,2s 后自动消失setError: function(msg){this.setState({error: msg});setTimeout(function(){this.setState({error: ''});}.bind(this),2000);},// 提交文字信息,判断是否符合条件submitWords: function(e){e.preventDefault();varnode=this.refs.words.getDOMNode(),// unique 用于生成唯一的字符words=_.unique((node.value||'').trim().split(''));if(words.length<3){this.setError('请至少输入三个不同的字符!');}else{this.props.onWordsEntered(words);node.value='';}},render: function(){return(<formclassName='form-inline'onSubmit={this.submitWords}><span>请输入你想记忆的字符:</span><inputclassName='form-control'type='text'ref='words'maxLength='10'defaultValue='文字记忆游戏'/><buttonclassName='btn btn-default'type='submit'>开始记忆</button><pclassName='error'>{this.state.error}</p></form>);}});module.exports=WordForm;
  • 由于用用到了 bootstrap 的样式和自定义了 error 样式,需要创建 css/style.css 文件
@import"../bower_components/bootstrap/dist/css/bootstrap.min.css";
.error {
color: red;
}

Boar

新建文件:js/board.js

varReact=require('react'),_=require('lodash'),Tile=require('./tile'),Status=require('./status');varBoard=React.createClass({// 需要提供 words 属性,以及 onEndGame 方法,分别对应 Game 的属性和方法propTypes: {words: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,onEndGame: React.PropTypes.func.isRequired},// 在组件还未 mount 之前用于计算总共有多少对文字卡片componentWillMount: function(){this.max=this.props.words.length/2;},// State 状态// found:表示找到了多少对文字卡片// message:显示当前的状态// tileStatesgetInitialState: function(){return{found: 0,message: 'chooseTile',tileStates: newArray(this.props.words.length+1).join('unturned ').trim().split(' ')};},// 游戏逻辑的处理方法clickedTile: function(index){// 当卡片的状态为 unturned(未翻转)时,才进行处理if(this.state.tileStates[index]==='unturned'){// flippedTile 用于保存上个点击的卡片的 indexif(this.flippedTile===undefined){this.flippedTile=index;// 设置状态为 findMatethis.setState({message: 'findMate',// 使用 lodash 方法,将对应的下标置为 revealed(翻转)状态tileStates: _.extend(this.state.tileStates,_.object([index],['revealed']))});}else{varotherIndex=this.flippedTile,matched=this.props.words[index]===this.props.words[this.flippedTile];if(matched){// 找到相对应的卡片,found + 1,并将状态置为 foundMatethis.setState({found: this.state.found+1,message: 'foundMate',// 使用 lodash 方法,将对应的下标置为 correct(正确)状态tileStates: _.extend(this.state.tileStates,_.object([index,otherIndex],['correct','correct']))});}else{// 没有找到相对应的卡片,将状态置为 wrongthis.setState({message: 'wrong',// 使用 lodash 方法,将对应的下标置为 wrong(错误)状态tileStates: _.extend(this.state.tileStates,_.object([index,otherIndex],['wrong','wrong']))});}// 删除保存的信息deletethis.flippedTile;// 1.5s 后我们将卡片翻转回来setTimeout(function(){// 需要判断组件是否 mountedif(this.isMounted()){// 假如所有都选中了,将状态置为 foundAllthis.setState({message: this.state.message==='findMate' ? 'findMate' :
this.max===this.state.found ? 'foundAll' : 'chooseTile',tileStates: matched ? this.state.tileStates : _.extend(this.state.tileStates,_.object([index,otherIndex],['unturned','unturned']))});}}.bind(this),1500);}}},render: function(){// 使用 map 方式,将所有的卡片显示出来vartiles=this.props.words.map(function(word,i){return(<divkey={i}onClick={_.partial(this.clickedTile,i)}><Tileword={word}status={this.state.tileStates[i]}/></div>);}.bind(this));return(<div><buttonclassName='btn btn-default'onClick={this.props.onEndGame}>结束记忆</button><Statusfound={this.state.found}max={this.max}message={this.state.message}/>{tiles}</div>);}});module.exports=Board;

Status

新建文件status.js

varReact=require('react');varStatus=React.createClass({propTypes: {found: React.PropTypes.number.isRequired,max: React.PropTypes.number.isRequired,message: React.PropTypes.oneOf(['chooseTile','findMate','wrong','foundMate','foundAll']).isRequired},render: function(){varfound=this.props.found,max=this.props.max,texts={chooseTile: '选择一张卡片!',findMate: '现在我们来查找相对应的卡片!',wrong: '很遗憾,这两张卡片不匹配!',foundMate: '不错,他们是一对的!',foundAll: '恭喜过关,你已经找到所有'+max+'对卡片了!'};return(<p>({found}/{max})&nbsp;&nbsp;{texts[this.props.message]}</p>);}});module.exports=Status;

Tile

  • 新建文件tile.js
varReact=require('react');varTile=React.createClass({propTypes: {status: React.PropTypes.string.isRequired,word: React.PropTypes.string.isRequired},render: function(){return(<divclassName={'brick '+this.props.status}><divclassName='front'><iclassName='glyphicon glyphicon-question-sign'></i></div><divclassName='back'>{this.props.word}</div></div>);}});module.exports=Tile;
  • 修改文件css/style.css,增加卡片需要的样式
@-webkit-keyframes wronganim {
to {
background-color: red;
}
}
@-moz-keyframes wronganim {
to {
background-color: red;
}
}
@keyframes wronganim {
to {
background-color: red;
}
}
@-webkit-keyframes correctanim {
to {
background-color: green;
color: white;
}
}
@-moz-keyframes correctanim {
to {
background-color: green;
color: white;
}
}
@keyframes correctanim {
to {
background-color: green;
color: white;
}
}
.brick>div {
width:80px;
height:80px;
border:1px solid black;
text-align: center;
line-height:80px;
font-size:24px;
-webkit-backface-visibility: hidden;
-webkit-transition: -webkit-transform 0.3s linear;
-moz-transition: -moz-transform 0.3s linear;
transition: transform 0.3s linear;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
position: absolute;
overflow: hidden;
border-radius:5px;
backface-visibility: hidden;
}
.brick> .front {
background-color:#AAA;
}
.brick, .brickdiv {
user-select: none;
cursor: pointer;
}
.brick {
float: left;
margin-right:10px;
margin-bottom:10px;
width:80px;
height:80px;
}
.brick> .back {
-webkit-animation-duration:0.5s;
-webkit-animation-timing-function: ease;
-webkit-animation-delay:0.3s;
-webkit-animation-iteration-count:1;
-webkit-animation-fill-mode: forwards;
-moz-animation-duration:0.5s;
-moz-animation-timing-function: ease;
-moz-animation-delay:0.3s;
-moz-animation-iteration-count:1;
-moz-animation-fill-mode: forwards;
animation-duration:0.5s;
animation-timing-function: ease;
animation-delay:0.3s;
animation-iteration-count:1;
animation-fill-mode: forwards;
}
.brick.wrong> .back {
-webkit-animation-name: wronganim;
-moz-animation-name: wronganim;
animation-name: wronganim;
}
.brick.correct> .back {
-webkit-animation-name: correctanim;
-moz-animation-name: correctanim;
animation-name: correctanim;
}
.brick> .back {
-webkit-transform:perspective(80px) rotateY(180deg) translate3d(0px,0px,2px);
-moz-transform:perspective(80px) rotateY(180deg) translate3d(0px,0px,2px);
transform:perspective(80px) rotateY(180deg) translate3d(0px,0px,2px);
}
.brick.correct> .front, .brick.wrong> .front, .brick.revealed> .front {
-webkit-transform:perspective(80px) rotateY(-180deg) translate3d(0px,0px,2px);
-moz-transform:perspective(80px) rotateY(-180deg) translate3d(0px,0px,2px);
transform:perspective(80px) rotateY(-180deg) translate3d(0px,0px,2px);
}
.brick.correct> .back, .brick.wrong> .back, .brick.revealed> .back {
-webkit-transform:perspective(80px) rotateY(0deg) translate3d(0px,0px,1px);
-moz-transform:perspective(80px) rotateY(0deg) translate3d(0px,0px,1px);
transform:perspective(80px) rotateY(0deg) translate3d(0px,0px,1px);
}
.front {
font-size:2em;
}

app.js

创建好了我们所有的组件之后,我们需要将组件组合起来,创建文件app.js

varReact=require('react'),Game=require('./game');React.render(<Game/>,document.getElementById('app'));

index.html

新建文件:index.html

<!DOCTYPE html><html><head><metacharset="utf-8"><title>记忆游戏</title><metaname="viewport" content="width=device-width, initial-scale=1"><linkrel="stylesheet" href="css/style.css"></head><body><navclass="navbar navbar-inverse navbar-fixed-top"><divclass="container"><divclass="navbar-header"><spanclass="navbar-brand">记忆游戏</span></div></div></nav><divid="app" class="container">正在努力加载中……</div><scriptsrc="build/app.js"></script></body></html>

查看结果

  • 开始编译监听 jsx 文件为 js
watchify -v -o build/app.js js/app.js
  • 启用 http server
http-server -p 8888

About

基于 nodejs + bower + react 的文字记忆游戏。

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages