ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue.js中实现@提及:Tribute.js动态用户选择与删除同步

Vue.js中实现@提及:Tribute.js动态用户选择与删除同步 简介这是一份面向Vue.js前端开发者的聊天室提及功能示例源码完整演示如何借助Tribute.js在输入框中实现动态用户。项目聚焦三个核心环节用户输入后通过Ajax请求从服务端实时获取人员列表从列表选中人员时解析用户名、ID等数据并同步到视图删除已提及成员时自动更新已选数据数组。此外示例还展示了Tribute.js以Vue插件形式接入项目灵活配置触发词、数据源、模板等选项与Vue的生命周期和数据绑定机制协同工作。资源以RAR压缩包形式提供大小约30.57MB上游暂未提供文件总数与具体文件类型明细。当前已有1457人学习下载适合正在开发聊天室、评论或协同编辑场景并希望实现功能的中级前端学习者可从中获得可直接改造的示例思路与细节实现参考。1. 为什么功能不能直接基于textarea实现一个聊天室需求摆在面前Vue.js里实现提及输入框里一下要弹出现有成员选完能回填删掉还能撤销。多数人第一反应是找个textarea自己监听结果很快发现夹在contenteditable和原生选区之间光把光标移到符号后面就要折腾半天。vue-tribute-demo这个示例基于Vue.js框架用Tribute.js把GitHub风格的提及拆成动态获取匹配用户、选中回填、删除已提及人三个可复用环节由Tribute接管最麻烦的输入框选区操作。适合正在做Web聊天室、IM评论区、工单回复框的Vue开发者也适合想搞清楚“为什么功能不能直接基于textarea实现”的人。2. 从textarea到Tribute.js选区接管与Vue协作边界2.1 textarea实现的三个硬伤先说textarea方案为什么做不干净。textarea在同一段文本里无法区分样式张三和普通消息是同一个字体同一种颜色用户看不到高亮也就不知道哪些人已经被提及。其次计算光标到段首的距离需要使用selectionStart再和符号位置做偏移匹配一旦用户插入过图片、换行或者Emoji偏移量就对不上了。最后替换选中文本时textarea只能拼纯文本提醒的消息体想给前端节点加data-id几乎没有下手空间。这三个问题叠加基本可以断定一个中型聊天室的功能不适合从textarea硬写。真正能支撑高亮的是contenteditable。Tribute.js把输入框内容当作DOM树而不是字符串处理触发词被识别后它会在当前光标位置计算选区范围把用户已键入的前缀与后续字符包裹进一个可编辑子节点菜单从该节点下方弹出。选人完成后菜单项会替换这个节点同时保留菜单项里配置的属性字段。对Vue开发者来说需要理解的是Tribute不负责消息内容的最终存储它只负责让输入框里看起来像张三。谁被加入会话、谁又被删掉是组件里维护selectedUsers数组的事。2.2 Tribute.js的核心配置与动态数据源Tribute的初始化是一个纯JavaScript对象不依赖Vue响应式因此可以放在任意生命周期里创建。下面这段配置是vue-tribute-demo最常见的使用方式import Tribute from tributejs const tribute new Tribute({ trigger: , values: (text, cb) { fetch(/api/users?query${encodeURIComponent(text)}) .then((res) res.json()) .then((data) cb(data)) }, lookup: username, fillAttr: username, menuItemTemplate: (item) span${item.original.username}/span, selectTemplate: (item) span>GET /api/users?queryzh返回结构是一个包含用户ID、用户名和头像的对象数组{ code: 0, data: [ { id: 1001, username: zhangSan, avatar: }, { id: 1002, username: zhaoSi, avatar: } ] }Tribute的values函数能拿到当前光标前输入的匹配文本用户键入了zhtext就是zh。把text拼进query参数发起请求回调里传入返回数组即可values: (text, cb) { const keyword text.trim() if (!keyword) return cb([]) fetch(/api/users?query${encodeURIComponent(keyword)}) .then((res) res.json()) .then((json) { if (json.code 0) cb(json.data) else cb([]) }) .catch(() cb([])) },注意请求失败时回调空数组避免菜单残留上一次的旧数据trim掉首尾空格防止用户键入加空格就发起无意义的查询。values里返回的每一项会被Tribute当作item.original传给lookup和selectTemplate所以后端返回的字段名要提前约定好。3.2 输入防抖与菜单模板字段映射聊天室输入速度很快每敲一个字母都打一次接口让服务端压力上升。常见做法是用lodash的debounce把请求延后300毫秒或者自己维护一个timer。debounce包在values内部即可不必动Tribute其他配置。同时在menuItemTemplate里配好列表展示字段映射关系如下表模板字段数据来源作用usernameitem.original.username菜单第一行显示用户昵称iditem.original.id业务ID用于回填data-idavataritem.original.avatar菜单项头像没有可省略nameitem.original.name用于lookup匹配的备用字段menuItemTemplate拼接HTML时字段值必须做转义。直接把用户昵称塞进innerHTML会开XSS口子聊天室这类暴露给所有在线用户的场景尤其危险一旦有人在用户名里写入html标签其他用户选择他时就会中招。转义函数可以自己写一个replace映射也可以直接引用lodash的escape。3.3 请求乱序问题异步请求还有一个隐藏问题网络返回顺序不一定和键入顺序一致。用户先打z再删掉换成xz的响应如果比x的响应后到菜单就会被旧数据覆盖。简单方案是给每次请求加一个递增序号只有最新序号的响应才能更新菜单let searchSeq 0 values: (text, cb) { const current searchSeq fetch(/api/users?query${encodeURIComponent(text)}) .then((res) res.json()) .then((json) { if (current searchSeq) cb(json.data || []) }) }如果当前请求的序号已经不是最新说明输入框已经有了新内容这个响应直接丢弃。该技巧在快速连续输入名字时很有效是聊天室项目里真实会踩到的边界接口响应越快越不容易暴露但一到弱网环境就立刻原形毕露。4. 选中与删除用户Vue响应式数据流的两个关键操作4.1 selectTemplate回填并同步selectedUsersTribute提供selectTemplate来生成最终回填到输入框的内容返回的是HTML字符串最终被插入contenteditable容器。要保证后续删除和发送都能定位用户需要在回填时把业务ID埋进节点selectTemplate: (item) { const hasUser selectedUsers.value.some((u) u.id item.original.id) if (!hasUser) { selectedUsers.value.push(item.original) } return span classtribute-mention>const syncMentions () { const editorEl editorRef.value const idSet new Set( [...editorEl.querySelectorAll(.tribute-mention)].map((el) el.dataset.id) ) selectedUsers.value selectedUsers.value.filter((u) idSet.has(String(u.id))) } editorEl.addEventListener(input, syncMentions)另一种路径是已选人区域提供叉号按钮直接移除用户。数组操作本身很简单用filter生成新数组即可触发Vue响应式更新但编辑器里的span节点需要根据data-user-id手动找到并移除。两条路径最终都要确保数组和DOM状态一致否则后果就是界面上看起来没有这个人发送的消息体里却带着他的ID。写input监听时需要一并判断compositionend避免中文输入法组合过程中提前触发同步。4.3 消息发送时组装对象与toUserIds发送消息这一步我会遍历编辑器里的mention节点收集data-id组成toUserIds传给服务端而不是直接依赖selectedUsersconst sendMessage () { const editorEl editorRef.value const mentionIds [...editorEl.querySelectorAll(.tribute-mention)].map((el) Number(el.dataset.id) ) const payload { content: editorEl.innerText, toUserIds: mentionIds, mentions: selectedUsers.value.filter((u) mentionIds.includes(u.id)), } api.sendMessage(payload) }content字段用innerText取出纯文本后端保存消息不需要span标签toUserIds从DOM节点上实时获取不依赖selectedUsers数组的状态。这样一来即使某个用户被删除后数组同步稍有延迟发送时也不会把已删除的人通知出去。5. Tribute.js在Vue生命周期中的挂载、销毁与回归验证5.1 mounted中初始化beforeUnmount中销毁Tribute操作的是真实DOM必须在输入框渲染完成后再初始化。onMounted里搭配nextTick确保ref拿到真实节点onBeforeUnmount里调用detach解除监听避免页面切换后监听器残留import { ref, onMounted, onBeforeUnmount, nextTick } from vue import Tribute from tributejs const editorRef ref(null) let tributeInstance null onMounted(async () { await nextTick() tributeInstance new Tribute({ trigger: , values: fetchUsers }) tributeInstance.attach(editorRef.value) }) onBeforeUnmount(() { tributeInstance?.detach() })attach接收的是DOM节点而不是Vue的响应式代理必须用editorRef.value取真实元素detach会移除Tribute在document上注册的键盘和鼠标事件。不销毁的话从聊天室切到其他页面再回来会出现按弹出旧菜单的残留问题。5.2 滚动容器下的菜单定位聊天室滚动容器通常是外层div而不是windowTribute默认把菜单定位在输入框坐标下外层滚动时菜单不会跟着移动。遇到“菜单出现在错误位置或滚出视口”优先排查滚动容器的overflow属性再处理positionMenu函数传入容器元素并在滚动事件里重新计算菜单top值。5.3 手动与自动化验证清单用例操作期望结果触发菜单键入a下拉出现与a匹配的用户选中用户点击第一个编辑器出现带高亮的用户节点删除提及Backspace删除整个节点已选人区域同步减少快速输入连续改选项菜单显示最后一次查询的结果发送消息点击发送按钮消息体里包含toUserIds自动化回归可以只覆盖最易碎的两点输入后tribute-mention节点数量以及selectedUsers数组长度。用Playwright的page.keyboard.type模拟键入断言mention节点出现再按Backspace删除断言数组长度减一。这两项通过说明Tribute挂载、selectTemplate回填和删除同步没有发生回归。本文还有配套的精品资源点击获取
返回列表