【daydayup】weTalk

先看一下项目效果



这个是我运行的作者的项目的wetalk-server项目,他还有wetalk-client 项目
先放下作者的github项目地址:https://github.com/mangyui/weTalk
这是一个才华横溢的作者,而且才大三。羡慕作者的青春,也羡慕他们的努力,不需要预计的是作者的前途无量。
因为运行中有点问题,就给作者提了issue
(后记作者人很好,很快就解决了Bug,万分感谢)
先言归正传,看可以运行的wetalk-server项目吧
一般server中都会引入websocket
作者在服务端使用的是Node + WebSocket 搭配 Express
看package.json文件

{
  "name": "wetalk-server",
  "version": "1.0.0",
  "description": "we talk server",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "main": "nodemon build/main.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/express": "^4.16.1",
    "express": "^4.17.0",
    "typescript": "^3.4.5",
    "ws": "^7.0.0"
  },
  "devDependencies": {
    "@types/ws": "^6.0.1",
    "nodemon": "^1.19.0"
  }
}

入口文件main.ts

import express = require('express')
const app: express.Application = express();
app.use(express.static('public'));

const port:number = 9612

const WebSocket = require('ws')

app.get('/', function (req, res) {
  res.send('Hello World!');
});

var server = app.listen(port, '0.0.0.0', () => {
  console.log('Example app listening on port ' + port);
});

const wss = new WebSocket.Server({ server });
// Broadcast to all.
const broadcast = (data: string) => {
	console.log('ccc', wss.clients.size)
	var dataJson = JSON.parse(data)
	dataJson.number = wss.clients.size
	wss.clients.forEach((client: any) => {
		if (client.readyState === WebSocket.OPEN) {
			client.send(JSON.stringify(dataJson));
		}
	});
};
wss.on('connection', (ws: any) => {
	console.log(new Date().toUTCString() + ' - connection established');
	ws.on('message', (data: string) => {
		broadcast(data);
	});

	ws.on('error', (error: any) => {
		console.log(error);
	});

	wss.on('close', (mes: any) => {
		console.log(mes);
		console.log('closed');
	});
});

wss.on('error', (err: any) => {
	console.log('error');
	console.log(err);
});

接下来我们来看客户端的项目
运行效果同服务器中的是一样的,作者是将前端项目打包之后放在后端public里面的。
先上代码
在main.js中会引入router以及对应的museui等框架

import Vue from 'vue'
import App from './App.vue'
import router from './router/'
import store from './store/'
import './plugins/museui.js'
import '@/styles/index.less'
import './guard.ts'
const Toast = require('muse-ui-toast')

Vue.config.productionTip = false
// Vue.use(Toast)

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
}).$mount('#app') // 与el: '#app'对应,手动挂载

guard.ts中进行了路由守卫的一些功能,能够判断当时对应的用户的信息

//guard.ts
import router from './router'
import store from './store'
import User from './model/user'
import Person from './assets/js/person'
let persons : Person[] = require('./assets/js/persons').persons //
// const whiteList = ['/login',
//   '/'
// ] // 不重定向白名单

router.beforeEach((to, from, next) => {
  console.log('....store.getters.user',store.getters.user)
  console.log('....store.getters.user.id',store.getters.user.id)
  if (!store.getters.user.id) {
    console.log('id.....', store.getters.user)
    var date = new Date(+new Date() + 8 * 3600 * 1000).toISOString().replace(/[T:-]/g, '').replace(/.[d]{3}Z/, '')
    var index = Math.floor(Math.random() * persons.length)
    var user = new User(date.substring(2) + index, persons[index].name, persons[index].avatar, '男')
    store.commit('initUserInfo', user)

    next()
    // if (whiteList.indexOf(to.path) !== -1) {
    //   next()
    //   console.log('aaaaaaaa')
    // } else {
    //   console.log('bnbbbb')
    //   next('/')
    // }
  } else {
    console.log('cccc')
    next()
  }
})

