从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(九) 消息接口
1.后端
首先在models下创建 message.model.js
import mongoose from "mongoose";
const messageSchema = new mongoose.Schema(
{
// 发送人Id
senderId: {
type: mongoose.Schema.Types.ObjectId, //在 Mongoose 模型中定义 _id 字段
required: true,
ref: "User"
},
// 接收人
receiverId: {
type: mongoose.Schema.Types.ObjectId, //在 Mongoose 模型中定义 _id 字段
required: true,
ref: "User"
},
// 文本
text: {
type: String
},
// 图片
image: {
type: String
}
},
{timestamps: true});
const Message = mongoose.model('Message', messageSchema);
完善message.controller.js中 getMessages 和 sendMessages 方法
import User from '../models/user.model.js';
import Message from '../models/message.model.js';
import cloudinary from '../lib/cloudinary.js';
export const getUsersForSidebar = async (req, res) => {
try {
// 获取当前登录用户的id
const loggedInUserId = req.user._id;
// 过滤用户 所有不等于当前用户id 的用户 .select 查询时排除password
const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } }).select("-password");
res.status(200).json(filteredUsers);
} catch (error) {
console.log("error in getSidebarUsers: ");
res.status(500).json({ message: error.message });
}
}
// 点击左侧用户时,获取该用户与当前用户之间的聊天记录
export const getMessages = async (req, res) => {
try {
const {id: userToChatId} = req.params;
// 发送人id 是当前用户id
const myId = req.user._id;
// 获取当前用户与点击的用户之间的聊天记录
const messages = await Message.find({
$or: [
{senderId: myId, receiverId: userToChatId},
{senderId: userToChatId, receiverId: myId}
]
})
res.status(200).json(messages);
} catch (error) {
console.log("error in getMessages controller: ");
res.status(500).json({ message: error.message });
}
}
// 发送消息
export const sendMessage = async (req, res) => {
try {
const {text,image} = req.body;
const {id: receiverId} = req.params;
const senderId = req.user._id;
let imageUrl;
if(image) {
// 上传base64图片到cloudinary
const uploadResonse = await cloudinary.uploader.upload(image)
imageUrl = uploadResonse.secure_url;
}
// 构建消息对象
const newMessage = new Message({
senderId,
receiverId,
text,
image: imageUrl
})
await newMessage.save();
res.status(201).json(newMessage);
} catch (error) {
console.log("error in sendMessage controller: ");
res.status(500).json({ message: error.message });
}
}
2.前端
在components 下 新建一个ChatHeader.jsx
import { useChatStore } from "../store/useChatStore";
import { useAuthStore } from "../store/useAuthStore";
import { X } from "lucide-react";
const ChatHeader = () => {
const {selectedUser,setSelectedUser} = useChatStore();
return (
<div className="p-2.5 border-b border-base-300">
<div className="flex items-center justify-between">
<div className="flex items-center">
{/* 头像 */}
<div className="avatar">
<div className="size-10 rounded-full relative">
<img src={selectedUser.profilePic || "https://picsum.photos/200"} alt={selectedUser.userName} />
</div>
</div>
{/* 用户信息 */}
<div>
<h3 className="font-medium">{selectedUser.userName}</h3>
</div>
</div>
{/* 关闭按钮 */}
<button onClick={()=>setSelectedUser(null)}>
<X/>
</button>
</div>
</div>
)
}
export default ChatHeader
然后再ChatBox.jsx 引入
import {useEffect} from "react"
import { useChatStore } from "../store/useChatStore"
import { useAuthStore } from "../store/useAuthStore"
import {formatMessageTime} from "@/lib/util"
import ChatHeader from "./ChatHeader"
const ChatBox = () => {
const {messages, getMessages, isMessagesLoading, selectedUser} = useChatStore()
const {authUser} = useAuthStore()
useEffect(()=>{
getMessages(selectedUser._id)
},[selectedUser._id, getMessages])
if(isMessagesLoading) return <div>Loading...</div>
return (
<div className="flex-1 flex flex-col overflow-auto">
{/* 聊天框头部 */}
<ChatHeader/>
{/* 聊天消息 */}
<div className="flex-1 overflow-auto p-4 space-y-4">
{messages.map((message)=> (
<div
key={message._id}
// 消息的发送者id和当前用户id一致,则显示在右侧,否则显示在左侧
className={`chat ${message.senderId===authUser._id ? 'chat-end' : 'chat-start'}`}
>
<div className="chat-image avatar">
<div className="size-10 rounded-full border">
<img
src={message.senderId === authUser._id ? authUser.profilePic || 'http://via.placeholder.com/150' : selectedUser.profilePic}
alt=""
/>
</div>
</div>
<div className="chat-header mb-1">
<time className="text-xs opacity-50 ml-1">{formatMessageTime(message.createdAt)}</time>
</div>
{/* flex-col 图片和文字上下排列 */}
<div className="chat-bubble flex flex-col">
{message.image && (
<img
src={message.image}
alt=""
className="sm:max-w-[200px] rounded-md mb-2"
/>
)}
{message.text && <p>{message.text}</p>}
</div>
</div>
))}
</div>
{/* 消息输入 */}
</div>
)
}
export default ChatBox
完善useChatStore.js
import {create} from "zustand";
import toast from "react-hot-toast"
import axiosInstance from "../lib/axios";
export const useChatStore = create((set, get) => ({
messages: [],
users: [],
selectedUser: null,
isUserLoading:false,
isMessageLoading:false,
// 选择一个联系人
setSelectedUser: (user) => {
set({selectedUser: user});
},
getUsers: async () => {
set({isUserLoading: true});
try {
const res = await axiosInstance.get("/messages/users");
set({users: res.data});
} catch{
toast.error("Error while fetching users");
} finally{
set({isUserLoading: false});
}
},
getMessages: async (userId) => {
set({isMessageLoading: true});
try {
const res = await axiosInstance.get(`/messages/${userId}`);
set({messages: res.data});
} catch {
toast.error("Error while fetching messages");
} finally {
set({isMessageLoading: false});
}
},
sendMessages: async (messageData) => {
const {selectedUser, messages} = get();
try {
const res = await axiosInstance.post(`/messages/send/${selectedUser._id}`, messageData);
set({messages: [...messages, res.data]});
} catch(error) {
toast.error("Error while sending message:" + error.message);
}
}
}))
效果如下
好这篇就到这 下一篇 实现聊天功能