UDN-企业互联网技术人气社区

板块导航

浏览  : 467
回复  : 2

[React] React学习之围棋记谱本制作(四)前端开发初步完成

[复制链接]
独领风骚的头像 楼主
本帖最后由 独领风骚 于 2016-12-21 15:28 编辑

  接上篇

  今天初始完成了页面端的开发工作。把遇到的问题说一说。

  (1)开始时,对javascript的对象或数组拷贝、赋值理解不是很透,折磨了我好长时间。 理解了对象或数组的赋值,实际上相当于C语言中的指针地址赋值,就知道了保存每一步的棋盘状态,要把对象拷贝一个副本,避免后继的变化,影响保存的状态。

  (2)JQuery提供了对象拷贝的方法,extend。这个方法有深拷贝、浅拷贝之分,如果浅拷贝,不复制对象中的对象。还有个问题,就是数组拷贝后,会变成一个伪数组,能用下标取值,但不支持length属性。调这个错也用了很长时间。还好chrome支持断点调试。

  目前支持功能:交替落子、布局摆子、撤销、重做、新建布局。界面如下图所示:
1.png
  整个工程涉及四个文件。组件文件、状态管理文件、样式文件、网页文件。下面提供的是源码,依赖bootstrap、jquery。

  一、状态管理文件GoStateManager.js
  1. /**
  2. * 用于GO的状态管理。管理所有组件的状态,所有组件订阅事件,同步状态
  3. * http://wallimn.iteye.com
  4. */
  5. "use strict"
  6. var Events = require('events');

  7. class GoStateManager {
  8.     constructor() {
  9.         this.initState();
  10.         this.initList();
  11.         this.eventEmitter = new Events.EventEmitter();
  12.         this.eventEmitter.setMaxListeners(500);

  13.         var t1 = this.getDefaultPieceState();
  14.         var t2 = this.getDefaultPieceState();

  15.     };

  16.     //一个棋子的初始状态
  17.     getDefaultPieceState(){
  18.         return {visibility:'hidden',num:0,black:false};
  19.     }

  20.     //所有棋子的初始状态
  21.     getDefaultPieces(){
  22.         var pieces=[];//所有棋子状态
  23.         for(var i=0 ; i<19*19; i++) pieces.push(this.getDefaultPieceState());
  24.         return pieces;
  25.     }

  26.     initState(){
  27.         //指示当前要下的子的状态,该状态使用后,调用next方法,切换状态
  28.         this.current= {
  29.             index:1,//当前步数
  30.             goBlack:true,//是否是黑子,指行棋时
  31.             placeBlack:true,//是否是黑子,指布局时
  32.             numShow:false,//是否显示数字
  33.             place:false,//是否是布局摆子,如果是,不改变当前步数,布局时摆的棋子上面不显示数字
  34.         };
  35.         //所有棋子的状态
  36.         this.pieces = this.getDefaultPieces();

  37.     }

  38.     //初始化重做、撤销两个队列
  39.     initList(){
  40.         this.undoList = [];//后进先出队列
  41.         this.redoList = [];//后进先出队列
  42.     }

  43.     //将状态压栈,保存,保存的是对象的副本。
  44.     pushUndoList(current,pieces){
  45.         this.undoList.push({
  46.             current:this.cloneObject(current),
  47.             pieces:this.cloneObject(pieces)
  48.         });

  49.     }

  50.     //清空RedoList,当执行下一步时执行此方法
  51.     clearRedoList(){
  52.         var len=this.redoList.length;
  53.         if(len>0)
  54.             this.redoList.splice(0,len);
  55.     }

  56.     //将状态压栈,保存,保存的是对象的副本。
  57.     pushRedoList(current,pieces){
  58.         this.redoList.push({
  59.             current:this.cloneObject(current),
  60.             pieces:this.cloneObject(pieces)
  61.         });
  62.     }

  63.     //弹出队列中的元素,复制一个副本
  64.     popList(list) {
  65.         var record = list.pop();
  66.         return {
  67.             current: this.cloneObject(record.current),
  68.             pieces: this.cloneObject(record.pieces)
  69.         }
  70.     }

  71.     //输出链表内容,用于调试
  72.     printList(list){
  73.         for(var i=0; i<list.length; i++){
  74.             console.log("第%d步:%s",i,this.getVisiblePieces(list[i].pieces));
  75.         }

  76.     }

  77.     getVisiblePieces(pieces){
  78.         var info = "";
  79.         for(var j=0; j<19*19; j++){
  80.             if(pieces[j].visibility=='visible'){
  81.                 info = info+pieces[j].num+',';
  82.             }
  83.         }
  84.         //console.log("可见棋子序号:"+info);
  85.         return info;
  86.     }

  87.     //撤销
  88.     undo(){
  89.         if (this.undoList.length==0){
  90.             console.log("不能撤销了!");
  91.             return;
  92.         }

  93.         //当前状态压入RedoList
  94.         this.pushRedoList(this.current,this.pieces);

  95.         var record = this.popList(this.undoList)
  96.         this.current = record.current;
  97.         this.pieces = record.pieces;

  98.         this.pubCurrentChange();
  99.         this.pubPieceChange();
  100.     }

  101.     //前进一步
  102.     redo(){
  103.         if (this.redoList.length==0){
  104.             console.log("不能前进了!");
  105.             return;
  106.         }

  107.         this.pushUndoList(this.current,this.pieces);

  108.         var record = this.popList(this.redoList)
  109.         this.current = record.current;
  110.         this.pieces = record.pieces;

  111.         //this.printList(this.undoList);
  112.         //this.printList(this.redoList);

  113.         this.pubCurrentChange();
  114.         this.pubPieceChange();

  115.     }

  116.     //订阅当前状态变化事件
  117.     subCurrentChange(listener) {
  118.         //console.log("订阅状态事件!");
  119.         this.eventEmitter.addListener('currentChange',listener);
  120.     }

  121.     //状态当前变化事件发生,通知监听器
  122.     pubCurrentChange(){
  123.         //console.log("发布状态事件");
  124.         this.eventEmitter.emit("currentChange",this.current);
  125.     }

  126.     //订阅棋子状态变化事件
  127.     subPieceChange(listener) {
  128.         //console.log("订阅棋子事件!");
  129.         this.eventEmitter.addListener('pieceChange',listener);
  130.     }

  131.     //状态棋子变化事件发生,通知监听器
  132.     pubPieceChange(){
  133.         //console.log("发布棋子事件");
  134.         //传递数据,对解耦有一点儿帮助
  135.         this.eventEmitter.emit("pieceChange",this.pieces);
  136.     }

  137.     //推进当前状态到下一步
  138.     //这个函数内部调用
  139.     next(){
  140.         this.printList(this.undoList);

  141.         if(this.current.place==true){
  142.             //如果是布局状态,不改变编号、颜色
  143.         }
  144.         else{
  145.             this.current.index++;
  146.             this.current.goBlack=!this.current.goBlack;
  147.         }
  148.         this.pubCurrentChange();

  149.     }

  150.     //克隆对象
  151.     //数组被jquery复制后,变成了类数组(伪数组),不带有length方法,这个也比较坑
  152.     cloneObject(object){
  153.         return $.extend(true,{},object);//深层次复制。这个比较坑
  154.     }

  155.     //是否处于布局状态
  156.     isPlace(){
  157.         return this.current.place==true;
  158.     }

  159.     //设置布局状态
  160.     setPlace(bBlack){
  161.         this.current.place=true;
  162.         this.current.placeBlack=(bBlack==true);
  163.         this.pubCurrentChange();
  164.     }

  165.     //重新开始
  166.     restart(){
  167.         this.initState();
  168.         this.initList();
  169.         this.pubCurrentChange();
  170.         this.pubPieceChange();
  171.     }

  172.     //返回当前的步数
  173.     getCurrentIndex(){
  174.         return this.current.index;
  175.     }

  176.     //返回当前状态
  177.     getCurrent(){
  178.         return this.current;
  179.     }

  180.     //返回所有棋子状态
  181.     getPieces(){
  182.         return this.pieces;
  183.     }

  184.     //设置先行方
  185.     setFirst(bBlack) {
  186.         this.current.place = false;
  187.         //仅处于第一步时,可以改变行棋的黑白颜色
  188.         if( this.current.index==1){
  189.             this.current.goBlack=(bBlack==true);
  190.         }
  191.         this.pubCurrentChange();

  192.     }

  193.     //在棋上点击
  194.     //如果棋子状态变化,则返回为true,否则返回false
  195.     clickPiece(index){
  196.         //每次下一步之前的状态都记下来,以便能够回退
  197.         this.pushUndoList(this.current,this.pieces);
  198.         this.clearRedoList();

  199.         var state=this.pieces[index];//应该传递的是指针,相当于起了个别名,实际对应同一块内存地址

  200.         if (state.visibility=='visible' && this.isPlace()==false){//棋子可见、非布局状态
  201.             console.log("棋子可见、非布局状态,退出!");
  202.             return false;
  203.         }
  204.         if (state.visibility=='visible' && this.isPlace()==true && state.num!=0){//棋子可见、布局状态,且非布局棋子
  205.             console.log("棋子可见、布局状态,且非布局棋子,退出!");
  206.             return false;
  207.         }

  208.         //console.log('可以修改棋子状态');
  209.         if (this.isPlace()==false){//行棋中
  210.             state.num = this.current.index;
  211.             state.black = this.current.goBlack;
  212.             state.visibility = 'visible';
  213.         }
  214.         else{//布局
  215.             state.num=0;//布局状态下,放的棋子,其数字设置为0
  216.             //如果原来棋子已经显示,且颜色相同,用是布局摆的棋子,设置其隐藏
  217.             if(state.visibility=='visible' && this.current.placeBlack==state.black && state.num==0){
  218.                 state.visibility = 'hidden';
  219.                 //棋子颜色不重要,下次再显示时,会设置颜色
  220.             }
  221.             else{
  222.                 state.black = this.current.placeBlack;
  223.                 state.visibility = 'visible';
  224.             }
  225.         }

  226.         this.pubPieceChange();
  227.         //StateManager.setPieceState(this.state.pieceId,state);//这里有点儿乱
  228.         //这个放最后,完成大大压栈工作
  229.         this.next();
  230.         //this.setState(state);
  231.         return true;
  232.     }

  233. }

  234. module.exports = new GoStateManager();
