从开发到部署的完整教程 – 使用Vue2、Vuex和WebSocket技术
Vue2实时聊天应用开发指南
本教程将指导您使用Vue2构建一个功能完整的实时聊天应用,包含用户认证、联系人列表、实时消息传递和通知系统。
架构设计: 采用模块化设计,分离UI组件、状态管理和网络通信层。
1. 项目结构设计
src/
├── assets/
├── components/
│ ├── ChatContainer.vue
│ ├── ContactList.vue
│ ├── MessageList.vue
│ ├── MessageInput.vue
│ └── UserStatus.vue
├── store/
│ ├── index.js # Vuex主文件
│ ├── modules/
│ │ ├── auth.js # 认证模块
│ │ ├── chat.js # 聊天模块
│ │ └── contacts.js # 联系人模块
├── services/
│ ├── auth.service.js # 认证服务
│ ├── chat.service.js # 聊天服务
│ └── websocket.js # WebSocket服务
├── router/
│ └── index.js # 路由配置
├── views/
│ ├── Login.vue
│ └── Chat.vue
├── App.vue
└── main.js
2. Vuex状态管理设计
使用Vuex管理应用状态:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './modules/auth'
import chat from './modules/chat'
import contacts from './modules/contacts'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth,
chat,
contacts
}
})
// store/modules/chat.js
export default {
state: {
currentConversation: null,
messages: [],
unreadCount: 0
},
mutations: {
SET_CURRENT_CONVERSATION(state, conversation) {
state.currentConversation = conversation
state.unreadCount = 0
},
ADD_MESSAGE(state, message) {
state.messages.push(message)
// 如果是当前会话的消息,不增加未读计数
if (state.currentConversation?.id !== message.sender) {
state.unreadCount++
}
},
CLEAR_MESSAGES(state) {
state.messages = []
}
},
actions: {
sendMessage({ commit, state }, content) {
const message = {
id: Date.now(),
sender: state.auth.user.id,
receiver: state.currentConversation.id,
content,
timestamp: new Date().toISOString(),
status: 'sent'
}
// 发送消息到WebSocket服务器
chatService.sendMessage(message)
// 立即添加到本地状态
commit('ADD_MESSAGE', message)
},
receiveMessage({ commit }, message) {
commit('ADD_MESSAGE', message)
}
}
}
3. WebSocket服务实现
创建WebSocket服务处理实时通信:
// services/websocket.js
class WebSocketService {
constructor() {
this.socket = null
this.listeners = []
}
connect(token) {
this.socket = new WebSocket(`wss://api.example.com/chat?token=${token}`)
this.socket.onopen = () => {
console.log('WebSocket connected')
}
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data)
this.listeners.forEach(listener => listener(message))
}
this.socket.onclose = () => {
console.log('WebSocket disconnected')
// 尝试重新连接
setTimeout(() => this.connect(token), 3000)
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data))
}
}
addListener(listener) {
this.listeners.push(listener)
}
removeListener(listener) {
this.listeners = this.listeners.filter(l => l !== listener)
}
disconnect() {
if (this.socket) {
this.socket.close()
}
}
}
export default new WebSocketService()
4. 核心组件实现
聊天容器组件:
<template>
<div class="chat-app">
<div class="app-header">
<div class="user-info">
<div class="avatar">{{ userInitials }}</div>
<div>{{ userName }}</div>
</div>
<button @click="logout">退出</button>
</div>
<div class="chat-container">
<ContactList />
<div class="chat-window">
<div class="chat-header" v-if="currentConversation">
<div class="avatar small">{{ currentContactInitials }}</div>
<div>
<div>{{ currentConversation.name }}</div>
<div class="status">
<span :class="['status-indicator', currentConversation.online ? 'online' : 'offline']"></span>
{{ currentConversation.online ? '在线' : '离线' }}
</div>
</div>
</div>
<MessageList v-if="currentConversation" />
<MessageInput v-if="currentConversation" @send="sendMessage" />
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
import ContactList from './ContactList.vue'
import MessageList from './MessageList.vue'
import MessageInput from './MessageInput.vue'
export default {
components: { ContactList, MessageList, MessageInput },
computed: {
...mapState('auth', ['user']),
...mapState('chat', ['currentConversation']),
...mapGetters('contacts', ['getContactById']),
userName() {
return this.user ? this.user.name : '未登录'
},
userInitials() {
return this.user ? this.user.name.charAt(0) : '?'
},
currentContact() {
return this.currentConversation
? this.getContactById(this.currentConversation.id)
: null
},
currentContactInitials() {
return this.currentContact
? this.currentContact.name.charAt(0)
: '?'
}
},
methods: {
...mapActions('auth', ['logout']),
...mapActions('chat', ['sendMessage'])
}
}
</script>
5. 消息列表组件
<template>
<div class="chat-messages">
<div
v-for="message in messages"
:key="message.id"
class="message"
:class="message.sender === userId ? 'sent' : 'received'"
>
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import moment from 'moment'
export default {
computed: {
...mapState('chat', ['messages']),
...mapState('auth', ['user']),
userId() {
return this.user ? this.user.id : null
}
},
methods: {
formatTime(timestamp) {
return moment(timestamp).format('HH:mm')
}
},
watch: {
messages() {
// 滚动到底部
this.$nextTick(() => {
const container = this.$el
container.scrollTop = container.scrollHeight
})
}
}
}
</script>
6. 部署指南
使用Docker容器化部署:
# Dockerfile
FROM node:14 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx配置示例:
# nginx.conf
server {
listen 80;
server_name yourdomain.com;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /socket.io {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
实时聊天应用演示
登录聊天系统
{{ currentConversation.online ? ‘在线’ : ‘离线’ }}
// 模拟WebSocket服务
class MockWebSocket {
constructor() {
this.listeners = []
this.connected = false
this.messages = []
this.interval = null
}
connect() {
this.connected = true
console.log(‘Mock WebSocket connected’)
// 模拟接收消息
this.interval = setInterval(() => {
if (Math.random() > 0.7 && this.listeners.length > 0) {
const senders = [2, 3, 4].filter(id => id !== this.currentUserId)
const senderId = senders[Math.floor(Math.random() * senders.length)]
const sender = this.contacts.find(c => c.id === senderId)
if (sender) {
const message = {
id: Date.now(),
sender: senderId,
receiver: this.currentUserId,
content: this.getRandomMessage(),
timestamp: new Date().toISOString()
}
this.listeners.forEach(listener => listener(message))
}
}
}, 3000)
}
disconnect() {
this.connected = false
clearInterval(this.interval)
console.log(‘Mock WebSocket disconnected’)
}
send(data) {
this.messages.push(data)
console.log(‘Message sent:’, data)
// 模拟消息发送成功
setTimeout(() => {
const message = {
…data,
status: ‘delivered’,
timestamp: new Date().toISOString()
}
this.listeners.forEach(listener => listener(message))
}, 300)
}
addListener(listener) {
this.listeners.push(listener)
}
removeListener(listener) {
this.listeners = this.listeners.filter(l => l !== listener)
}
getRandomMessage() {
const messages = [
“你好,最近怎么样?”,
“你看到我发的文件了吗?”,
“我们明天什么时候开会?”,
“那个项目进展如何?”,
“周末有什么计划吗?”,
“我找到了解决那个问题的方法”,
“需要帮忙吗?”,
“记得查看邮件”,
“这个功能什么时候能完成?”,
“客户反馈很不错”
]
return messages[Math.floor(Math.random() * messages.length)]
}
}
// 聊天应用Vue实例
new Vue({
el: ‘#app’,
data: {
loginForm: {
username: ”,
password: ”
},
currentUser: null,
contacts: [
{ id: 2, name: ‘张经理’, online: true },
{ id: 3, name: ‘李设计师’, online: false },
{ id: 4, name: ‘王开发’, online: true },
{ id: 5, name: ‘赵测试’, online: true },
{ id: 6, name: ‘孙产品’, online: false }
],
currentConversation: null,
messages: [],
newMessage: ”,
unreadCounts: {},
websocket: new MockWebSocket()
},
computed: {
currentUserId() {
return this.currentUser ? this.currentUser.id : null
}
},
methods: {
login() {
if (this.loginForm.username) {
this.currentUser = {
id: 1,
name: this.loginForm.username
}
// 连接WebSocket
this.websocket.currentUserId = this.currentUser.id
this.websocket.contacts = this.contacts
this.websocket.connect()
this.websocket.addListener(this.handleWebSocketMessage)
// 初始化未读计数
this.contacts.forEach(contact => {
this.unreadCounts[contact.id] = 0
})
}
},
register() {
if (this.loginForm.username) {
alert(`用户 ${this.loginForm.username} 注册成功!`)
this.login()
}
},
logout() {
this.currentUser = null
this.currentConversation = null
this.messages = []
this.websocket.disconnect()
},
selectConversation(contact) {
this.currentConversation = contact
this.messages = this.getConversationMessages(contact.id)
// 重置未读计数
this.unreadCounts[contact.id] = 0
},
getConversationMessages(contactId) {
// 模拟从服务器获取消息
return [
{ id: 1, sender: contactId, receiver: 1, content: ‘你好,最近项目进展如何?’, timestamp: ‘2023-07-15T10:30:00Z’ },
{ id: 2, sender: 1, receiver: contactId, content: ‘一切顺利,下周可以完成第一版’, timestamp: ‘2023-07-15T10:32:00Z’ },
{ id: 3, sender: contactId, receiver: 1, content: ‘太好了!客户很期待看到成果’, timestamp: ‘2023-07-15T10:35:00Z’ }
]
},
getLastMessage(contactId) {
const messages = this.getConversationMessages(contactId)
return messages.length > 0 ? messages[messages.length – 1].content : ‘没有消息’
},
sendMessage() {
if (this.newMessage.trim() && this.currentConversation) {
const message = {
id: Date.now(),
sender: this.currentUser.id,
receiver: this.currentConversation.id,
content: this.newMessage,
timestamp: new Date().toISOString()
}
// 通过WebSocket发送
this.websocket.send(message)
// 添加到本地消息列表
this.messages.push(message)
// 清空输入框
this.newMessage = ”
}
},
handleWebSocketMessage(message) {
// 如果消息是发给当前用户的
if (message.receiver === this.currentUser.id) {
// 如果当前正在查看该联系人的聊天,直接添加消息
if (this.currentConversation && this.currentConversation.id === message.sender) {
this.messages.push(message)
} else {
// 否则增加未读计数
this.unreadCounts[message.sender] = (this.unreadCounts[message.sender] || 0) + 1
}
}
},
formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString([], { hour: ‘2-digit’, minute: ‘2-digit’ })
}
},
mounted() {
// 初始化模拟数据
this.contacts.forEach(contact => {
this.unreadCounts[contact.id] = Math.floor(Math.random() * 3)
})
}
});