接下来看router中的文件,router中进行懒加载,以及对应的跳转页面信息

//src
outerindex.ts
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/views/Home.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '',
      component: () => import('@/views/Home.vue'),
      redirect: '/home/lobby'
    },
    {
      path: '/home',
      name: 'home',
      component: () => import('@/views/Home.vue'),
      children: [
        {
          path: 'lobby',
          name: 'Lobby',
          component: () => import('@/components/Lobby.vue')
        },
        {
          path: 'usercenter',
          name: 'UserCenter',
          component: () => import('@/components/UserCenter.vue')
        }
      ]
    },
    {
      path: '/WorldRoom',
      name: 'WorldRoom',
      component: () => import('@/views/WorldRoom.vue')
    },
    {
      path: '*',
      redirect: '/'
    }
  ]
})

在App.vue里面定义了页面渲染的入口

<template>
  <div id="app">
    <!-- <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div> -->
    <router-view />
  </div>
</template>

<style lang="less">
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#nav {
  padding: 30px;
  a {
    font-weight: bold;
    color: #2c3e50;
    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>

接下来我们看各个页面效果

这个是在home.vue

<template>
  <div class="home">
    <router-view />
    <mu-bottom-nav :value.sync="tab" @change="changeTab">
      <mu-bottom-nav-item value="labby" title="大厅" icon="home"></mu-bottom-nav-item>
      <mu-bottom-nav-item value="usercenter" title="我" icon="account_circle"></mu-bottom-nav-item>
    </mu-bottom-nav>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component({
})
export default class Home extends Vue {
  private tab: string = 'labby'
  changeTab () {
    if (this.tab === 'labby') {
      this.$router.push('/home/lobby')
    } else {
      this.$router.push('/home/usercenter')
    }
  }
  created () {
    if (this.$route.name === 'UserCenter') {
      this.tab = 'usercenter'
    }
  }
}
</script>

<style lang="less">
.about{
  .mu-bottom-nav-shift-wrapper{
    justify-content: space-around;
  }
  .mu-paper-round{
    height: calc(100% - 56px);
  }
}
</style>

页面一开始进来会渲染的是lobby组件,但是也会加载UserCenter组件里面的内容

这部分是lobby组件渲染的,也是纯的UI组件,不过有路由跳转的功能,可以跳到另一个页面

<template>
  <div>
    <mu-paper :z-depth="0" class="">
      <mu-appbar :z-depth="0" color="lightBlue400">
        <mu-button icon slot="left">
          <mu-icon value="menu"></mu-icon>
        </mu-button>
        大厅
        <mu-button icon slot="right">
          <mu-icon value="add"></mu-icon>
        </mu-button>
      </mu-appbar>
      <mu-list>
        <router-link to="/WorldRoom">
          <mu-list-item avatar button :ripple="false">
            <mu-list-item-action>
              <mu-avatar color="#2196f3">
                <mu-icon value="public"></mu-icon>
              </mu-avatar>
            </mu-list-item-action>
            <mu-list-item-title>世界聊天室</mu-list-item-title>
            <mu-list-item-action>
              <mu-icon value="chat_bubble" color="#2196f3"></mu-icon>
            </mu-list-item-action>
          </mu-list-item>
        </router-link>
        <mu-list-item avatar button :ripple="false">
          <mu-list-item-action>
            <mu-avatar color="#2196f3">
              <mu-icon value="group_add"></mu-icon>
            </mu-avatar>
          </mu-list-item-action>
          <mu-list-item-title>多人聊天室</mu-list-item-title>
          <mu-list-item-action>
            <mu-icon value="speaker_notes_off"></mu-icon>
          </mu-list-item-action>
        </mu-list-item>
        <mu-list-item avatar button :ripple="false">
          <mu-list-item-action>
            <mu-avatar color="#2196f3">
              <mu-icon value="people"></mu-icon>
            </mu-avatar>
          </mu-list-item-action>
          <mu-list-item-title>双人聊天室</mu-list-item-title>
          <mu-list-item-action>
            <mu-icon value="speaker_notes_off"></mu-icon>
          </mu-list-item-action>
        </mu-list-item>
        <mu-list-item avatar button :ripple="false">
          <mu-list-item-action>
            <mu-avatar color="#2196f3">
              <mu-icon value="person"></mu-icon>
            </mu-avatar>
          </mu-list-item-action>
          <mu-list-item-title>自言自语室</mu-list-item-title>
          <mu-list-item-action>
            <mu-icon value="speaker_notes_off"></mu-icon>
          </mu-list-item-action>
        </mu-list-item>
      </mu-list>
    </mu-paper>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Lobby extends Vue {
}
</script>

<style lang="less" scoped>
.mu-list{
  background: #fff;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
}
</style>

这个就是跳转到的世界聊天室页面

这里使用的是WorldRoom组件

//srcviewsWorldRoom.vue
<template>
  <div class="talk-room">
    <mu-paper :z-depth="0" class="demo-list-wrap">
      <mu-appbar :z-depth="0" color="cyan">
        <mu-button icon slot="left" @click="toback">
          <mu-icon value="arrow_back"></mu-icon>
        </mu-button>
        世界聊天室
        <mu-badge class="barBadge" :content="number" slot="right" circle color="secondary">
          <mu-icon value="person"></mu-icon>
        </mu-badge>
      </mu-appbar>
      <div class="mess-box">
        <div class="mess-list">
          <div class="list-item" v-for="(item,index) in msgList" :key="index">
            <div class="mess-item" v-if="item.type==1&&item.user.id!=user.id">
              <mu-avatar>
                <img :src="item.user.avatar">
                <img class="icon-sex" :src="item.user.sex=='男'?require('@/assets/img/male.svg'):require('@/assets/img/female.svg')" alt="">
              </mu-avatar>
              <div class="mess-item-right">
                <span>{{item.user.name}}</span>
                <p class="mess-item-content">{{item.content}}</p>
                <p class="mess-item-time">{{item.time}}</p>
              </div>
            </div>
            <div class="mess-item-me" v-else-if="item.type==1&&item.user.id==user.id">
              <mu-avatar>
                <img :src="user.avatar">
                <img class="icon-sex" :src="user.sex=='男'?require('@/assets/img/male.svg'):require('@/assets/img/female.svg')" alt="">
              </mu-avatar>
              <div class="mess-item-right">
                <span>{{user.name}}</span>
                <mu-menu cover placement="bottom-end">
                  <p class="mess-item-content">{{item.content}}</p>
                  <mu-list slot="content">
                    <mu-list-item button @click="backMess(index)">
                      <mu-list-item-title>撤销</mu-list-item-title>
                    </mu-list-item>
                  </mu-list>
                </mu-menu>
                <p class="mess-item-time">{{item.time}}</p>
              </div>
            </div>
            <div class="mess-system" v-else>
              {{item.content}}
            </div>
          </div>
        </div>
      </div>
    </mu-paper>
    <div class="talk-bottom">
      <div class="talk-send">
        <textarea v-model="sendText" @keyup.enter="toSend" rows="1" name="text"></textarea>
        <mu-button @click="toSend" color="primary" :disabled="sendText==''?true:false">发送</mu-button>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import Message from '../model/message'
import User from '../model/user'
import { Component, Vue } from 'vue-property-decorator'
import { mapState, mapMutations, mapGetters } from 'vuex'

@Component({
  computed: {
    ...mapGetters(['user', 'msgList'])
  },
  methods: {
    ...mapMutations(['addMsg'])
  }
})
export default class WorldRoom extends Vue {
  sendText: string = ''
  number: string = '0' // ui组件要string型的
  // mesgLists: Array<Object> = []
  ws: any
  private user: User = this.$store.getters.user
  private msgList: Message[] = this.$store.getters.msgList
  public createWebsocket () {
    // this.ws = new WebSocket('ws://' + window.location.host)
    // 创建websocket
    this.ws = new WebSocket('ws://' + 'localhost:9612')
    // 进入聊天室事件
    this.ws.onopen = (e: any) => {
      // console.log('connection established')
      this.creatSending(this.user.name + ' 进入聊天室', 0)
    }

    this.ws.onmessage = (e: any) => {
      // console.log(e)
      // 发送事件
      var resData = JSON.parse(e.data)
      // console.log(message.user, this.user, message.user === this.user)
      // this.mesgLists.push({ message })
      console.log('resData', resData)
      // 移除事件
      if (resData.isRemove) {
        // 删除消息
        this.$store.commit('removeMsg', resData.message)
      } else {
        // 添加消息
        this.$store.commit('addMsg', resData.message)
      }

      if (resData.message.type === -1) {
        this.number = (resData.number - 1) + ''
      } else {
        this.number = resData.number + ''
      }
      this.$nextTick(() => {
        try {
          const msgEl = document.querySelector('.mess-list .list-item:last-child')
          if (msgEl) {
            msgEl.scrollIntoView()
          }
        } catch (err) {
          console.error(err)
        }
      })
    }
  }
  backMess (index: number) {
    this.backoutMess(this.msgList[index])
  }
  // 撤回消息
  backoutMess (message: Message) {
    console.log('Message', Message)
    var data = {
      message: message,
      isRemove: true
    }
    this.ws.send(JSON.stringify(data))
  }
  // 发送消息
  creatSending (content: string, type: number) {
    // 发送消息时间
    var time = new Date(+new Date() + 8 * 3600 * 1000).toISOString().replace(/T/g, ' ').replace(/.[d]{3}Z/, '')
    var message = new Message(time, content, type, type === 1 ? this.user : null)
    var data = {
      message: message
    }
    this.ws.send(JSON.stringify(data))
  }
  toSend () {
    if (this.sendText !== '') {
      this.creatSending(this.sendText, 1)
      this.sendText = ''
    }
  }
  // 返回
  toback () {
    this.$router.push('/')
  }
  created () {
    // 页面进来创建websocket连接
    this.createWebsocket()
  }
  // 销毁阶段
  beforeDestroy () {
    this.creatSending(this.user.name + ' 退出聊天室', -1)
    this.ws.close()
  }
}
</script>

<style lang="less">
.mu-paper-round{
  background: #fafafa;
}
.mess-box{
  text-align: left;
  padding: 0 10px 10px;
  height: calc(100% - 37px);
  overflow: auto;
  .mess-system{
    text-align: center;
    margin: 9px 0;
    font-size: 12px;
    color: #aaa;
  }
  .mess-item,.mess-item-me{
    display: flex;
    align-items: top;
    padding-right: 40px;
    margin: 10px 0;
    .mu-avatar{
      flex-shrink: 0;
      position: relative;
      .icon-sex{
        position: absolute;
        right: -4px;
        bottom: -8px;
         20px;
        background: #fff;
        height: 20px;
      }
    }
    .mess-item-right{
      margin-left: 15px;
      margin-right: 15px;
      flex-grow: 1;
       0;
      span{
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        display: block;
        font-size: 13px;
      }
      p.mess-item-content{
        margin: 0;
        font-size: 14px;
        padding: 10px 14px;
        background: #fff;
        border-radius: 3px;
        box-shadow: 0 1px 1px rgba(0,0,0,0.1);
        position: relative;
        &::after{
          display: block;
          content: '';
          border: 7px solid;
          border- 5px 7px;
          position: absolute;
          top: 2px;
          right: 100%;
          border-color: transparent #fff transparent transparent;
        }
      }
      p.mess-item-time{
        margin: 0;
        text-align: right;
        font-size: 12px;
        color: #777;
        letter-spacing: 0.8px;
      }
    }
  }
  .mess-item-me{
    flex-direction: row-reverse;
    padding-left: 40px;
    padding-right: 0px;
    .mess-item-right{
      .mu-menu{
        display: block;
      }
      span{
        text-align: right
      }
      p.mess-item-content{
        background: #2196f3;
        color: #fff;
        &:after{
          right: unset;
          left: calc(100% - 0.5px);
          border-color: transparent transparent transparent #2196f3;
        }
      }
      p.mess-item-time{
        text-align: left
      }
    }
  }
}
.talk-room{
  .mu-paper-round{
    height: calc(100% - 56px);
  }
}
.talk-bottom{
  position: fixed;
  bottom: 0;
   100%;
  .talk-send{
    display: flex;
    padding: 5px 5px;
    align-items: flex-end;
    background: #fefefe;
    box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.1);
    textarea{
      flex-grow: 1;
      min-height: 36px;
      max-height: 240px;
      border: 1px solid #cccc;
      border-radius: 2px;
      margin-right: 5px;
    }
  }
}
</style>

