這個項目多是個虎頭蛇尾的項目?跟我一塊兒分析吧,比較簡單的一個項目
另外,我也想跟本身說,我好像失去了那個努力的本身了。要珍惜時間,好好加油啊~
項目地址爲: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