先看一下項目效果
這個是我運行的做者的項目的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.tsvue
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等框架node
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中進行了路由守衛的一些功能,可以判斷當時對應的用戶的信息git
//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中進行懶加載,以及對應的跳轉頁面信息github
//src\router\index.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裏面定義了頁面渲染的入口web
<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>
接下來咱們看各個頁面效果
vue-router
這個是在home.vuevuex
<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組件,不過有路由跳轉的功能,能夠跳到另外一個頁面typescript
<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>
這個就是跳轉到的世界聊天室頁面
express
這裏使用的是WorldRoom組件
//src\views\WorldRoom.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; width: 20px; background: #fff; height: 20px; } } .mess-item-right{ margin-left: 15px; margin-right: 15px; flex-grow: 1; width: 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-width: 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; width: 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; width: 100%; max-width: 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{ width: 30%; color: #777; } p,.mu-input{ margin: 0; width: auto; flex-grow: 1; } } } </style>
在model文件夾下,定義了字段的一些基本類型
//src\model\message.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
//src\model\room.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
//src\model\user.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)) } } } }
我以爲我失去夢想了,哈哈哈哈,應該多看書,不管什麼經過看書均可以去解決一些問題。 在一個地方久了,或者看到的風景太陳舊了,這個時候,應該給心靈來個旅行,應該在書中旅行。