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页面是一片空白。