这个项目多是个虎头蛇尾的项目?跟我一块儿分析吧,比较简单的一个项目
另外,我也想跟本身说,我好像失去了那个努力的本身了。要珍惜时间,好好加油啊~
项目地址为:https://github.com/xiaobeila/vue-websocket.git
这个项目和其余的项目的区别是,这个项目里面将服务器端,即websocket.io直接与前端项目集成在一块儿了。
javascript
//app.js var app = require('express')() var http = require('http').Server(app) var io = require('socket.io')(http) // 设置跨域访问 app.all('*', function (req, res, next) { res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Headers', 'X-Requested-With') res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS') res.header('X-Powered-By', ' 3.2.1') res.header('Content-Type', 'application/json;charset=utf-8') next() }) /** * 路由配置 */ // 服务器根目录 app.get('/', function (req, res) { res.send('<h1>Welcome Realtime Server</h1>') }) // demo子目录 app.get('/demo', function (req, res) { res.send('<h1>Welcome Realtime Server - demo</h1>') }) // 在线用户 var onlineUsers = [] // 当前在线人数 var onlineCount = 0 /** * 创建socket连接 */ io.on('connection', function (socket) { console.log('a user connected') /** * 监听新用户加入 */ socket.on('login', function (obj) { // 将新加入用户的惟一标识看成socket的名称,后面退出的时候会用到 socket.name = obj.userId // 检查在线列表,若是不在里面就加入 if (!onlineUsers.hasOwnProperty(obj)) { onlineUsers.push(obj) onlineCount++// 在线人数+1 } // 向全部客户端广播用户加入 io.emit('login', { onlineUsers: onlineUsers, onlineCount: onlineCount, user: obj }) console.log(socket.handshake)// 打印握手信息 console.log(obj.userName + ' 登陆') }) /** * 监听用户退出 */ socket.on('disconnect', function () { console.log('[Leo]socket name => ', socket.name) // 将退出的用户从在线列表中删除 for (let i = 0, len = onlineUsers.length; i < len; i++) { let user = onlineUsers[i] if (user.userId == socket.name) { let tempUser = user onlineUsers.splice(i, 1) onlineCount-- io.emit('logout', { onlineUsers, onlineCount, user: tempUser }) console.log(user.userName + ' 退出登陆', JSON.stringify(tempUser)) break } } console.log('剩余在线用户 => ', JSON.stringify(onlineUsers)) }) /** * 监听用户发布聊天内容 */ socket.on('message', function (obj) { // obj数据结构例子 /* eslint-disable */ let testObj = { 'from': { 'userId': '123', 'userName': '123' }, 'to': { 'userId': '456', 'userName': '456' }, content: '聊天内容', sendtime: '2016年10月9日 11:25:05' } // 向全部客户端广播发布的消息 // io.emit('message', obj); io.emit(obj.to.userId, obj) console.log( obj.from.userName + ' 对 ' + obj.to.userName + ' 说 ' + obj.content ) }) }) http.listen(3000, function () { console.log('listening on *:3000') })
接下来咱们看客户端的代码css
//main.js import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import * as filters from './filters' import VueTimeago from 'vue-timeago' // VueTimeago组件时间还有i18n的功能 Vue.use(VueTimeago, { name: 'timeago', // component name, `timeago` by default autoUpdate: 1, maxTime: 86400, locale: 'zh-CN', locales: { 'zh-CN': require('date-fns/locale/zh_cn'), 'ja': require('date-fns/locale/ja') } }) Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]) }) Vue.config.productionTip = false const app = new Vue({ router, store, ...App // Object spread copying everything from App.vue : render: h => h(App) }).$mount('#app')// 挂载到DOM元素 export { app, store, router } // new Vue({ // render: h => h(App) // }).$mount('#app')
router.js为前端
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export const asyncRouterMap = [ { path: '*', redirect: '/login' }, { path: '/', redirect: '/login', component: resolve => require(['./views/pages/login'], resolve) }, { path: '/login', name: 'login', component: resolve => require(['./views/pages/login'], resolve) }, { path: '/dashboard', name: 'dashboard', component: resolve => require(['./views/pages/dashboard'], resolve), children: [{ path: '/chat/:id/:name', name: 'chat', component: resolve => require(['./views/pages/chat'], resolve) }] } ] export default new Router({ mode: 'history', routes: asyncRouterMap })
App.vue为vue
<template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'app' } </script> <style> #app { width: 100vw; height: 100vh; } </style>
<template> <div id="login"> <ul class="login"> <li><input type="text" name="userName" id="userName" placeholder="请输入用户名" required autofocus v-model="userName" @keyup.13="doLogin" /></li> <li> <a href="javascript:void(0);" @click="doLogin" class="login-btn">登陆</a> </li> </ul> </div> </template> <script> import { mapState, mapMutations } from 'vuex' import * as types from '../../store/mutation-types' import io from 'socket.io-client' import common from '../../utils/common' export default { name: 'login', data () { return { userName: '', password: '' } }, computed: { ...mapState({ me: ({ users }) => users.me, online: ({ users }) => users.online, socket: ({ base }) => base.socket }) }, methods: { ...mapMutations({ login: types.LOGIN, genUid: types.GEN_UID, setSocket: types.SET_SOCKET }), doLogin () { const _self = this if (!this.userName) { console.log('请输入用户名') return } // TODO:ajax获取登陆数据 let user = { userId: common.genUid(), userName: _self.userName } // 链接websocket后端服务器 _self.setSocket(io('ws://127.0.0.1:3000')) if (_self.socket) { // 告诉服务器端有用户登陆 _self.socket.emit('login', user) // 贮存登陆用户的信息 _self.login(user) } // 进入首页 this.$router.push({ path: '/dashboard' }) } } } </script> <style scoped> ul, li { list-style: none; } .login { position: absolute; top: 50%; left: 50%; text-align: center; width: 400px; margin-left: -200px; margin-top: -150px; padding: 50px 20px; border-radius: 5px; box-shadow: 1px 1px 2px #ccc, -1px -1px 2px #ccc; background-color: #ffffff; } input[type="text"] { border: 1px solid #cccccc; line-height: 50px; width: 100%; text-align: center; } .login-btn { display: inline-block; margin-top: 20px; width: 100%; background-color: dodgerblue; color: #ffffff; line-height: 50px; text-decoration: none; } </style>
接下来进入了dashboard页面
java
<template> <div class="main"> <div class="top-menu clearfix"> <span>IM</span> <span> <span v-text="me.userName"></span> | <a href="javascript:;" @click="doLogout">退出</a> </span> </div> <ul class="user-list"> <li v-for="item in online.users" :key="item.id" track-by="$index" @click='chat(item)' :class="{'v-link-active':item.userId==currentActive}"> {{item.userName}} <span class="noread" v-if="item.noRead">{{item.noRead}}</span> </li> </ul> <div class="doc"> <router-view keep-alive></router-view> </div> </div> </template> <script> import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import * as types from '../../store/mutation-types' export default { name: 'index', data () { return { currentActive: '-1' } }, computed: { ...mapState({ me: state => state.users.me, online: state => state.users.online, socket: ({ base }) => base.socket }), ...mapGetters({}) }, methods: { ...mapActions([]), ...mapMutations({ logout: types.LOGOUT, updateUsers: types.UPDATE_USERS, addUsers: types.ADD_USERS, removeUser: types.REMOVE_USER, addReceiveMsg: types.ADD_RECEIVE_MSG }), doLogout () { this.socket.disconnect() this.logout() this.$router.push({ path: '/login' }) }, // 监听新用户登陆 listenLogin () { const _self = this if (_self.socket) { _self.socket.on('login', function (o) { console.log('[Leo]新用户加入 => ', o.user) console.log('[Leo]当前在线用户 => ', o.onlineUsers) _self.updateUsers(o.onlineUsers) }) } }, // 监听用户退出 listenLogout () { const _self = this if (_self.socket) { _self.socket.on('logout', function (o) { console.log('[Leo]有用户退出 => ', o) _self.removeUser(o.user.userId) }) } }, // 监听消息发送 listenMsg () { const _self = this if (_self.socket) { _self.socket.on(_self.me.userId, function (obj) { console.log('[Leo]有人对我说话 => ', obj.from.userName + ' 对 ' + obj.to.userName + ' 说 ' + obj.content) _self.addReceiveMsg(obj) }) } }, chat (user) { this.currentActive = user.userId this.$router.push({ name: 'chat', params: { id: user.userId, name: user.userName } }) } }, created () { if (!this.me.userName) { this.$router.push({ name: 'login' }) } this.listenLogin() this.listenLogout() this.listenMsg() } } </script> <style lang="less" scoped> .main { position: relative; width: 100vw; height: 100vh; border: 1px solid #efefef; box-shadow: 1px 1px 15px #ccc; background-color: #efeff4; overflow: hidden; } .top-menu { background-color: #3d3d3d; color: #fff; height: 45px; width: 100%; font-size: 12px; line-height: 45px; font-size: larger; font-family: "Microsoft YaHei UI", "微软雅黑", "Helvetica Neue", Helvetica, STHeiTi, sans-serif; span:first-child { text-align: left; margin-left: 10px; & + span { float: right; margin-right: 10px; } } a { color: #ffffff; text-decoration: none; } } ul, li { list-style: none; padding: 0; margin: 0; } .user-list { position: absolute; top: 45px; bottom: 0; left: 0; z-index: 9999999; width: 300px; overflow-y: auto; background-color: #fff; box-shadow: 3px 2px 5px #ccc; @height: 30 px; li { padding: 10px; line-height: @height; cursor: pointer; border-bottom: 1px dashed #efefef; img { float: left; width: @height; border-radius: 50%; } & :hover, & :active { background: #efefef; } .noread { display: inline-block; background-color: #f00; color: #fff; min-width: 20px; height: 20px; border-radius: 50%; font-size: 12px; line-height: 20px; text-align: center; } } } .doc { position: absolute; top: 45px; bottom: 0; left: 300px; right: 0; } .v-link-active { background-color: #efefef; } </style>
//src\views\pages\chat.vue <template> <div class="chat"> <div class="list"> <ul> <li v-for="msg in getMsgs" :key="msg.id"> <msg-item :type="msg.from.userId==me.userId?'me':'other'" :msg="msg"></msg-item> </li> </ul> </div> <div class="send"> <div class="send-bar"> <input type="file" id="fileImg" name="fileImg" style="display: none;" accept="image/*" ref="fileImg" @change="sendImg"> <label for="fileImg" class="fa fa-picture-o" aria-hidden="true"></label> </div> <div class="send-msg"> <textarea class="send-msg-input" placeholder="请输入聊天内容" autofocus v-model="content" @keyup.13="sendText" ref="msgInput"></textarea> <a href="javascript:void(0)" class="send-msg-btn" @click="sendText">发送</a> </div> </div> </div> </template> <script> import { mapState, mapMutations } from 'vuex' import * as types from '../../store/mutation-types' import msgItem from '@/components/msg-item' export default { name: 'chat', components: { msgItem }, data () { return { content: '', fileImg: null } }, computed: { ...mapState({ me: ({ users }) => users.me, users: ({ users }) => users.online.users, socket: ({ base }) => base.socket }), getMsgs () { const _self = this let msgs = [] for (let user of _self.users) { if (user.userId != _self.$route.params.id) continue if (user.msg) msgs = user.msg user.noRead = 0 break } /* eslint-disable */ setTimeout(_self.scrollToBottom, 0) console.log('[Leo]getMsgs => ', msgs) return msgs } }, methods: { ...mapMutations({ addSendMsg: types.ADD_SEND_MSG }), // 让浏览器滚动条保持在最低部 scrollToBottom: function () { window.scrollTo(0, document.querySelectorAll('.list ul')[0].clientHeight) window.document.querySelectorAll('.list')[0].scrollTop = document.querySelectorAll('.list ul')[0].clientHeight }, // 上传图片 <https://segmentfault.com/a/1190000004924160> sendImg (event) { let _vm = this let file = event.target.files[0] // 获取图片资源 // 只选择图片文件 if (!file.type.match('image.*')) { return false } let reader = new FileReader() reader.readAsDataURL(file)// 读取文件 // 渲染文件 reader.onload = function (arg) { _vm.submit('img', arg.target.result) _vm.$refs.fileImg.files[0] = null _vm.$refs.msgInput.focus() } // TODO:上传图片 _vm.uploadFile(file).then(res => { console.log('[Leo]图片上传成功 => ', res) }).catch(error => { console.error('[Leo]图片上传出错 => ', error) }) }, // 提交聊天消息内容 sendText () { const _vm = this if (_vm.content != '') { _vm.submit('text', _vm.content) } else { console.log('请输入聊天内容') } _vm.$nextTick(function () { _vm.scrollToBottom() _vm.content = '' _vm.$refs.msgInput.focus() }) return false }, // 提交聊天消息内容 submit (type, content) { const _vm = this let obj = { 'from': { 'userId': _vm.me.userId, 'userName': _vm.me.userName }, 'to': { 'userId': _vm.$route.params.id, 'userName': _vm.$route.params.name }, 'msgType': type, 'content': content, 'sendtime': (new Date()).getTime() } _vm.addSendMsg(obj) _vm.socket.emit('message', obj) }, /** * 上传文件 * @param file */ uploadFile (file) { let formData = new FormData() // 把上传的数据放入form_data formData.append('img', file) // 异步提交数据 return fetch('url', { method: 'POST', body: formData }) } }, mounted () { const _self = this _self.$nextTick(function () { _self.scrollToBottom() _self.$refs.msgInput.focus() }) } } </script> <style scoped lang="scss" rel="stylesheet/scss"> input, button, select, textarea { outline: none; } ul, li { list-style: none; } .chat { position: absolute; top: 0; bottom: 0; right: 0; left: 0; box-sizing: border-box; overflow: hidden; } .list { padding: 10px; height: calc(100% - 100px - 40px); overflow-y: auto; overflow-x: hidden; } .send { position: relative; display: flex; flex-direction: column; &-bar { flex: 1; height: 40px; display: flex; justify-content: flex-start; align-items: center; background-color: #ffffff; .fa { padding: 10px 15px; cursor: pointer; } } &-msg { display: flex; flex: 1; height: 100px; overflow: hidden; box-shadow: 0 -1px 2px #efefef; background-color: #fff; &-input { flex: 1; padding: 0 10px; box-sizing: border-box; border: none; line-height: 30px; resize: none; } &-btn { display: inline-block; width: 100px; height: 100%; line-height: 100px; background-color: dodgerblue; text-align: center; text-decoration: none; color: #fff; } } } </style>
页面效果没有数据,应该是项目存在问题git