使用React重写SprintPoker前端

By | 2019年1月6日

SprintPoker是一个简单的,为Sprint Task分配Story Point的网页。主要功能:

  • 创建房间,每个team在各自的房间讨论。
  • 在所有人完成打分之前,隐藏每个人的打分。
  • 只有房主可以清空打分,重新开始。
  • 房主离开,第二个进入的人自动成为房主。
  • 所有人离开房间,房间自动消失。

后台是Tomcat发布的servlet,前后端使用WebSocket进行通信。前端只有一个文件,javascript代码嵌入式的写在HTML页面里面。整个文件435行,去掉空格大概400行左右。

项目地址:https://github.com/tadckle/sprint-poker

单一的文件,导致项目不好维护。再者为了学习使用React,就决定用React重写一下前端,下面记录一下重写的过程。

重写后的项目地址:https://github.com/tadckle/sprint-poker-react

1. 创建工程

1.1 安装脚手架

create-react-app是基于最佳实践,将Webpack, Babel, ESlint等工具的配置做了封装,用create-react-app创建的项目无需进行任何配置,从而使开发者可以专注于应用发开。并且创建好的项目本身就是一个git仓库,已经将node_modules,build等动态生成的文件夹加入.gitignore,可以直接和GitHub账户关联,上传的GitHub进行管理。

npm install -g create-react-app

1.2 创建空项目

使用create-react-app在当前目录创建一个空的react project,命名为sprint-poker-react。然后进入创建的文件夹,进行后续的module安装。因为是新项目,对module版本没有依赖限制,所以没指定版本号,都安装最新的。

create-react-app sprint-poker-react

1.3 安装redux和react-redux

我们使用redux来管理状态。redux和react并无关联,redux可以和很多库一起使用。为了方便在react中使用redux,我们需要安装react-redux。

npm install redux
npm install react-redux

1.4 安装semantic-ui-react

Semantic UI React官方安装指南。

semantic-ui是一个有名的前端组件库,其官方发布了和React集成的组件库,方便在React项目中使用。p.s. 项目第一个版本采用的就是semantic-ui的css样式。

npm intall semantic-ui-react

semantic-ui-react并不包含样式,要单独安装。

npm install semantic-ui-css

然后在项目入口,如脚手架创建的App.js文件引入样式,所有组件就都可以使用该css样式了。

import 'semantic-ui-css/semantic.min.css'

1.5 总结

没有安装react-router-dom,就是说没有路由。一开始是想用路由拆分成多页面,但是每个页面会重新进行一次socket连接,导致之前页面socket记录的信息丢失。这个问题是由于后端数据全在内存,没有持久性的记录。

不想花费时间重写后端,就采用单页面进行重写。

2. 项目组织结构

项目的每个模块对应的css文件和controller文件,和模块本身放在相同的文件夹下,便于查找和管理。P.S. React官方应用是所有controller放在一个folder,所有action放在一个folder,所有css文件放在一个folder等,这种集中存放。

structure

4. state结构

因为项目比较小,state不是很多,所以只有一个Reducer。本项目的Reducer是’./src/actions/Reducer.js’,当项目比较复杂时,可以每个页面一个Reducer。

let initialState = {
    app: {
        page: pages.LOGIN,
        errorMsg: "",
        modalOpen: false
    },
    login: { name: "" },
    createRoom: { rooms: [] },
    pokerRoom: {
        roomId: "-1",
        isHost: false,
        fibonacciNum: -1,
        messages: [],
        players: [],
        hasVoted: false
    }
};

5. Component

每个Component都是由其对应的Controller创建的(分离View代码和业务逻辑代码),Controller用于给组件提供数据和调用函数。下面是Poker Room组件的Controller代码,其它组件都是类似。

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { UNKNOW_FIBONICA, setIsHost, setFibonacciNum, changePage, clearMessage,
     updateHasVoted, updateErrorMsg } from '../../actions/Reducer';
import PokerRoom from './PokerRoom';

function countAveragePoiont(players, isAllDone) {
    if (!isAllDone) {
        return "XXX";
    }
    let clearVotePlayers = players.filter(player => player.fibonacciNum !== UNKNOW_FIBONICA);
    if (clearVotePlayers.length <= 0) {
        return UNKNOW_FIBONICA;
    }
    let sum = clearVotePlayers.map(player => player.fibonacciNum)
            .reduce((num1, num2) => num1 + num2)
    let average = sum / clearVotePlayers.length;
    return Math.floor(average * 10) / 10;
}

function generateStatistics(players, isAllDone) {
    if (!isAllDone) {
        return [];
    }
    let playerGroup = [];
    players.forEach(player => {
        let playerItem = playerGroup.find(item => item.fibonacciNum === player.fibonacciNum);
        if (playerItem === undefined) {
            playerGroup.push({fibonacciNum: player.fibonacciNum, players: [player]});
        } else {
            playerItem.players.push(player);
        }
    })
    
    let statistics = [];
    playerGroup.forEach(group => {
        statistics.push({fibonacciNum: group.fibonacciNum, count: group.players.length})
    })
    statistics.sort((count1, count2) => count2.count - count1.count);
    return statistics;
}

