基于Vue+Axios+NodeJS+Express+Socketio+TypeScript实现的vue在线聊天室——后台部分



  • 在一个月前,我们讨论了WebSocket的基本用途和基本用法。现在我们将使用SocketIO来实现在线聊天室的后台功能。使用SocketIO的原因是这个库是WebSocket的超集,能够兼容更多的浏览器,在使用方面上也比WebSocket更加的灵巧、方便。

    开发思路

    针对需求,我们可以设计出如下的运行流程:

    1. 客户端先展示登录界面,输入用户名与密码,此时暂定用户名与密码是相同的。
    2. 密码正确后进入选择房间的页面,点击其中一个房间的进入按钮,即可进入聊天室参与多人聊天。

    经过分析,我们可以得到这样的开发思路:我们可以使用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即可。

    后台实践

    在这次实践中,我们将会定义三个类:HallRoomRoomMemberHall是大厅类,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通信功能,首先我们先安装expresssocketio,一般都是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代码。
    0_1568032176823_e59c3ed7-6064-4792-b82a-b52681484fca-image.png
    这时聊天室后台部分基本完成。关于测试方面留到前端部分再讲。
    总体来说,在线聊天室的逻辑并不复杂,但是三个类的数据结构还是需要花费一定心思的,但也不是什么大问题。通过这次的实践,我们可以抽离出这三个类,实现任意的关于房间的项目,比如棋牌室游戏、大富翁游戏等等的基于房间的小游戏,有了一定的基础后,开发这类游戏就简单上手了。
    大家学到了什么,有什么问题都可以来交流交流哦。o(*≧▽≦)ツ┏━┓


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待