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集成的组件库,方便在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等,这种集中存放。
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(…)方法,将‘/’修改为‘./’,表示是相对路径。
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页面是一片空白。