接下来我们看usercenter页面

<template>
  <div class="usercenter">
    <div class="avatar-box" :style="{backgroundImage:'url(' + require('@/assets/img/user_bg' + bgindex+ '.svg')+')'}">
      <mu-avatar :size="75" color="#00bcd4">
        <img :src="user.avatar">
      </mu-avatar>
      <mu-button icon large  color="#eee" @click="refreshUser">
        <mu-icon value="refresh"></mu-icon>
      </mu-button>
    </div>
    <div class="info">
      <div class="info-item">
        <span>昵称:</span>
        <mu-text-field v-model="user.name" :max-length="10"></mu-text-field>
      </div>
      <div  class="info-item">
        <span>性别:</span>
        <mu-flex class="select-control-row">
          <mu-radio v-model="user.sex" value="男" label="男"></mu-radio>
          <mu-radio v-model="user.sex" value="女" label="女"></mu-radio>
        </mu-flex>
      </div>
      <!-- <div  class="info-item">
        <span>序号:</span>
        <p>{{user.id}}</p>
      </div> -->
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import User from '../model/user'
import Person from '@/assets/js/person'
let persons : Person[] = require('@/assets/js/persons').persons

@Component
export default class UserCenter extends Vue {
  private user: User = this.$store.getters.user
  private bgindex: number = Math.floor(Math.random() * 6)
  // 点击refreshUser图片会改变,对应的昵称的会改变
  refreshUser () {
    this.bgindex = Math.floor(Math.random() * 6)
    var index = Math.floor(Math.random() * persons.length)
    // 图片和名字刷新
    this.$store.commit('updateUserAvatar', persons[index].avatar)
    this.$store.commit('updateUserName', persons[index].name)
  }
  beforeDestroy () {
    this.$store.commit('initUserInfo', this.user)
  }
}
</script>

