这个项目可能是个有始无终的项目?跟我一起分析吧,比较简单的一个项目
另外,我也想跟自己说,我好像失去了那个努力的自己了。要珍惜时间,好好加油啊~
项目地址为:https://github.com/xiaobeila/vue-websocket.git
这个项目和其他的项目的区别是,这个项目里面将服务器端,即websocket.io直接与前端项目集成在一起了。
//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')
})
接下来我们看客户端的代码
//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为
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
#app {
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;
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;
100%;
text-align: center;
}
.login-btn {
display: inline-block;
margin-top: 20px;
100%;
background-color: dodgerblue;
color: #ffffff;
line-height: 50px;
text-decoration: none;
}
</style>
接下来进入了dashboard页面
<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;
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;
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;
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;
@height;
border-radius: 50%;
}
& :hover,
& :active {
background: #efefef;
}
.noread {
display: inline-block;
background-color: #f00;
color: #fff;
min- 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>
//srcviewspageschat.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;
100px;
height: 100%;
line-height: 100px;
background-color: dodgerblue;
text-align: center;
text-decoration: none;
color: #fff;
}
}
}
</style>
页面效果没有数据,应该是项目存在问题