在一个月前,我们讨论了WebSocket的基本用途和基本用法。现在我们将使用SocketIO来实现在线聊天室的后台功能。使用SocketIO的原因是这个库是WebSocket的超集,能够兼容更多的浏览器,在使用方面上也比WebSocket更加的灵巧、方便。
开发思路
针对需求,我们可以设计出如下的运行流程:
- 客户端先展示登录界面,输入用户名与密码,此时暂定用户名与密码是相同的。
- 密码正确后进入选择房间的页面,点击其中一个房间的进入按钮,即可进入聊天室参与多人聊天。
经过分析,我们可以得到这样的开发思路:我们可以使用http库实现用户的登录与房间的进入,使用SocketIO库来实现多人在线聊天功能。这样我们可以整理出如下的路由以及通信事件:
- http路由:
login
:用来管理用户输入的账号密码
roomlist
:用来显示房间列表
- SocketIO通信事件:
login
:实现进入房间
sendMessage
:实现消息的发送
disconnect
:实现退出房间
以上就是这个在线聊天室的基本开发思路。在线聊天室的逻辑其实并不复杂,本文的主要目的就是为了实现TS、JS的混合编程。现在我们来看看如何使用TypeScript结合NodeJS来开发后台。
TypeScript
相较于JavaScript来说,TypeScript会有更加严谨的变量声明与参数类型限制,另外还增加了Java、C#中OOP的概念,比如加入了泛型、多态、接口等面向对象的概念,建议查看TypeScript的相关文档或视频,并且对JavaScript有一定基础的再来看这次的内容。
首先,我们先安装TypeScript。在工程文件夹下输入npm install -g typescript
命令即可全局安装TypeScript。
接下来我们运行自定义编译进程。我这次使用的是VSCode作为IDE,在工程文件夹下输入tsc --init
可以生成tsconfig.json
文件。将outDir
属性写为"./js"
,意思是将ts编译出来的js放在js文件夹中,并且可以按照目录来更新。然后在compilerOptions
外面添加一个同级属性exclude
,其内容是"[node_modules]"
。代码如下所示:
//tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./js",
"strict": true,
"esModuleInterop": true
},
"exclude": [
"node_modules"
]
}
在终端窗口点击运行任务,选择watch(监视)
,即可开启进程。watch
的目的是可以在ts保存修改后实现自动编译。
如果出现
error TS5058: The specified path does not exist: 'd:我的网页chatOnlineservertsconfig.json'.
终端进程已终止,退出代码: 1
在命令行中直接启动tsc -p "./tsconfig.json" --watch
即可。
后台实践
在这次实践中,我们将会定义三个类:Hall
、Room
、RoomMember
。Hall
是大厅类,Room
是房间类、RoomMember
是成员类。接下来我使用TypeScript先实现这三个类的属性、方法的定义以及实现。
先是RoomMember
类:
//RoomMember.ts
import {Room} from "./Room" //同于ES6的引入规范,不加ts后缀
class RoomMember {
private id:string; //私有变量
private username: string; //定义类型string
enterTime: Date = new Date(); //Date类型,没有private默认为public共有变量
enterRoomTime: Date = new Date();
inWhichRoom:Room | undefined = undefined; //定义类型Room类或undefined
socket:any; //可以为任意类型
constructor(id:string,username:string,socket:any=undefined){ //RoomMember的构造函数,socket为缺省值
this.id = id;
this.username = username;
this.socket = socket;
}
isInRoom():boolean { //函数返回是boolean类型
if(this.inWhichRoom===undefined){
return false;
} else {
return true;
}
}
getUserName():string{
return this.username;
}
getId():string{
return this.id;
}
getEnterRoomTime():Date{
return this.enterRoomTime;
}
getSocket():any{
return this.socket;
}
sendMsgToAnotherMember(id:string,event:string,msg:any):boolean{
if(id===this.id){
return false;
}
this.socket.emit(event,msg); //向指定用户发送信息msg
return true;
}
}
export {RoomMember}; //同ES6的导出方法
然后是Room
类:
//Room.ts
import {RoomMember} from './RoomMember';
class Room {
private id:string;
private name:string;
private createTime:Date=new Date;
private lastTime:Date=new Date();
private memberList:RoomMember[]=[]; //定义RoomMember数组类型
public desc:string='';
constructor(id:string,name:string=''){
this.id = id;
this.name = name;
}
getId():string{
return this.id;
}
getName():string{
return this.name;
}
getMemberNum():number{
return this.memberList.length;
}
getMemberList():RoomMember[]{
return this.memberList;
}
getCreateTime():Date{
return this.createTime;
}
getLastTime():Date{
return this.lastTime;
}
addMember(member:RoomMember):boolean{
member.enterRoomTime = new Date();
member.inWhichRoom= this; //成员的所在房间指向this
this.memberList.push(member); //将用户添加进房间成员数组中
this.lastTime = new Date();
return true;
}
deleteMember(id:string):boolean{
let member_num:number = this.getMemberNum();
for(let i=0;i<member_num;i++){
if(this.memberList[i].getId()===id){
this.memberList[i].inWhichRoom = undefined;
this.memberList.splice(i,1);
this.lastTime = new Date();
return true
}
}
return false;
}
sendMsgToAllRoomMember(event:string, msg:any):boolean{
this.memberList.forEach(member => {
member.getSocket().emit(event,msg); //向房间内所有用户发送信息msg
})
return true;
}
}
export {Room};
最后的Hall
你们来实现,其实也是十分简单的。
接下来实现TCP通信功能,首先我们先安装express
、socketio
,一般都是npm install <名字>
来安装,如果出现需要安装@type
版本的,那就安装那个。之后会生成package.json
文件,dependencies
属性为:
//package.json
//...
"dependencies": {
"@types/express": "^4.17.1",
"@types/socket.io": "^2.1.2",
"express": "^4.17.1", /* 可以删除 */
"socketio": "^1.0.0" /* 可以删除 */
},
//...
然后我们实现http的监听和socketio的监听。
//httpInit.ts
import express from "express";
import http from 'http';
import io from 'socket.io';
let ExpressApp:any = undefined;
let HttpServer:any = undefined;
function HttpInit(PORT:number):any{
ExpressApp = express();
ExpressApp.use(express.json());
//解决越界问题
ExpressApp.all('*', function (req:any, res:any, next:any) {
res.header('Access-Control-Allow-Origin', '*');
//Access-Control-Allow-Headers ,可根据浏览器的F12查看,把对应的粘贴在这里就行
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
HttpServer = http.createServer(ExpressApp);
HttpServer.listen(PORT);
return ExpressApp;
}
function SocketIOInit():any{
if(HttpServer===undefined){
console.error("HttpServer haven't been inited !");
return null;
}
return io(HttpServer);
}
export {HttpInit,SocketIOInit};
最后就是实现前后台交互的逻辑了。
//app.ts
import { HttpInit,SocketIOInit } from "./httpInit";
import { Hall } from "./modules/Hall";
import {Room} from "./modules/Room";
import {RoomMember} from './modules/RoomMember';
let PORT = 8081;
let ExpressApp = HttpInit(PORT);
console.log("app listen at " + PORT);
let io = SocketIOInit();
let hall = new Hall();
hall.addRoom('0','room0');
hall.addRoom('1','room1'); //现在先创建两个房间
/**************************HTTP************************/
//用户输入登录
ExpressApp.post('/login',(req:{body:{username:string,password:string}},res:any)=>{
if(req.body.password===req.body.username){
let newUser = new RoomMember(req.body.username,req.body.username);
if(hall.addMember(newUser)){
res.send({success:true});
}
} else {
res.send({success:false});
}
})
//用户获取房间列表
ExpressApp.get('/roomlist',(req:any,res:any)=>{
let sendData = {
roomlist : hall.getRoomList().map(room => {
return {
id: room.getId(),
name: room.getName(),
member_num: room.getMemberNum(),
create_time: room.getCreateTime(),
last_time: room.getLastTime(),
description: room.desc
};
})
};
res.send(sendData);
})
/***************************SOCKET************************/
//客户端创建socket连接
io.on('connection',function(socket:any) {
let user:RoomMember
let room:Room;
//客户端进入房间
socket.on('login', function(data:{username:string,room_id:string}){
console.log('login', data);
for(let i=0;i<hall.getRoomList().length;i++){
if(hall.getRoomList()[i].getId()===data.room_id){
room = hall.getRoomList()[i];
break;
}
}
for(let i=0;i<hall.getUserNum();i++){
if(hall.getUserList()[i].getId()===data.username) {
user = hall.getUserList()[i];
user.socket = socket;
break;
}
}
if(room.addMember(user)){
console.log('newUser', user.getUserName(),' enter '+data.room_id);
room.sendMsgToAllRoomMember('add',{username:user.getUserName()}) //给所有房间内成员发送本成员进入
socket.emit('loginSuccess', {userlist: room.getMemberList().map(user => {
return { username: user.getUserName() }; //给本成员发送所有房间成员列表
})});
} else {
socket.emit('loginFail', {});
}
});
//客户端发送信息
socket.on('sendMessage', (data:any) => {
socket.emit('sendMessageSuccess', {});
room.sendMsgToAllRoomMember('receiveMessage',data);
});
//客户端离开房间
socket.on('disconnect',()=>{
console.log('disconnect', user.getUserName());
room.deleteMember(user.getId());
room.sendMsgToAllRoomMember('leave',user.getUserName());
})
})
写完代码记得保存。这时在文件目录下系统自动生成js目录,里面就是编译好的ES5代码。
这时聊天室后台部分基本完成。关于测试方面留到前端部分再讲。
总体来说,在线聊天室的逻辑并不复杂,但是三个类的数据结构还是需要花费一定心思的,但也不是什么大问题。通过这次的实践,我们可以抽离出这三个类,实现任意的关于房间的项目,比如棋牌室游戏、大富翁游戏等等的基于房间的小游戏,有了一定的基础后,开发这类游戏就简单上手了。
大家学到了什么,有什么问题都可以来交流交流哦。o(*≧▽≦)ツ┏━┓