<style scoped lang="less">
.avatar-box{
  padding: 45px 5px;
  background: #222;
  position: relative;
  // background-image: url('../assets/img/user_bg0.svg')
  .mu-avatar{
    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.25);
  }
  .mu-button{
    position: absolute;
    right: 0;
    bottom: 0;
  }
}
.info{
  background: #fff;
   100%;
  max- 768px;
  margin: 15px auto;
  padding: 15px 5px;
  box-shadow: 0 1px 1px rgba(0,0,0,0.05);
  .info-item{
    display: flex;
    padding: 10px 5px;
    align-items: center;
    span{
       30%;
      color: #777;
    }
    p,.mu-input{
      margin: 0;
       auto;
      flex-grow: 1;
    }
  }
}
</style>

在model文件夹下,定义了字段的一些基本类型

//srcmodelmessage.ts
import User from './user'

class Message {
  public time: string
  public content: string = ''
  public type: number //  0 为系统消息(加入聊天室) -1(退出聊天室) 1为用户消息
  public user: User | null
  constructor (time: string, content: string, type: number, user: User | null) {
    this.time = time
    this.content = content
    this.type = type
    this.user = user
  }
}

export default Message
//srcmodel
oom.ts
class Room {
  private id: number
	public name: string = ''
	private number: number
	constructor (id: number, name: string, number: number) {
		this.name = name
		this.id = id
    this.number = number
    if (this.name === '') {
      this.name = '第'+this.id+'号聊天室'
    }
	}
}

