中后台学习

页面的主要路由分析

1591325371527

全局是一个大的容器,APP.vue,然后显示的组件时layout,layout组件的构成

  • slide-bar

  • header

  • app-main 其中app-main也是一个容器,承载页面的主要内容,用router-view显示子路由

    {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
    path: 'dashboard',
    name: 'Dashboard',
    component: () => import('@/views/dashboard/index'),
    meta: { title: 'Dashboard', icon: 'dashboard' }
    }]
    },

    可以看出,layout组件的app-main就是主要内容

添加一个页面

{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'excel',
meta: {
title: 'excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: ()=>import('excel/exportExcel'),
name: 'exportExcel',
meta: { title: 'exportExcel' }
}
]
}

权限校验

生成权限菜单

async-router上配置路由元信息,增加roles属性

{
path:'/student',
component:layout,
redirect:'/student/add',
meta:{title:'学生管理',icon:'documation',roles=['admin']}
children:[
{
path:'/create',
component:()=>important('')
name
}
]
}

修改配置代理接口

首先关闭mock数据

注释以下代码

main.js

if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}

vue.config.js

devServer

更改axiosbaseUrl

修改环境配置文件 全局变量

登录源码分析

handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
// 通过校验
this.loading = true
// 提交表单数据 提交一个vuex的commit,因为用了namespaced = true 所以要加上namespace
this.$store.dispatch('user/login', this.loginForm)
.then(() => {
// 保存token之后 如果有redirect就前往redirect 默认到达根路径
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
.catch(() => {
// 捕获异常
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}

vuex中的user


// 动作定义 异步操作 需要更改多个state
const actions = {
// 用户登陆
login({ commit }, userInfo) {
const { username, password } = userInfo
// 返回promise对象
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
// 设置vuex数据和保存cookie
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},

// 获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
// 携带token信息请求用户信息
getInfo(state.token).then(response => {
const { data } = response
const { roles, name, avatar } = data
// 返回信息 roles是必须的
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
// 保存vuex数据
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},

// 用户退出登录
logout({ commit, state }) {
return new Promise((resolve, reject) => {
// 退出登录
try {
// 将cookie中的token清除
removeToken()
// 路由重定向
resetRouter()
// vuex信息清除
commit('RESET_STATE')
resolve()
} catch (error) {
reject(error)
}
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}

export default {
namespaced: true,
state,
mutations,
actions
}


分析:

登录分为几步操作

  1. 校验表单,提交数据
  2. 执行vuex的commit,请求服务端数据然后保存tokencookievuex当中,返回promise·resolve
  3. 请求导数据后路由重定向到redirect,如果没有就重定向到/

获取用户信息

    1.  携带`token`请求接口
    2.  获取`roles`,`avatar`,`name`,其中`roles`必须存在
    3.  保存数据在`vuex`

退出登录

     1.  清除`cookie`中的数据
     2.  将`vuex`用户信息置空
     3.  路由重定向,此时不携带`token`,路由会定位到`login`

其中这两个接口返回的数据相对固定

  • 登录

    {
    "data":{
    token:""
    }
    }
  • 获取用户信息

    {
    "data":{
    avatar:"",
    name:"",
    roles:['admin'...]
    }
    }

框架的路由权限分析

访问路由时会从 Cookie 中获取 Token,判断 Token 是否存在:

  • 如果 Token 存在,将根据用户角色生成动态路由,然后访问路由,生成对应的页面组件。这里有一个特例,即用户访问 /login 时会重定向至 / 路由;
  • 如果 Token 不存在,则会判断路由是否在白名单中,如果在白名单中将直接访问,否则说明该路由需要登录才能访问,此时会将路由生成一个 redirect 参数传入 login 组件,实际访问的路由为:/login?redirect=/xxx
router.beforeEach(async(to, from, next) => {
// 启动进度条
NProgress.start()
// 修改页面标题
document.title = getPageTitle(to.meta.title)
// 从 Cookie 获取 Token
const hasToken = getToken()

// 判断 Token 是否存在
if (hasToken) {
// 如果当前路径为 login 则直接重定向至首页
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
// 判断用户的角色是否存在
const hasRoles = store.getters.roles && store.getters.roles.length > 0
// 如果用户角色存在,则直接访问
if (hasRoles) {
next()
} else {

try {
// 异步获取用户的角色
const { roles } = await store.dispatch('user/getInfo')
//! 根据用户角色,动态生成路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 调用 router.addRoutes 动态添加路由
router.addRoutes(accessRoutes)
// 使用 replace 访问路由,不会在 history 中留下记录
next({ ...to, replace: true })
} catch (error) {
// 移除 Token 数据
await store.dispatch('user/resetToken')
// 显示错误提示
Message.error(error || 'Has Error')
// 重定向至登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
}
else {
// 如果访问的 URL 在白名单中,则直接访问
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// 如果访问的 URL 不在白名单中,则直接重定向到登录页面,并将访问的 URL 添加到 redirect 参数中
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})

router.afterEach(() => {
// 停止进度条
NProgress.done()
})

图表

折线图

<template>
<div class="chart" style="width:100%;height:350px" />
</template>

<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'

export default {
mixins: [resize],
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
// 组件销毁 清空图表提高性能
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
// 初始化图表
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
// 配置项
// expectedData: [100, 120, 161, 134, 105, 160, 165],
// actualData: [120, 82, 91, 154, 162, 140, 145]
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
// x轴
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
// 不显示标尺
boundaryGap: false,
axisTick: {
show: false
}
},
// 网格分布
grid: {
left: 10,
right: 10,
bottom: 20,
top: 30,
containLabel: true
},
// 提示信息
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
// 点击可切换折线图显示的选项
legend: {
data: ['expected', 'actual']
},
// 数据配置
series: [{
name: 'expected', itemStyle: {
// 拐点颜色和线颜色
normal: {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
}
}
}
</script>

饼图

<template>
<div class="chart" style="width:100%;height:550px" />
</template>

<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import resize from '@/utils/resize'
export default {
name: '',
components: {},
mixins: [resize],
props: {
pipeData: {
type: Array,
default: () => {
return []
}
}
},
data() {
return {

}
},

computed: {},

watch: {},
created() {},

beforeMount() {},

mounted() {
this.initChart()
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},

methods: {
// 初始化图表
initChart() {
setTimeout(() => {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions()
}, 500)
},
setOptions() {
this.chart.setOption({
title: {
text: '学生课程分数情况图',
subtext: '仅供参考',
left: 'center'
},
// 配置提示
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
// 选项卡配置
legend: {
// horizontal为横轴
orient: 'veritcal',
// 图例组件离容器左侧的距离
left: 70,
top: 40,
data: ['60-70分课程数', '70-80分课程数', '80-90分课程数', '90-100分课程数', '不及格课程数']
},
series: [
{
name: '课程分布情况',
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: this.pipeData[1], name: '60-70分课程数' },
{ value: this.pipeData[2], name: '70-80分课程数' },
{ value: this.pipeData[3], name: '80-90分课程数' },
{ value: this.pipeData[4], name: '90-100分课程数' },
{ value: this.pipeData[0], name: '不及格课程数' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}
}

}

</script>
<style lang='scss' scoped>

</style>

1591965732372

表格

无边框表格

<el-table
:data="userInfo.course_list"
fit
highlight-current-row
style="box-sizing:border-box;width: 100%;padding-top:15px;margin-left:40px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);"
>
<el-table-column label="序号" type="index" align="center" width="90" />
<el-table-column label="课程编号" prop="cno" align="center" width="90">
<template slot-scope="scope">
<span style="color:#409EFF;font-size:17px;font-weight:500">{{ scope.row.cno }}</span>
</template>
</el-table-column>
<el-table-column label="课程名" prop="cno" align="center" min-width="200">
<template slot-scope="scope">
<span style="color:#606266;font-size:16px;line-height:23px;font-weight:500">{{ scope.row.course_detail[0].name }}</span>
</template>
</el-table-column>
<el-table-column label="学分" prop="cno" align="center" width="90">
<template slot-scope="scope">
<span style="color:#409EFF;font-size:17px;font-weight:500">{{ scope.row.course_detail[0].credit }}</span>
</template>
</el-table-column>
<el-table-column label="学时" prop="cno" align="center" width="90">
<template slot-scope="scope">
<span style="color:#909399;font-size:17px;font-weight:500">{{ scope.row.course_detail[0].period }}</span>
</template>
</el-table-column>
<el-table-column label="学期" align="center" width="200">
<template slot-scope="{row}">
<el-tag v-if="row.course_detail[0].term !== '第二学期'" type="danger">{{ row.course_detail[0].term }}</el-tag>
<el-tag v-if=" row.course_detail[0].term=== '第二学期'" type="success">{{ row.course_detail[0].term }}</el-tag>
</template>
</el-table-column>
</el-table>

具体配置参考element-ui文档

有边框文档

<el-table
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%;"
>
<el-table-column label="序号" type="index" align="center" width="100" />
<el-table-column label="课程编号" prop="cno" align="center" width="100">
<template slot-scope="scope">
<span style="color:#409EFF;font-size:17px;font-weight:500">{{ scope.row.cno }}</span>
</template>
</el-table-column>
<el-table-column label="课程名" prop="course_detail[0].name" align="center" width="130">
<template slot-scope="scope">
<span>{{ scope.row.course_detail[0].name }}</span>
</template>
</el-table-column>
<el-table-column label="学分" prop="course_detail[0].credit" align="center" width="100">
<template slot-scope="scope">
<span style="color:#E6A23C;font-size:17px;font-weight:500">{{ scope.row.course_detail[0].credit }}</span>
</template>
</el-table-column>
<el-table-column label="学期" prop="course_detail[0].term" align="center" width="150">
<template slot-scope="scope">
<span>{{ scope.row.course_detail[0].term }}</span>
</template>
</el-table-column>
<el-table-column label="学时" prop="course_detail[0].period" align="center" width="100">
<template slot-scope="scope">
<span style="color:#67C23A;font-size:17px;font-weight:500">{{ scope.row.course_detail[0].period }}</span>
</template>
</el-table-column>
<el-table-column label="封面图" prop="course_detail[0].img" align="center" width="199">
<template slot-scope="scope">
<img width="80px" height="80px" :src="scope.row.course_detail[0].img" alt="">
</template>
</el-table-column>
<el-table-column label="成绩" prop="score" align="center" width="100">
<template slot-scope="scope">
<span style="color:#F56C6C;font-size:17px;font-weight:500">{{ scope.row.score }}</span>
</template>
</el-table-column>

<el-table-column label="操作" align="center" width="290" class-name="small-padding fixed-width">
<template slot-scope="{row}">
<el-button disabled type="primary" size="mini" @click="confirmEdit(row)">
编辑
</el-button>
<el-button disabled size="small" type="success">
添加
</el-button>
<el-button disabled size="small" type="danger">
删除
</el-button>
</template>
</el-table-column>
</el-table>

综合表格

<div class="app-container">
<div class="filter-container">
<el-input v-model="listQuery.title" :placeholder="$t('table.title')" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />
<el-select v-model="listQuery.importance" :placeholder="$t('table.importance')" clearable style="width: 90px" class="filter-item">
<el-option v-for="item in importanceOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="listQuery.type" :placeholder="$t('table.type')" clearable class="filter-item" style="width: 130px">
<el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name+'('+item.key+')'" :value="item.key" />
</el-select>
<el-select v-model="listQuery.sort" style="width: 140px" class="filter-item" @change="handleFilter">
<el-option v-for="item in sortOptions" :key="item.key" :label="item.label" :value="item.key" />
</el-select>
<el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">
{{ $t('table.search') }}
</el-button>
<el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-edit" @click="handleCreate">
{{ $t('table.add') }}
</el-button>
<el-button v-waves :loading="downloadLoading" class="filter-item" type="primary" icon="el-icon-download" @click="handleDownload">
{{ $t('table.export') }}
</el-button>
<el-checkbox v-model="showReviewer" class="filter-item" style="margin-left:15px;" @change="tableKey=tableKey+1">
{{ $t('table.reviewer') }}
</el-checkbox>
</div>

<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%;"
@sort-change="sortChange"
>
<el-table-column :label="$t('table.id')" prop="id" sortable="custom" align="center" width="80" :class-name="getSortClass('id')">
<template slot-scope="scope">
<span>{{ scope.row.id }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.date')" width="150px" align="center">
<template slot-scope="scope">
<span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.title')" min-width="150px">
<template slot-scope="{row}">
<span class="link-type" @click="handleUpdate(row)">{{ row.title }}</span>
<el-tag>{{ row.type | typeFilter }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('table.author')" width="110px" align="center">
<template slot-scope="scope">
<span>{{ scope.row.author }}</span>
</template>
</el-table-column>
<el-table-column v-if="showReviewer" :label="$t('table.reviewer')" width="110px" align="center">
<template slot-scope="scope">
<span style="color:red;">{{ scope.row.reviewer }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.importance')" width="80px">
<template slot-scope="scope">
<svg-icon v-for="n in +scope.row.importance" :key="n" icon-class="star" class="meta-item__icon" />
</template>
</el-table-column>
<el-table-column :label="$t('table.readings')" align="center" width="95">
<template slot-scope="{row}">
<span v-if="row.pageviews" class="link-type" @click="handleFetchPv(row.pageviews)">{{ row.pageviews }}</span>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.status')" class-name="status-col" width="100">
<template slot-scope="{row}">
<el-tag :type="row.status | statusFilter">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('table.actions')" align="center" width="230" class-name="small-padding fixed-width">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="handleUpdate(row)">
{{ $t('table.edit') }}
</el-button>
<el-button v-if="row.status!='published'" size="mini" type="success" @click="handleModifyStatus(row,'published')">
{{ $t('table.publish') }}
</el-button>
<el-button v-if="row.status!='draft'" size="mini" @click="handleModifyStatus(row,'draft')">
{{ $t('table.draft') }}
</el-button>
<el-button v-if="row.status!='deleted'" size="mini" type="danger" @click="handleModifyStatus(row,'deleted')">
{{ $t('table.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>

<pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" />

<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="70px" style="width: 400px; margin-left:50px;">
<el-form-item :label="$t('table.type')" prop="type">
<el-select v-model="temp.type" class="filter-item" placeholder="Please select">
<el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item :label="$t('table.date')" prop="timestamp">
<el-date-picker v-model="temp.timestamp" type="datetime" placeholder="Please pick a date" />
</el-form-item>
<el-form-item :label="$t('table.title')" prop="title">
<el-input v-model="temp.title" />
</el-form-item>
<el-form-item :label="$t('table.status')">
<el-select v-model="temp.status" class="filter-item" placeholder="Please select">
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item :label="$t('table.importance')">
<el-rate v-model="temp.importance" :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" style="margin-top:8px;" />
</el-form-item>
<el-form-item :label="$t('table.remark')">
<el-input v-model="temp.remark" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="Please input" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">
{{ $t('table.cancel') }}
</el-button>
<el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
{{ $t('table.confirm') }}
</el-button>
</div>
</el-dialog>

<el-dialog :visible.sync="dialogPvVisible" title="Reading statistics">
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
<el-table-column prop="key" label="Channel" />
<el-table-column prop="pv" label="Pv" />
</el-table>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogPvVisible = false">{{ $t('table.confirm') }}</el-button>
</span>
</el-dialog>
</div>

导出Excel表格

引入vue-element-admin提供的导出js文件

handleDownload() {
this.downLoading = true
import('@/vendor/export2Excel').then(excel => {
// 设置表头
const tHeader = ['课程编号', '课程名', '学分', '学期', '学时', '成绩']
// 选择导出到表的字段
const filterVal = ['cno', 'name', 'credit', 'term', 'period', 'score']
const data = this.formatJson(filterVal, this.list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '成绩单'
})
this.downLoading = false
})
},
// 将对象数组对应fileterval的字段取出 返回一个数组
formatJson(filterval, data) {
// 遍历json数据
return data.map(v => filterval.map(item => {
if (item === 'cno' || item === 'score') {
return v[item]
} else {
const course_detail = v.course_detail[0]
return course_detail[item]
}
}))
}

websocket

服务端

const WebSocket = require('ws')
const http = require('http')
// 创建WebSocket服务
const wss = new WebSocket.Server({ noServer: true })
const server = http.createServer()

const group = {} // 记录每个房间号的人数

// 多聊天室功能
// roomid -> 对应相同的roomid进行广播消息
wss.on('connection', function connection(ws) {
// ws代表当前收到消息的客户端

// 接收客户端消息
ws.on('message', function(msg) {
console.log(msg) //msg: {"event":"enter","message":"1","roomid":"1"}
// 转成对象
const msgObj = JSON.parse(msg)

// 进入房间
if (msgObj.event === 'enter') {
ws.name = msgObj.message
ws.roomid = msgObj.roomid
if (typeof group[ws.roomid] === 'undefined') {
group[ws.roomid] = 1
} else {
group[ws.roomid] += 1
}
}

// 广播消息(即获取所有的客户端)
wss.clients.forEach(client => {
// 补充:如何判断非自己的客户端:ws !== client
if (client.readyState === WebSocket.OPEN && client.roomid === ws.roomid) {
msgObj.name = ws.name
// msgObj.num = wss.clients.size; // 聊天室的人数
msgObj.num = group[ws.roomid] // 聊天室的人数
// 发送回客户端在线人数和 消息
client.send(JSON.stringify(msgObj))
}
})
})

// 当ws客户端断开链接的时候
ws.on('close', function(msg) {
if (ws.name) {
group[ws.roomid] -= 1
}

const msgObj = {}
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN && ws.roomid === client.roomid) {
msgObj.name = ws.name
msgObj.num = group[ws.roomid] // 聊天室的人数
msgObj.event = 'out'
client.send(JSON.stringify(msgObj))
}
})
})
})

server.on('upgrade', function upgrade(request, socket, head) {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request)
})
})

server.listen(3000)

客户端

变量均用双向绑定定义


message: '',
lists: [],
ws: {},
name: '',
isShow: true,
num: 0,
roomid: "" //房间号
mounted(){
this.ws = new WebSocket('ws://127.0.0.1:3000');
this.ws.onopen = this.onOpen
this.ws.onmessage = this.onMessage
this.ws.onclose = this.onClose
this.ws.onerror = this.onError
}


// 监听是否打开websocket
onOpen: function () {
console.log('open:' + this.ws.readyState);
},

// 进入聊天室
enter() {
if(this.name.trim() === '') {
alert('用户名不得为空')
return
}
this.isShow = false
this.ws.send(JSON.stringify({
event: "enter",
message: this.name,
roomid: this.roomid
}))
},
// 接收消息
onMessage: function (event) {
// 当用户未进入聊天室,则不接收消息
if (this.isShow) {
return
}

// 接收服务端发送过来的消息
var tmp = JSON.parse(event.data)

if (tmp.event === 'enter') {
// 当有新用户进入聊天室 tmp.message为用户名
this.lists.push('欢迎:' + tmp.message + '加入聊天室')
} else if (tmp.event === 'out') {
this.lists.push(tmp.name + '已经退出了聊天室!')
} else {
if (tmp.name !== this.name) { // 屏蔽发送给自己的消息
// 接收正常的聊天
this.lists.push(tmp.name + ':' + tmp.message)
}
}
this.num = tmp.num
},
// 发送消息
send: function () {
// 客户端显示
this.lists.push(this.name + ':' + this.message)
this.ws.send(JSON.stringify({
event: "message",
message: this.message
}))
this.message = ''
}


// 关闭和异常
onClose: function () {
// 当链接主动断开的时候触发close事件
console.log('close:' + this.ws.readyState);
},
onError: function () {
// 当连接失败的时候触发error事件
console.log('error:' + this.ws.readyState);
},

实例(服务端)

const WebSocket = require('ws')
const http = require('http')
const wss = new WebSocket.Server({ noServer: true })
const server = http.createServer()
const group = {} // 记录每个房间号的人数
// 多聊天室功能
// roomid -> 对应相同的roomid进行广播消息
wss.on('connection', function connection(ws) {
// ws代表当前收到消息的客户端
// 接收客户端消息
ws.on('message', function(msg) {
const msgObj = JSON.parse(msg)
if (msgObj.event === 'enter') {
// 用户信息
ws.name = msgObj.name
ws.avatar = msgObj.avatar
ws.roomid = msgObj.roomid
// 首次进入房间判断 房间人数置为1
if (typeof group[ws.roomid] === 'undefined') {
group[ws.roomid] = 1
} else {
group[ws.roomid] += 1
}
}
// 广播消息(即获取所有的客户端)
wss.clients.forEach(client => {
// 补充:如何判断非自己的客户端:ws !== client
if (client.readyState === WebSocket.OPEN && client.roomid === ws.roomid) {
// 同一个房间 进行广播
msgObj.name = ws.name
msgObj.avatar = ws.avatar
// 房间人数
msgObj.num = group[ws.roomid]
// 发送广播消息 附带消息
client.send(JSON.stringify(msgObj))
}
})
})
// 当ws客户端断开链接的时候
ws.on('close', function(msg) {
if (ws.name) {
group[ws.roomid] -= 1
}
const msgObj = {}
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN && ws.roomid === client.roomid) {
msgObj.name = ws.name
msgObj.avatar = ws.avatar
msgObj.num = group[ws.roomid] // 聊天室的人数
msgObj.event = 'out'
client.send(JSON.stringify(msgObj))
}
})
})
})
server.on('upgrade', function upgrade(request, socket, head) {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request)
})
})
server.listen(3000)

实例(客户端)

监听开启websocket

mounted() {
this.isShow = true
this.ws = new WebSocket('ws://127.0.0.1:3000')
this.ws.onopen = this.onOpen
this.ws.onmessage = this.onMessage
this.ws.onclose = this.onClose
this.ws.onerror = this.onError
},
methods: {
// 监听开启
onOpen() {
console.log('webSocket is open' + this.ws.readyState)
}
}

监听进入房间

enter() {
this.isShow = false
// 发送消息 广播进入房间
this.ws.send(JSON.stringify({
event: 'enter',
name: this.userInfo.name,
avatar: this.userInfo.avatar,
rootmid: this.roomid
}))
},

监听传来的消息

// 接收信息
onMessage(event) {
// 服务端传递过来的数据
if (this.isShow) {
return
}
const data = JSON.parse(event.data)
// 如果是进入或者退出房间
const messageInfo = {}
messageInfo.avatar = data.avatar
messageInfo.roomNum = data.num
messageInfo.name = data.name
if (data.event === 'enter') {
messageInfo.message = '加入房间'
} else if (data.event === 'close') {
messageInfo.message = '退出了房间'
} else {
messageInfo.message = data.message
}
this.lists.push(messageInfo)
this.num = data.num
this.message = ''
},

发送消息

// 发送消息
send() {
// this.lists.push()
this.ws.send(JSON.stringify({
event: 'message',
message: this.message
}))
},

监听错误或者推出


clear() {
this.message = ''
},
// 监听关闭和异常
onClose: function() {
// 当链接主动断开的时候触发close事件
console.log('close:' + this.ws.readyState)
},
onError: function() {
// 当连接失败的时候触发error事件
console.log('error:' + this.ws.readyState)
}
}

客户端完整代码

<template>
<div class="container">
<el-card v-if="!isShow" class="message-content">
<p slot="header" style="padding:10px;max-height:2px!important;box-sizing:border-box;font-weight:800">在线聊天系统 在线人数:{{ num }}人</p>
<el-alert
center
:title="time"
type="info"
:closable="false"
/>
<div v-for="(item,index) of lists" ref="ms" :key="index" class="message">
<el-alert
v-if="item.message === '加入房间' && typeof(item.name) !== 'undefined'"
center
:title="item.name+item.message"
style="margin-top:20px;"
type="success"
:closable="false"
/>
<el-alert
v-if="item.message === '加入房间'&& typeof(item.name) === 'undefined'"
center
:title="'超级管理员'+item.message"
style="margin-top:20px;"
type="error"
:closable="false"
/>
<div v-if="item.name !== userInfo.name && item.message!== '加入房间'" class="content-left">
<img style="border-radius:6px" :src="item.avatar" alt="" width="40px" height="40px">
<div class="info">
<span class="name">{{ item.name }}</span>
<span class="message-left" type="info">{{ item.message }}</span>
</div>
</div>
<div v-if="item.name === userInfo.name && item.message !== '加入房间'" class="content-right">
<div class="info">
<!-- <span class="name">{{ item.name }}</span> -->
<span class="message-right" type="primary">{{ item.message }}</span>
</div>
<img style="border-radius:6px" :src="item.avatar" alt="" width="40px" height="40px">
</div>
</div>
</el-card>
<div v-if="!isShow" class="message-input">
<el-input
v-model="message"
type="textarea"
:rows="6"
placeholder="请输入内容"
/>
</div>
<div v-if="!isShow" class="send-wrapper">
<el-button style="width:120px;border:none" @click="clear">取消</el-button>
<el-button type="primary" style="width:120px; margin-left:15px" @click="send">发送</el-button>
</div>
<div class="enter-button">
<el-button v-if="isShow" type="primary" @click="enter">进入聊天室</el-button>
</div>
</div>
</template>

<script>
import dayjs from 'dayjs'
import { mapGetters } from 'vuex'
export default {
name: '',
components: {},
props: {},
data() {
return {
message: '',
lists: [],
ws: {},
isShow: true,
num: 0,
roomid: 1,
time: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
},
computed: {
...mapGetters([
'userInfo'
])
},

watch: {},
created() {},

beforeMount() {},

mounted() {
this.isShow = true
this.ws = new WebSocket('ws://127.0.0.1:3000')
this.ws.onopen = this.onOpen
this.ws.onmessage = this.onMessage
this.ws.onclose = this.onClose
this.ws.onerror = this.onError
},

methods: {
// 监听开启
onOpen() {
console.log('webSocket is open' + this.ws.readyState)
},

// 监听进入房间
enter() {
this.isShow = false
// 发送消息 广播进入房间
this.ws.send(JSON.stringify({
event: 'enter',
name: this.userInfo.name,
avatar: this.userInfo.avatar,
rootmid: this.roomid
}))
},

// 接收信息
onMessage(event) {
// 服务端传递过来的数据
if (this.isShow) {
return
}
const data = JSON.parse(event.data)
// 如果是进入或者退出房间
const messageInfo = {}
messageInfo.avatar = data.avatar
messageInfo.roomNum = data.num
messageInfo.name = data.name
if (data.event === 'enter') {
messageInfo.message = '加入房间'
} else if (data.event === 'close') {
messageInfo.message = '退出了房间'
} else {
messageInfo.message = data.message
}
this.lists.push(messageInfo)
this.num = data.num
this.message = ''
},

// 发送消息
send() {
// this.lists.push()
this.ws.send(JSON.stringify({
event: 'message',
message: this.message
}))
},
clear()
this.message = ''
},
// 监听关闭和异常
onClose: function() {
// 当链接主动断开的时候触发close事件
console.log('close:' + this.ws.readyState)
},
onError: function() {
// 当连接失败的时候触发error事件
console.log('error:' + this.ws.readyState)
}
}

}