复制代码

  二、组件文件Go.js
  1. //http://wallimn.iteye.com
  2. var React = require('react');
  3. var ReactDOM = require('react-dom');
  4. require('../../../css/go.css');
  5. var StateManager = require('../../store/main/GoStateManager');
  6. "user strick"
  7. //当前步状态指示器,可以指标当前步数、落子方、是否处理布局状态等信息
  8. class CurrentLabel extends React.Component{
  9.         constructor(props){
  10.                 super(props);
  11.                 //使用全局的状态作为初始状态
  12.                 var current = StateManager.getCurrent();
  13.                 this.state={
  14.                         index:current.index,
  15.                         goBlack:current.goBlack,
  16.                         placeBlack:current.placeBlack,
  17.                         place:current.place,
  18.                 };
  19.                 //设置currentChange函数的this
  20.                 this.currentChange=this.currentChange.bind(this);
  21.                 //注册事件监听器
  22.                 StateManager.subCurrentChange(this.currentChange);
  23.         }

  24.         //状态改变事件监听器,调整组件的状态
  25.         currentChange(current){
  26.                 this.setState({
  27.                         index:current.index,
  28.                         goBlack:current.goBlack,
  29.                         placeBlack:current.placeBlack,
  30.                         place:current.place,
  31.                 });
  32.         }

  33.         render(){
  34.                 return <span className="bg-success">
  35.             <strong>当前步数:</strong>{this.state.index}
  36.                         <strong> 落子方:</strong>{this.state.goBlack==true?'黑方':'白方'}
  37.                         <strong> 布局子:</strong>{this.state.placeBlack==true?'黑子':'白子'}
  38.                         <strong> 状态:</strong>{this.state.place==true?'布局':'行棋'}
  39.         </span>;
  40.         }
  41. }

  42. //围棋桌面
  43. class GoDesk extends React.Component {
  44.     constructor(props) {
  45.         super(props);
  46.         this.state = {
  47.             refresh: false
  48.         };
  49.     }

  50.     render() {
  51.         var self = this;
  52.         this.state.refresh=false;
  53.         var pieces = [];
  54.                 //每个交叉点上都放一个子,只是未点击时不显示,棋子黑白、编号都不重要,用户点击时会修改
  55.         for (var i=0; i<19*19; i++){
  56.                 pieces.push(
  57.                         <GoPiece black={i % 2==0 ?true:false} key={'go'+(i+1)} pieceId={i}/>
  58.                 );
  59.         }
  60.         return <div className="go-desk">
  61.                         <div className="go-opr">
  62.                                 <GoBtns />
  63.                         </div>
  64.                 <div className="go-board">
  65.                                 {pieces}                                       
  66.                 </div>
  67.                         <div className="text-center">
  68.                                 <CurrentLabel />
  69.                         </div>
  70.         </div>;
  71.     }
  72. }

  73. //使用bootstap的按钮组,可以不用控制按钮的状态,较为方便,还没有完全走通
  74. //使用radio按钮组实现几个控制行棋的按钮,因为只能处于其中一个状态
  75. class GoBtns extends React.Component{
  76.         constructor(props){
  77.                 super(props);
  78.                 this.state={index:1};//指标按钮的激活状态,没有完成
  79.                 this.setFirstClickHandle=this.setFirstClickHandle.bind(this);
  80.                 this.setPlaceClickHandle=this.setPlaceClickHandle.bind(this);
  81.                 this.newClickHandle=this.newClickHandle.bind(this);
  82.                 this.saveClickHandle=this.saveClickHandle.bind(this);
  83.                 this.loadClickHandle=this.loadClickHandle.bind(this);

  84.                 this.redoClickHandle=this.redoClickHandle.bind(this);
  85.                 this.undoClickHandle=this.undoClickHandle.bind(this);
  86.         }

  87.         //这个还没有验证
  88.         getActiveBtnIndex(){
  89.                 if (StateManager.current.place==false) return 1;//黑先、白先差别不大,似乎没有影响
  90.                 else if (StateManager.current.placeBlack)return 2;
  91.                 else return 3;
  92.         }
  93.         //设置黑先
  94.         setFirstClickHandle(bBlack){
  95.                 console.log("设置落子方颜色:"+bBlack);
  96.                 StateManager.setFirst(bBlack);
  97.         }

  98.         setPlaceClickHandle(bBlack){
  99.                 console.log("设置布局子颜色:"+bBlack);
  100.                 StateManager.setPlace(bBlack);
  101.         }

  102.         newClickHandle(){
  103.                 if (confirm('您确定要新建布局吗?')==true){
  104.                         StateManager.restart();
  105.                 }
  106.         }

  107.         saveClickHandle(){
  108.                 alert("暂示实现");
  109.         }

  110.         loadClickHandle(){
  111.                 alert("暂示实现");
  112.         }
  113.         redoClickHandle(){
  114.                 StateManager.redo();
  115.         }

  116.         undoClickHandle(){
  117.                 StateManager.undo();
  118.         }
  119.         render(){
  120.                 return <div>
  121.                                         <span>
  122.                                                 <div className="btn-group" data-toggle="buttons">
  123.                                                   <label className="btn btn-sm btn-default active" onClick={this.setFirstClickHandle.bind(this,true)}><input type="radio" autoComplete="off" defaultChecked title="黑方先走" />黑先</label>
  124.                                                   <label className="btn btn-sm btn-default" onClick={this.setFirstClickHandle.bind(this,false)}><input type="radio" autoComplete="off" />白先</label>
  125.                                                   <label className="btn btn-sm btn-default" onClick={this.setPlaceClickHandle.bind(this,true)}><input type="radio" autoComplete="off" />黑子</label>
  126.                                                   <label className="btn btn-sm btn-default" onClick={this.setPlaceClickHandle.bind(this,false)}><input type="radio" autoComplete="off" />白子</label>
  127.                                                 </div>
  128.                                         </span>

  129.                                         <span>
  130.                                                 <button className="btn btn-sm btn-default" onClick={this.newClickHandle}>新建</button>
  131.                                                 <button className="btn btn-sm btn-default" onClick={this.saveClickHandle}>保存</button>
  132.                                                 <button className="btn btn-sm btn-default" onClick={this.loadClickHandle}>打开</button>
  133.                                         </span>

  134.                                         <span>
  135.                                                 <button className="btn btn-sm btn-default" onClick={this.undoClickHandle}>撤销</button>
  136.                                                 <button className="btn btn-sm btn-default" onClick={this.redoClickHandle}>重做</button>
  137.                                         </span>
  138.                                 </div>;
  139.         }
  140. }

  141. //棋子
  142. class GoPiece extends React.Component{
  143.         constructor(props){
  144.                 super(props);
  145.                 var pieceId = props.pieceId;
  146.                 var pieceState = StateManager.getPieces()[pieceId];
  147.                 this.state={
  148.                         showNum:true,//是否显示数字,这个应该是个全局参数
  149.                         num:pieceState.num,//子上显示的数字,如果为零,表示布局时摆的子,不显示数字
  150.                         black:pieceState.black,//true表示为黑
  151.                         last:false,//是否是最后一个子
  152.                         pieceId:pieceId,//棋子的ID,左上为0,从左到右、从上到下,赋值后不发生变化
  153.                         visibility:pieceState.visibility,//不可见时,为未放子或者被吃掉,从全局变量中取,
  154.                 }
  155.                
  156.                 //设置this,很重要
  157.                 this.handleClick=this.handleClick.bind(this);
  158.                 this.pieceChange=this.pieceChange.bind(this);
  159.                 StateManager.subPieceChange(this.pieceChange);
  160.         }

  161.         pieceChange(piecesArray){
  162.                 //React会判断UI要不要更新,全部更新,不要紧
  163.                 this.setState({
  164.                                 visibility:piecesArray[this.state.pieceId].visibility,
  165.                                 black:piecesArray[this.state.pieceId].black,
  166.                                 num:piecesArray[this.state.pieceId].num,
  167.                                 showNum:StateManager.current.numShow,
  168.                                 last:piecesArray[this.state.pieceId].num==StateManager.current.index,//没有想好如何判断
  169.                 });
  170.         }

  171.         //这个函数不直接改变自己组件的状态
  172.         handleClick(){
  173.                 StateManager.clickPiece(this.state.pieceId);
  174.         }
  175.         
  176.         render(){
  177.                 var className="go-piece go-piece-"+(this.state.black==true?'black':'white');
  178.                 //console.log(this.state);
  179.                 if (this.state.visibility=='hidden') className = className+" go-piece-hidden";

  180.                 var pieceNum = this.state.num==0?'':this.state.num;
  181.                 return <div className={className} onClick={this.handleClick} id={'piece_'+this.state.pieceId}>
  182.                         <span style={{visibility:this.state.visibility}}>{pieceNum}</span>
  183.                 </div>;
  184.         }
  185. }

  186. ReactDOM.render(
  187.   <GoDesk />,
  188.   document.getElementById('go-container')
  189. );