export default Room
//srcmodeluser.ts
class User {
  public id: string // 当前以时间为id
  public name: string = ''
  public sex: string = ''
  private avatar : string
  constructor (id: string, name: string, avatar: string, sex: string) {
    this.id = id
    this.name = name
    this.avatar = avatar
    this.sex = sex
    if (this.name === '') {
      this.name = '游客'
    }
  }
  getUserId (): string {
    return this.id
  }
}

export default User

在store中,我们定义的是数据的状态
usercenter对应的状态,里面还使用了localstorage存储数据

//user.ts
import User from '../model/user'

export default {
  state: {
    user: JSON.parse(localStorage.getItem('user') || '{}') || {}
  },
  mutations: {
    updateUserAvatar (state: any, avatar: string) {
      state.user.avatar = avatar
      localStorage.setItem('user', JSON.stringify(state.user))
    },
    updateUserName (state: any, name: string) {
      state.user.name = name
      localStorage.setItem('user', JSON.stringify(state.user))
    },
    initUserInfo (state: any, user: User) {
      state.user = user
      localStorage.setItem('user', JSON.stringify(state.user))
    },
    logoutUser (state: any) {
      state.user = {}
      localStorage.setItem('user', JSON.stringify(state.user))
    }
  },
  actions: {}
}

