Vue 3 + Composition API 实战:从零构建一个可复用的聊天气泡组件
Vue 3 Composition API 实战从零构建可复用的聊天气泡组件在当今前端开发领域组件化思维已经成为构建复杂应用的基石。Vue 3带来的Composition API更是将这种思维提升到了新的高度让我们能够以更灵活、更模块化的方式组织代码逻辑。本文将带你从零开始使用Vue 3和Composition API构建一个高度可复用的聊天气泡组件不仅实现基本功能还会深入探讨如何通过逻辑封装提升组件的可维护性和复用性。1. 项目初始化与基础结构搭建首先我们需要创建一个新的Vue 3项目。如果你还没有安装Vue CLI可以通过以下命令进行安装npm install -g vue/cli vue create vue-chat-bubble cd vue-chat-bubble接下来我们创建一个基础的聊天气泡组件结构。在src/components目录下新建ChatBubble.vue文件template div classchat-container div classmessage-list !-- 消息列表将在这里渲染 -- /div div classmessage-input !-- 输入区域将在这里实现 -- /div /div /template script setup // Composition API逻辑将在这里编写 /script style scoped /* 样式将在这里定义 */ /style这个基础结构包含了三个主要部分消息列表容器、输入区域和样式定义。我们使用script setup语法这是Vue 3中Composition API的编译时语法糖可以让我们更简洁地编写组件逻辑。2. 使用Composition API管理状态Composition API的核心优势在于能够将相关逻辑组织在一起而不是按照选项API的生命周期来分割代码。让我们首先定义组件需要管理的状态script setup import { ref, reactive, computed } from vue // 当前用户身份 const currentUser ref(UserA) // 消息列表 const messages reactive([ { id: 1, sender: UserA, content: 你好, timestamp: new Date() }, { id: 2, sender: UserB, content: 你好最近怎么样, timestamp: new Date() } ]) // 输入框内容 const inputMessage ref() // 用户列表可用于选择发送者 const users ref([UserA, UserB]) /script这里我们使用了ref和reactive来定义响应式数据。ref适用于基本类型和对象引用而reactive则更适合复杂的对象结构。对于消息列表我们使用reactive因为它是一个数组我们需要跟踪数组内部的变化。3. 实现消息发送功能现在我们来添加发送消息的功能。这包括处理用户输入和更新消息列表script setup // ...之前的代码... const sendMessage () { if (!inputMessage.value.trim()) return const newMessage { id: Date.now(), sender: currentUser.value, content: inputMessage.value.trim(), timestamp: new Date() } messages.push(newMessage) inputMessage.value // 自动滚动到最新消息 nextTick(() { const messageList document.querySelector(.message-list) messageList.scrollTop messageList.scrollHeight }) } /script template !-- ...之前的模板代码... -- div classmessage-input select v-modelcurrentUser classuser-select option v-foruser in users :keyuser :valueuser{{ user }}/option /select input v-modelinputMessage keyup.entersendMessage placeholder输入消息... classmessage-input-field / button clicksendMessage classsend-button发送/button /div /template这个实现包含了几个关键点使用v-model实现表单元素的双向绑定通过keyup.enter监听回车键事件使用nextTick确保DOM更新后执行滚动操作消息对象包含完整的信息便于后续扩展4. 渲染聊天气泡并添加样式聊天气泡的核心视觉特征是不同用户的对话显示在不同侧并有不同的颜色。让我们实现这个功能template div classchat-container div classmessage-list div v-formessage in messages :keymessage.id :class[message-bubble, { current-user: message.sender currentUser, other-user: message.sender ! currentUser }] div classmessage-sender{{ message.sender }}/div div classmessage-content{{ message.content }}/div div classmessage-time {{ formatTime(message.timestamp) }} /div /div /div !-- 输入区域保持不变 -- /div /template script setup // ...之前的代码... const formatTime (date) { return date.toLocaleTimeString([], { hour: 2-digit, minute: 2-digit }) } /script style scoped .chat-container { display: flex; flex-direction: column; height: 500px; max-width: 600px; margin: 0 auto; border: 1px solid #e1e1e1; border-radius: 8px; overflow: hidden; background-color: #f9f9f9; } .message-list { flex: 1; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; } .message-bubble { max-width: 70%; padding: 12px; border-radius: 12px; position: relative; } .current-user { align-self: flex-end; background-color: #007bff; color: white; border-bottom-right-radius: 0; } .other-user { align-self: flex-start; background-color: #e9ecef; color: #212529; border-bottom-left-radius: 0; } .message-sender { font-weight: bold; font-size: 0.8em; margin-bottom: 4px; } .message-content { word-wrap: break-word; } .message-time { font-size: 0.7em; opacity: 0.7; text-align: right; margin-top: 4px; } .message-input { display: flex; padding: 12px; background-color: white; border-top: 1px solid #e1e1e1; } .user-select { margin-right: 8px; padding: 8px; border-radius: 4px; border: 1px solid #ced4da; } .message-input-field { flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #ced4da; margin-right: 8px; } .send-button { padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .send-button:hover { background-color: #0056b3; } /style这段代码实现了消息气泡根据发送者不同显示在不同侧当前用户的消息使用蓝色背景其他用户的消息使用灰色背景每条消息显示发送者、内容和时间响应式设计适应不同屏幕尺寸美观的视觉效果和交互状态5. 使用Composition API封装业务逻辑为了提升组件的复用性我们可以将聊天逻辑提取到一个独立的组合式函数中。创建src/composables/useChat.jsimport { ref, reactive, computed } from vue export default function useChat(initialUsers, initialMessages []) { const currentUser ref(initialUsers[0]) const users ref(initialUsers) const messages reactive(initialMessages) const inputMessage ref() const sendMessage () { if (!inputMessage.value.trim()) return const newMessage { id: Date.now(), sender: currentUser.value, content: inputMessage.value.trim(), timestamp: new Date() } messages.push(newMessage) inputMessage.value return newMessage } const formatTime (date) { return date.toLocaleTimeString([], { hour: 2-digit, minute: 2-digit }) } return { currentUser, users, messages, inputMessage, sendMessage, formatTime } }然后修改我们的组件使用这个组合式函数script setup import useChat from /composables/useChat const { currentUser, users, messages, inputMessage, sendMessage, formatTime } useChat( [UserA, UserB], [ { id: 1, sender: UserA, content: 你好, timestamp: new Date() }, { id: 2, sender: UserB, content: 你好最近怎么样, timestamp: new Date() } ] ) /script这种封装方式带来了几个显著优势逻辑复用现在可以在多个组件中使用相同的聊天逻辑可测试性聊天逻辑可以独立于组件进行测试可维护性相关逻辑集中在一个地方便于理解和修改灵活性可以轻松扩展更多功能如消息撤回、已读回执等6. 添加高级功能与优化一个完整的聊天组件还需要考虑更多细节。让我们添加一些常见的高级功能6.1 消息时间分组在长时间对话中将消息按日期分组可以提升可读性。首先更新useChat.js// ...之前的代码... const groupedMessages computed(() { const groups [] let currentDate null messages.forEach(message { const messageDate message.timestamp.toDateString() if (messageDate ! currentDate) { groups.push({ type: date, date: messageDate, id: date-${messageDate} }) currentDate messageDate } groups.push({ ...message, type: message }) }) return groups }) return { // ...之前的返回值... groupedMessages }然后更新组件模板template div classchat-container div classmessage-list template v-foritem in groupedMessages :keyitem.id div v-ifitem.type date classdate-divider {{ formatDate(item.date) }} /div div v-else :class[message-bubble, { current-user: item.sender currentUser, other-user: item.sender ! currentUser }] div classmessage-sender{{ item.sender }}/div div classmessage-content{{ item.content }}/div div classmessage-time {{ formatTime(item.timestamp) }} /div /div /template /div !-- 输入区域保持不变 -- /div /template script setup // ...之前的代码... const formatDate (dateString) { const date new Date(dateString) const today new Date() if (date.toDateString() today.toDateString()) { return 今天 } const yesterday new Date(today) yesterday.setDate(yesterday.getDate() - 1) if (date.toDateString() yesterday.toDateString()) { return 昨天 } return date.toLocaleDateString() } /script style scoped /* 添加日期分隔线样式 */ .date-divider { text-align: center; margin: 16px 0; color: #6c757d; font-size: 0.8em; position: relative; } .date-divider::before, .date-divider::after { content: ; position: absolute; top: 50%; width: 30%; height: 1px; background-color: #dee2e6; } .date-divider::before { left: 0; } .date-divider::after { right: 0; } /style6.2 添加消息状态反馈用户发送消息后提供视觉反馈可以改善用户体验。更新useChat.jsconst sendMessage () { if (!inputMessage.value.trim()) return const tempId temp-${Date.now()} const newMessage { id: tempId, sender: currentUser.value, content: inputMessage.value.trim(), timestamp: new Date(), status: sending } messages.push(newMessage) inputMessage.value // 模拟网络请求延迟 setTimeout(() { const messageIndex messages.findIndex(m m.id tempId) if (messageIndex ! -1) { messages[messageIndex] { ...messages[messageIndex], id: Date.now(), status: delivered } } }, 1000) return newMessage }更新模板和样式template !-- ...之前的模板... -- div :class[message-bubble, { current-user: item.sender currentUser, other-user: item.sender ! currentUser }] div classmessage-sender{{ item.sender }}/div div classmessage-content{{ item.content }}/div div classmessage-status {{ formatTime(item.timestamp) }} span v-ifitem.sender currentUser classstatus-icon {{ getStatusIcon(item.status) }} /span /div /div /template script setup // ...之前的代码... const getStatusIcon (status) { switch (status) { case sending: return case delivered: return ✓ case read: return ✓✓ default: return } } /script style scoped /* 添加状态图标样式 */ .message-status { display: flex; align-items: center; justify-content: flex-end; font-size: 0.7em; opacity: 0.7; margin-top: 4px; } .status-icon { margin-left: 4px; } /style6.3 实现消息撤回功能添加消息撤回功能可以提升用户体验。更新useChat.js// ...之前的代码... const recallMessage (messageId) { const messageIndex messages.findIndex(m m.id messageId) if (messageIndex ! -1 messages[messageIndex].sender currentUser.value) { messages[messageIndex] { ...messages[messageIndex], content: 消息已撤回, isRecalled: true } } } return { // ...之前的返回值... recallMessage }更新模板template !-- ...之前的模板... -- div :class[message-bubble, { current-user: item.sender currentUser, other-user: item.sender ! currentUser, recalled: item.isRecalled }] contextmenu.preventhandleContextMenu($event, item) !-- ...之前的内容... -- /div /template script setup // ...之前的代码... const handleContextMenu (event, message) { if (message.sender currentUser.value !message.isRecalled) { if (confirm(确定要撤回这条消息吗)) { recallMessage(message.id) } } } /script style scoped /* 添加撤回消息样式 */ .recalled .message-content { font-style: italic; opacity: 0.7; } /style7. 组件Props设计与外部集成为了使我们的组件真正可复用我们需要定义清晰的props接口。更新ChatBubble.vuescript setup import { defineProps, defineEmits } from vue import useChat from /composables/useChat const props defineProps({ initialUsers: { type: Array, required: true, validator: value value.length 0 }, initialMessages: { type: Array, default: () [] }, currentUser: { type: String, default: null }, bubbleColors: { type: Object, default: () ({ currentUser: #007bff, otherUser: #e9ecef }) }, showTimestamps: { type: Boolean, default: true }, showSenderNames: { type: Boolean, default: true } }) const emit defineEmits([message-sent, message-recalled]) const { currentUser: internalCurrentUser, users, messages, inputMessage, sendMessage: internalSendMessage, formatTime, recallMessage: internalRecallMessage, groupedMessages } useChat( props.initialUsers, props.initialMessages ) // 如果通过props传递了currentUser则使用它 if (props.currentUser) { internalCurrentUser.value props.currentUser } const sendMessage () { const message internalSendMessage() if (message) { emit(message-sent, message) } } const recallMessage (messageId) { internalRecallMessage(messageId) emit(message-recalled, messageId) } // ...之前的工具函数... /script template div classchat-container div classmessage-list template v-foritem in groupedMessages :keyitem.id div v-ifitem.type date classdate-divider {{ formatDate(item.date) }} /div div v-else :class[message-bubble, { current-user: item.sender internalCurrentUser, other-user: item.sender ! internalCurrentUser, recalled: item.isRecalled }] contextmenu.preventhandleContextMenu($event, item) :style{ backgroundColor: item.sender internalCurrentUser ? bubbleColors.currentUser : bubbleColors.otherUser, color: item.sender internalCurrentUser ? white : inherit } div classmessage-sender v-ifshowSenderNames {{ item.sender }} /div div classmessage-content{{ item.content }}/div div classmessage-status v-ifshowTimestamps {{ formatTime(item.timestamp) }} span v-ifitem.sender internalCurrentUser classstatus-icon {{ getStatusIcon(item.status) }} /span /div /div /template /div div classmessage-input select v-modelinternalCurrentUser classuser-select :disabled!!currentUser option v-foruser in users :keyuser :valueuser {{ user }} /option /select input v-modelinputMessage keyup.entersendMessage placeholder输入消息... classmessage-input-field / button clicksendMessage classsend-button发送/button /div /div /template现在我们的组件可以通过props进行高度定制template ChatBubble :initial-users[张三, 李四] :current-usercurrentUser :bubble-colors{ currentUser: #4CAF50, otherUser: #F5F5F5 } message-senthandleNewMessage message-recalledhandleRecalledMessage / /template8. 性能优化与最佳实践为了确保我们的组件在大规模应用中也能表现良好我们需要考虑一些性能优化策略8.1 虚拟滚动对于可能包含大量消息的聊天组件实现虚拟滚动可以显著提升性能。我们可以使用vue-virtual-scroller库npm install vue-virtual-scroller然后更新我们的组件template div classchat-container RecycleScroller classmessage-list :itemsgroupedMessages :item-size100 key-fieldid v-slot{ item } template v-ifitem.type date div classdate-divider {{ formatDate(item.date) }} /div /template template v-else div :class[message-bubble, { current-user: item.sender internalCurrentUser, other-user: item.sender ! internalCurrentUser, recalled: item.isRecalled }] contextmenu.preventhandleContextMenu($event, item) :style{ backgroundColor: item.sender internalCurrentUser ? bubbleColors.currentUser : bubbleColors.otherUser, color: item.sender internalCurrentUser ? white : inherit } !-- 消息内容保持不变 -- /div /template /RecycleScroller !-- 输入区域保持不变 -- /div /template script setup import { RecycleScroller } from vue-virtual-scroller import vue-virtual-scroller/dist/vue-virtual-scroller.css // ...其余代码保持不变... /script style scoped /* 更新消息列表样式 */ .message-list { height: calc(100% - 60px); } /style8.2 使用Web Workers处理繁重任务如果我们的聊天组件需要处理大量数据或复杂计算如消息搜索可以考虑使用Web Workers来避免阻塞主线程。创建一个src/workers/chatWorker.jsself.onmessage function(e) { const { type, data } e.data switch (type) { case SEARCH_MESSAGES: const results data.messages.filter(message message.content.toLowerCase().includes(data.query.toLowerCase()) ) self.postMessage({ type: SEARCH_RESULTS, results }) break case GROUP_MESSAGES: // 实现消息分组逻辑 break // 可以添加更多任务类型 } }然后在组件中使用script setup import { ref, onMounted, onBeforeUnmount } from vue const chatWorker ref(null) onMounted(() { if (window.Worker) { chatWorker.value new Worker(new URL(/workers/chatWorker.js, import.meta.url)) chatWorker.value.onmessage function(e) { const { type, data } e.data switch (type) { case SEARCH_RESULTS: // 处理搜索结果 break } } } }) onBeforeUnmount(() { if (chatWorker.value) { chatWorker.value.terminate() } }) const searchMessages (query) { if (chatWorker.value) { chatWorker.value.postMessage({ type: SEARCH_MESSAGES, data: { messages: messages, query: query } }) } else { // 回退到主线程处理 } } /script8.3 使用provide/inject实现深层嵌套如果我们的聊天组件需要被深层嵌套在组件树中可以使用provide/inject API来避免prop逐层传递script setup import { provide } from vue // 提供聊天配置 provide(chatConfig, { bubbleColors: props.bubbleColors, showTimestamps: props.showTimestamps, showSenderNames: props.showSenderNames }) /script然后在子组件中script setup import { inject } from vue const chatConfig inject(chatConfig) /script8.4 使用Teleport实现全屏模式如果我们需要实现聊天组件的全屏模式可以使用Vue 3的Teleport功能template button clickisFullscreen !isFullscreen {{ isFullscreen ? 退出全屏 : 全屏 }} /button Teleport tobody div v-ifisFullscreen classfullscreen-chat click.selfisFullscreen false div classfullscreen-content ChatBubble v-bind$props / /div /div /Teleport /template script setup import { ref } from vue const isFullscreen ref(false) /script style scoped .fullscreen-chat { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1000; } .fullscreen-content { width: 90%; height: 90%; max-width: 1200px; } /style9. 测试与调试策略为了确保我们的组件质量我们需要实现全面的测试策略。我们可以使用Vue Test Utils和Jest来编写测试。9.1 单元测试创建tests/unit/ChatBubble.spec.jsimport { mount } from vue/test-utils import ChatBubble from /components/ChatBubble.vue describe(ChatBubble.vue, () { it(renders initial messages, () { const wrapper mount(ChatBubble, { props: { initialUsers: [UserA, UserB], initialMessages: [ { id: 1, sender: UserA, content: Hello, timestamp: new Date() } ] } }) expect(wrapper.text()).toContain(Hello) }) it(emits message-sent event when sending a message, async () { const wrapper mount(ChatBubble, { props: { initialUsers: [UserA, UserB] } }) await wrapper.find(.message-input-field).setValue(New message) await wrapper.find(.send-button).trigger(click) expect(wrapper.emitted(message-sent)).toBeTruthy() }) // 可以添加更多测试用例 })9.2 组件测试对于更复杂的交互我们可以使用Testing Library进行组件测试import { render, fireEvent, screen } from testing-library/vue import ChatBubble from /components/ChatBubble.vue test(allows sending messages, async () { render(ChatBubble, { props: { initialUsers: [UserA, UserB] } }) const input screen.getByPlaceholderText(输入消息...) await fireEvent.update(input, Test message) await fireEvent.click(screen.getByText(发送)) expect(screen.getByText(Test message)).toBeInTheDocument() })9.3 E2E测试对于端到端测试我们可以使用Cypressdescribe(ChatBubble, () { it(sends and displays messages, () { cy.visit(/) cy.get(.message-input-field).type(Hello Cypress{enter}) cy.contains(.message-content, Hello Cypress).should(exist) }) })9.4 性能测试我们可以使用Chrome DevTools的Performance面板来测试组件的渲染性能打开DevTools并切换到Performance面板开始录制添加100条消息停止录制并分析结果重点关注脚本执行时间布局和绘制时间内存使用情况10. 文档与示例为了让其他开发者能够轻松使用我们的组件我们需要提供清晰的文档和示例。我们可以使用Storybook来展示组件的各种状态和用法。10.1 安装Storybooknpx sb init npm install10.2 创建ChatBubble的故事创建src/stories/ChatBubble.stories.jsimport ChatBubble from /components/ChatBubble.vue export default { title: Components/ChatBubble, component: ChatBubble, argTypes: { initialUsers: { control: array }, bubbleColors: { control: object }, onMessageSent: { action: message-sent } } } const Template (args) ({ components: { ChatBubble }, setup() { return { args } }, template: ChatBubble v-bindargs / }) export const Default Template.bind({}) Default.args { initialUsers: [Alice, Bob], initialMessages: [ { id: 1, sender: Alice, content: 你好, timestamp: new Date() }, { id: 2, sender: Bob, content: 你好最近怎么样, timestamp: new Date() } ] } export const CustomColors Template.bind({}) CustomColors.args { initialUsers: [Alice, Bob], bubbleColors: { currentUser: #FF5722, otherUser: #9C27B0 } }10.3 编写组件文档创建src/components/ChatBubble.docs.md# ChatBubble 组件 一个使用Vue 3和Composition API构建的可复用聊天气泡组件。 ## 功能特性 - 支持多用户聊天 - 可定制的聊天气泡颜色 - 消息撤回功能 - 响应式设计 - 高性能虚拟滚动可选 ## 基本用法 vue template ChatBubble :initial-users[UserA, UserB] :initial-messagesinitialMessages message-senthandleNewMessage / /template script setup import { ref } from vue import ChatBubble from ./components/ChatBubble.vue const initialMessages ref([ { id: 1, sender: UserA, content: Hello, timestamp: new Date() } ]) const handleNewMessage (message) { console.log(New message:, message) } /script ## Props | 名称 | 类型 | 默认值 | 说明 | |------|------|--------|------| | initialUsers | Array | - | 初始用户列表必需 | | initialMessages | Array | [] | 初始消息列表 | | currentUser | String | null | 当前用户如果未指定则使用initialUsers[0] | | bubbleColors | Object | { currentUser: #007bff, otherUser: #e9ecef } | 气泡颜色配置 | | showTimestamps | Boolean | true | 是否显示时间戳 | | showSenderNames | Boolean | true | 是否显示发送者名称 | ## 事件 | 名称 | 参数 | 说明 | |------|------|------| | message-sent | message | 当发送新消息时触发 | | message-recalled | messageId | 当消息被撤回时触发 | ## 插槽 | 名称 | 说明 | |------|------| | message-content | 自定义消息内容显示 | | message-input | 自定义输入区域 |10.4 创建演示页面我们可以创建一个专门的演示页面来展示组件的各种功能template div classdemo-container h1ChatBubble 组件演示/h1 div classdemo-section h2基本用法/h2 ChatBubble :initial-users[Alice, Bob] :initial-messagesbasicMessages / /div div classdemo-section h2自定义颜色/h2 ChatBubble :initial-users[Alice, Bob] :bubble-colors{ currentUser: #4CAF50, otherUser: #F5F5F5 } / /div div classdemo-section h2隐藏时间戳/h2 ChatBubble :initial-users[Alice, Bob] :show-timestampsfalse / /div /div /template script setup import { ref } from vue import ChatBubble from /components/ChatBubble.vue const basicMessages ref([ { id: 1, sender: Alice, content: 你好, timestamp: new Date() }, { id: 2, sender: Bob, content: 你好最近怎么样, timestamp: new Date() } ]) /script style scoped .demo-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .demo-section { margin-bottom: 40px; } .demo-section h2 { margin-bottom: 10px; color: #333; } /style
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2429939.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!