复制代码

  三、样式文件go.css
  1. html,body{
  2.         height:100%;
  3. }
  4. .go-desk{
  5.         background-image:url(../img/go/bk.png);
  6.         width:100%;
  7.         height:100%;
  8.         padding:20px;
  9. }

  10. .go-opr{
  11.         height:30px;
  12.         text-align:center;
  13.         margin-bottom:1em;
  14. }
  15. .go-opr span{
  16.         margin:0 0.5em;
  17. }
  18. .go-opr span button{
  19.         margin:0 0.05em;
  20. }

  21. .go-board{
  22.         width:800px;
  23.         height:800px;
  24.         margin:0 auto;
  25.         background-image:url(../img/go/board.png);
  26.         background-repeat:no-repeat;
  27.         padding:20px;
  28. }

  29. .go-piece{
  30.         width:40px;
  31.         height:40px;
  32.         float:left;
  33.         background-image:url(../img/go/piece.png);
  34.         text-align:center;
  35.         line-height:40px;
  36.         vertical-align:middle;
  37.         font-size:20px;
  38. }
  39. .go-piece span{
  40. }
  41. .go-piece-white{
  42.         background-position:-40px 0;
  43.         color:black;
  44. }
  45. .go-piece-black{
  46.         background-position:0 0;
  47.         color:white;
  48. }
  49. .go-piece-hidden{
  50.         background-image:none;

  51. }
复制代码

  四、网页文件go.html
  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4.     <meta charset="utf-8"/>
  5.     <title><%= htmlWebpackPlugin.options.title%></title>
  6. </head>
  7. <body>
  8.     <div id="go-container"></div>
  9. </body>
  10. </html>
复制代码

  编译好的文件,可以单机基于浏览器运行。所有源码已经托管到码云,访问地址:https://git.oschina.net/wallimn/rwne.git。把插件也传上去了,有点儿大。

  下篇

相关帖子

发表于 2017-1-5 10:27:02 | 显示全部楼层
我完全是被标题<<React学习之围棋记谱本制作(四)前端开发初步完成>>吸引过来的
使用道具 举报

回复

发表于 2017-1-5 15:13:04 来自手机 | 显示全部楼层
无论是不是沙发都得回复下
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部