const PokerRoomView = connect(state => {
    let players = state.pokerRoom.players;
    let isAllDone = players.filter(player => player.fibonacciNum === -1).length <= 0;
    return {
        roomId: state.pokerRoom.roomId,
        messages: state.pokerRoom.messages,
        players: state.pokerRoom.players,
        isHost: state.pokerRoom.isHost,
        hasVoted: state.pokerRoom.hasVoted,
        isAllDone,
        averagePoint: countAveragePoiont(players, isAllDone),
        statistics: generateStatistics(players, isAllDone)
    };
}, dispatch => {
    return {
        setIsHost: bindActionCreators(setIsHost, dispatch),
        setFibonacciNum: bindActionCreators(setFibonacciNum, dispatch),
        changePage: bindActionCreators(changePage, dispatch),
        clearMessage: bindActionCreators(clearMessage, dispatch),
        updateHasVoted: bindActionCreators(updateHasVoted, dispatch),
        updateErrorMsg: bindActionCreators(updateErrorMsg, dispatch)
    };
})(PokerRoom);

export default PokerRoomView;

6. SocketHandler.js

SocketHandler.js是socket对象的封装,用于发送数据给后台,以及接收后台发送的数据。下面展示其核心逻辑代码:

const socket = new WebSocket("ws://127.0.0.1:8080/SprintPoker/poker");

socket.onopen = function(event) {
    console.log("Client WebSocekt is opened.");
}

socket.onclose = function(event) {
    alert("Cannot establish connection with server.");
    socket.close();
    window.location.reload();
}

// Send message to server.
function sendMessage(cmdType) {
    let state = store.getState();
    let user = {
        name: state.login.name, 
        isHost: state.pokerRoom.isHost, 
        fibonacciNum: state.pokerRoom.fibonacciNum};
    let command = {type: cmdType, roomNum: state.pokerRoom.roomId, player: user};
    socket.send(JSON.stringify(command));
}

function sendChatText(message) {
    let state = store.getState();
    let user = {
        name: state.login.name, 
        isHost: state.pokerRoom.isHost, 
        fibonacciNum: state.pokerRoom.fibonacciNum};
    let command = {type: cmdTypes.CHAT, player: user, chatMsg: message};
    socket.send(JSON.stringify(command));
}

socket.onmessage = event => {
    var returnMsg = JSON.parse(event.data);
    if (returnMsg.actionType === cmdTypes.SHOW_LOGIN) {
        showDashBoard();
    } else if (returnMsg.actionType === cmdTypes.UPDATE_ROOMS) {
        updateAvailableRooms(returnMsg.rooms);
    } else  if (returnMsg.actionType === cmdTypes.CREATE_ROOM_DONE) {
        showRoom(returnMsg.roomId);
    } else if (returnMsg.actionType === cmdTypes.JOIN_ROOM_DONE) {
        showRoom(returnMsg.message);
    } else if (returnMsg.actionType === cmdTypes.CHAT) {
        updateChatMsg(returnMsg.name, returnMsg.chatMessage);
    } else if (returnMsg.actionType === cmdTypes.ALL_USER) {
        updatePokerData(returnMsg.players);
    } else if (returnMsg.actionType === cmdTypes.CLEAR) {
        store.dispatch(updateHasVoted(false));
    } else if (returnMsg.actionType === cmdTypes.SET_AS_HOST) {
        store.dispatch(setIsHost(true));
    } else if (returnMsg.actionType === cmdTypes.ERROR) {
        store.dispatch(updateErrorMsg(returnMsg.message));
    }
};

7. 发布

执行“npm run build”就可以打包程序了,会在项目根目录生成build文件夹,将build文件夹里的内容全部copy到tomcat\webapps里,就发布成功了。

如果不是发布在tomcat\webapps根目录,而是里面的子目录,需要稍微修改一下,然后重新build。

方法:{项目目录}\node_modules\react-scripts\config\path.js,找到getServedPath(…)方法,将‘/’修改为‘./’,表示是相对路径。

image

8. 其它

如果是多页面程序,发布到非Node环境,要配置服务器,让所有没有找到的路径,指向程序首页。因为React是前端路由,所有页面都是先进入首页,然后根据路径的不同由React Router进行跳转。如果不这样配置,除了首页以外,直接访问其它页面,报404页面找不到的错误。

Tomcat的配置:

{发布目录}\WEB-INF\web.xml文件,在<web-app/>节点里加入如下节点:

<error-page>
    <error-code>404</error-code>
    <location>/index.html</location>
</error-page>

脚手架创建的项目,配置文件说不支持<=11版本的IE,但Chrome肯定是支持的。我用IE 11试了下,进入到Poker Room页面是一片空白。

image