room.ts中的为更新room的数量名字以及初始化和关闭等方法

import Room from '../model/room'

export default {
  state: {
    isLiving: true,
    room: {}
  },
  mutations: {
    closeOpenRoom (state: any, living: boolean) {
      state.isLiving = living
    },
    updateRoomNumber (state: any, number: number) {
      state.room.number = number
    },
    updateRoomname (state: any, username: string) {
      state.room.number = username
    },
    initRoomInfo (state: any, room: Room) {
      state.room = room
    }
  },
  actions: {}
}

message中为,可以添加message,也可以移除message

import Message from '../model/message'

export default {
  state: {
    msgList: JSON.parse(sessionStorage.getItem('msgList') || '[]') || []
  },
  mutations: {
    // 添加数据方法
    addMsg (state: any, msg: Message) {
      // 数据列表中添加数据
      state.msgList.push(msg)
      sessionStorage.setItem('msgList', JSON.stringify(state.msgList))
    },
    // 移除数据
    removeMsg (state: any, msg: Message) {
      let index = '-1'
      for (const key in state.msgList) {
        if (state.msgList.hasOwnProperty(key)) {
          if (state.msgList[key].time === msg.time && msg.user && state.msgList[key].user.id === msg.user.id) {
            console.log('key', state.msgList[key])
            index = key
          }
        }
      }
      // console.log('index', msg, new Message(state.msgList[3].time, state.msgList[3].content, state.msgList[3].type, state.msgList[3].user))
      console.log('index', index)
      if (index !== '-1') {
        let time = new Date(+new Date() + 8 * 3600 * 1000).toISOString().replace(/T/g, ' ').replace(/.[d]{3}Z/, '')
        let message = new Message(time, (msg.user ? msg.user.name : '用户') + ' 撤回了一条消息', 0, null)
        state.msgList.splice(index, 1, message)
        // state.msgList.push(msg)
        sessionStorage.setItem('msgList', JSON.stringify(state.msgList))
      }
    }
  }
}

我觉得我失去梦想了,哈哈哈哈,应该多看书,无论什么通过看书都可以去解决一些问题。
在一个地方久了,或者看到的风景太陈旧了,这个时候,应该给心灵来个旅行,应该在书中旅行。

原文地址:https://www.cnblogs.com/smart-girl/p/11357663.html