ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue + 图灵机器人 H5 聊天改造:数据驱动渲染与异步时序修复

Vue + 图灵机器人 H5 聊天改造:数据驱动渲染与异步时序修复 简介压缩包内为一份PDF文档系统介绍基于Vue.js实现H5机器人聊天测试版的前端交互应用。面向具备HTML/CSS/JavaScript基础、希望学习Vue组件化开发与聊天界面搭建的开发者可快速掌握从Vue实例创建、数据绑定到动态渲染聊天消息的完整流程。文档详细讲解了消息数据模型type/content/time的构建并通过v-for指令循环生成机器人与用户的不同样式消息框同时给出输入框与发送按钮的事件监听实现包括获取输入内容、更新消息数组、清空输入框等核心逻辑。还延伸介绍了结合WebSocket或API接口实现机器人回复、以及引入Dialogflow等NLP服务提升智能性的思路。整包仅1个PDF文件大小46KB内容紧凑、示例代码清晰便于对照实践。目前已有1176人学习适合作为Vue.js入门聊天应用、课程设计或移动端H5交互界面的参考笔记。1. 一个 Vue 图灵机器人混编的 H5 聊天 Demo拿到这份题名为「vue.js 实现 h5 机器人聊天测试版」的源码时第一反应是好奇为什么一个宣称 Vue.js 实现的聊天页面里会同时出现$.ajax、mui.css和rpx单位打开文件后发现这其实是一份「小程序或 uni-app 页面转写 H5」的典型混编代码作者用 Vue 管理了按钮点击和消息回显但消息列表没有用v-for渲染机器人回复也没有真正进入数据流而是用 jQuery 请求图灵机器人接口、再手动赋值给一个data字段。这种写法在原型阶段很常见但它暴露了三个值得展开的问题聊天消息列表如何用 Vue 的数组动态渲染、异步请求返回后如何写回数据层而不丢状态、以及移动端 H5 聊天页的输入框布局到底怎么处理才不会被键盘顶飞。这篇就把这三处拆开顺着源码把静态双行聊天改造成真正由数据驱动的消息列表同时把图灵 API 对接和 H5 布局细节一起收进来。2. 双端消息流与视图结构.reply / .send 的渲染逻辑2.1 从 chat.wxml 注释看这份代码的原生形态源码里有一段被注释掉的!--chat.wxml--这是关键线索。wxml是微信小程序的模板语言也就是说这份代码的最初形态可能是小程序页面后来被搬到 H5。rpx单位也佐证了这一点rpx是小程序的响应式像素单位在 H5 里浏览器并不认需要构建工具编译或者手动改成px、rem、vw才能正常显示。源码里大量使用80rpx、480rpx、120rpx这样的值直接丢到手机浏览器里这些样式不会生效。理解了这一点再看视图结构就清晰了。页面的核心是#cont容器里面有两条写死的消息一条机器人的.reply行一条用户的.send行。.reply和.send都是display: flex区别在于.send设置了justify-content: flex-end让用户消息靠右。每条消息内部是「头像 内容」两段头像用image标签包了一张圆角图内容用.text包文字。机器人气泡白底黑字用户气泡#78fd75绿底左右区分除了对齐方式还有颜色。div classreply div classpic image classimage srcimg/list_img.png/image /div div classcontent div classtext{{ hh }}/div /div /div这里{{ hh }}是 Vue 的插值表达式hh的初始值是「你说啥」。也就是说机器人回复区的文案是绑定在 Vue 实例data.hh上的当 JavaScript 拿到图灵接口返回的文本后执行this.hh str页面上的机器人气泡就会更新。这是整份代码里唯一真正用到 Vue 数据绑定的地方。但注意用户发送的消息走的是另一条路names字段直接绑定在.send .text里点击发送时把输入框的值赋给names。这种「两个静态区块 两个 data 字段」的结构本质上是把 Vue 当成了模板引擎用没有发挥数组渲染的能力。2.2 消息数据的理想结构type 字段决定渲染方向摘要在描述里提到了一种更标准的做法在data里维护一个messages数组每个元素是包含type、content字段的对象模板用v-for遍历数组通过message.type判断渲染成机器人气泡还是用户气泡。原代码中其实已经有这段雏形只不过被注释掉了包括v-if{{item.type 1}}这种写法——注意{{ }}包在v-if里是错的Vue 指令本身不需要插值括号直接写v-ifitem.type 1即可。按这个思路理想的数据模型是data: { messages: [ { type: 1, content: 你好我是老五, time: Date.now() }, { type: 0, content: 你好, time: Date.now() } ] }type: 1表示机器人回复type: 0表示用户发送。time字段用来做时间戳展示也可以作为v-for的:key。模板里遍历messages用v-if/v-else区分两种气泡样式v-bind:class或者直接在行内判断 class 名样式表基本可以复用原项目的.reply/.send——这是把静态版改成动态版的成本最低路径。原代码里的.reply .pic、.send .content这些后代选择器天然适合这种双分支渲染直接换绑定即可。2.3 把写死的双行改成 v-for 渲染动手改造时我把原代码的静态双行删除替换为div idcont div v-for(item, index) in messages :keyindex classmsg-row div classreply v-ifitem.type 1 div classpic img classimage srcimg/list_img.png altrobot / /div div classcontent div classtext{{ item.content }}/div /div /div div classsend v-else div classcontent div classtext{{ item.content }}/div /div div classpic img classimage srcimg/list_img.png altuser / /div /div /div div classsendMessage input idinputs typetext v-modelinputMsg placeholder请输入您要说的话 / button idbtn_submit clickmit()发送/button /div /div这段模板里做三件事用v-for遍历messages每个消息对象独立渲染成一条气泡用v-ifitem.type 1判断消息来源机器人靠左、用户靠右输入框用v-model双向绑定inputMsg替代原来document.getElementById(inputs).value的 DOM 取值方式。:key这里先用index兜底实际项目里如果消息支持删除或插入建议用time或服务端返回的消息 ID 做 key避免 Vue 复用 DOM 导致状态错乱。绑定完 Vue 实例挂载到#contmessages数组初始为空页面首次加载不显示任何气泡。3. 图灵机器人 API 对接与 AJAX 时序问题修复3.1 原代码的请求链路和参数缺陷原代码的机器人回复逻辑写在mit()方法里点击发送按钮后执行两部分先操作 DOM 更新用户消息再发起$.ajaxGET 请求到http://www.tuling123.com/openapi/api携带key和info两个参数。key是图灵机器人开放平台的开发者密钥info是发给机器人的用户输入文本。图灵机器人是国内早期的免费聊天机器人 API接口返回 JSON核心字段是text代表机器人生成的回复文本。但这段代码有两个明显的缺陷。第一个缺陷在请求参数上info: msg.content而msg对象是{ type: 0, src: ..., texts: name }字段名是texts不是content所以msg.content是undefined。也就是说图灵接口拿到的info是空值机器人实际上不知道用户说了什么。第二个缺陷更隐蔽看这段代码$.ajax({ type: get, url: http://www.tuling123.com/openapi/api, data: { key: fa7f4d06b0a24b479d29ea0a01672350, info: msg.content }, success: function(data) { str data.text; } }); console.log(str, str); this.hh str;$.ajax默认是异步请求success回调在请求完成后才执行而this.hh str写在了$.ajax调用之后、请求完成之前。所以第一次点击发送时str还是空字符串hh被赋值为空等请求返回str被赋值为data.text但此时hh不会再更新除非用户再点一次发送。这是一个典型的异步时序 bug把回调外的代码误认为会等请求结束才执行。表现是「机器人第一次不回复第二次回复第一条消息的内容」。3.2 用 Promise 包装替代回调地狱修复思路是把请求封装返回 Promise在.then里更新 Vue 数据让消息写入严格发生在请求成功之后。我并不建议继续用 jQuery 的$.ajaxVue 项目里axios是更常见的选择它基于 Promise且响应拦截器方便统一处理错误码。改造后的mit()方法如下mit() { const text this.inputMsg.trim(); if (text ) return; this.messages.push({ type: 0, content: text }); this.inputMsg ; this.fetchReply(text) .then((reply) { this.messages.push({ type: 1, content: reply }); this.scrollToBottom(); }) .catch((err) { this.messages.push({ type: 1, content: 机器人走神了请稍后再试 }); console.error(图灵接口异常, err); }); }, fetchReply(text) { return axios .get(/api/turing, { params: { info: text } }) .then((res) { if (res.data res.data.text) return res.data.text; return 我没听明白换个说法试试; }); }逻辑说明mit()先做输入校验空消息直接返回接着把用户消息push进messages数组Vue 的响应式系统会让新气泡立即出现在界面上随后清空输入框调用fetchReply发请求。请求成功后再把机器人回复push进去这样用户消息和机器人回复按顺序进入同一个数组渲染层不用关心消息从哪来只需要遍历。失败分支给用户一个兜底提示而不是静默无响应。scrollToBottom是辅助方法用this.$nextTick确保 DOM 更新后把聊天容器滚动到底部。3.3 为什么建议用代理转发而不是直接请求第三方原代码直接请求http://www.tuling123.com/openapi/api这在 H5 上线后会遇到两个问题。第一图灵机器人开放平台已经停止对个人开发者开放新的 API key老 key 大多也已失效www.tuling123.com这套 openapi 接口目前基本处于不可用状态。第二即使接口依然开放前端直接配置 API key 等于把密钥公开在源码里任何人打开控制台都能看到。更稳妥的做法是在自己服务器上做一个转发代理前端请求同域路径由后端把info转发给图灵或你自己的机器人服务再把结果返回给前端。上面代码里的/api/turing就是这个设计——开发时用 Vite 或 webpack-dev-server 的 proxy 配置把/api代理到https://turing.xxx.com生产环境用 Nginx 反代或云函数转发。这样前端代码里不出现第三方域名和密钥修改机器人服务商时也只需改服务端不用重新发布 H5。4. 用 v-for 重写消息列表把静态拼装改成数据驱动4.1 数组 push 与响应式更新Vue 的变更检测机制上一章的改造已经把消息写入了messages数组但这里有一个 Vue 2 的经典坑值得单独说明数组push是 Vue 能检测到的变更方法splice、pop、shift、unshift、sort、reverse也都在白名单里但如果直接按下标赋值比如this.messages[0] xxxVue 2 无法侦测到变化视图不会更新。原代码用this.message list的方式重新赋值其实是在绕这个问题。Vue 3 基于 Proxy 重写了响应式系统按下标赋值已经能被追踪但如果项目还在用 Vue 2统一推荐用push和splice操作消息数组避免数组索引赋值不生效的隐蔽 bug。// Vue 2 推荐的写法 this.messages.push({ type: 1, content: reply }); // 不要这样写view 不会更新 // this.messages[this.messages.length] { type: 1, content: reply };push还有一个好处天然保留了消息的先后顺序。聊天场景的消息流是从上往下追加的push正好符合这个方向不需要额外维护游标或索引。如果要做「加载更多历史消息」的功能则在数组头部用unshift插入同时配合scrollTop保持滚动位置这是另一种操作模式但基础机制不变。4.2 消息对象扩展时间、状态与消息类型把聊天做成数据驱动之后消息对象不应该只包含type和content两个字段。真实场景下至少要加time时间戳和status状态位。time用来展示消息发送时间或者做按时间分组比如把 5 分钟内的消息归为一组显示时间头status用来标记机器人的回复状态比如loading时显示「正在输入…」成功改为success失败改为error前端根据状态显示不同的气泡样式。// 用户消息 { type: 0, content: 你好, time: Date.now(), status: success } // 机器人消息初始为 loading { type: 1, content: , time: Date.now(), status: loading }加入status字段后发送逻辑会变成先 push 一条用户消息和一条status: loading的机器人占位消息请求成功后用splice或重新赋值的方式把占位消息的content和status更新这样界面上会出现「对方正在输入」的效果请求失败则把气泡文案改为「网络开小差了」。这种交互模式在微信、钉钉的聊天界面里都很常见实现成本不高但对用户体验的提升很明显。模板里用v-if判断message.status loading时渲染一个 CSS 动画的点或者直接用「正在输入…」文本代替。4.3 摸清键盘遮挡问题固定底栏与滚动容器的协作聊到 H5 聊天页面输入框被键盘顶飞是绕不开的问题。原代码的.sendMessage用了position: fixed; bottom: 0在浏览器里看着没问题但在 iOS 的 WebView 里软键盘弹起时fixed元素会被键盘顶起或者因为visualViewport的缩放行为出现错位。Android 的 WebView 行为又不一样键盘弹起时window.innerHeight会变化而 iOS Safari 不变化。这意味着单纯靠 CSSbottom: 0不可靠需要配合 JavaScript 监听visualViewport的resize事件来调整底栏的位置。const viewport window.visualViewport; if (viewport) { viewport.addEventListener(resize, () { document.querySelector(.sendMessage).style.bottom window.innerHeight - viewport.height px; }); }这段代码的思路是visualViewport.height表示当前可视区域的高度键盘弹起时这个值会变小window.innerHeight - viewport.height就是键盘占据的高度把它赋给底栏的bottom底栏就会恰好悬浮在键盘上方。注意这段逻辑只在支持visualViewport的浏览器里执行现代 iOS Safari 和 Android Chrome 都支持老浏览器可以只用 CSS 的env(safe-area-inset-bottom)做兜底。另外聊天滚动容器的高度也要动态调整否则底栏上移后滚动区域底部被遮住最新消息看不全。4.4 完整改造后的 Vue 实例代码把所有改造点整合起来一个可运行的 Vue 2 版本实例大致如下new Vue({ el: #cont, data: { messages: [], inputMsg: , scrollTop: 0 }, methods: { mit() { const text this.inputMsg.trim(); if (!text) return; this.messages.push({ type: 0, content: text, time: Date.now(), status: success }); this.inputMsg ; this.messages.push({ type: 1, content: , time: Date.now(), status: loading }); this.scrollToBottom(); this.fetchReply(text).then((reply) { const last this.messages[this.messages.length - 1]; if (last last.status loading) { last.content reply; last.status success; } this.scrollToBottom(); }).catch(() { const last this.messages[this.messages.length - 1]; if (last last.status loading) { last.content 机器人不在线请稍后再试; last.status error; } this.scrollToBottom(); }); }, fetchReply(text) { return axios.get(/api/turing, { params: { info: text } }).then((res) res.data.text); }, scrollToBottom() { this.$nextTick(() { const container document.getElementById(scrollArea); if (container) container.scrollTop container.scrollHeight; }); } } });这里有一个和原代码完全不同的设计发送用户消息后立即 push 一条机器人占位消息请求返回后再更新这条占位消息的内容。这样做的好处是界面反馈即时用户能看到「机器人正在输入」而不是发送后毫无反应。注意last.content reply这种对象属性修改在 Vue 2 中同样可以被侦测到因为messages数组的push已经让 Vue 为这个新对象建立了响应式代理后续修改其属性会触发视图更新。这里不需要重新赋值整个数组比原代码的this.message list更精确。5. 移动端 H5 聊天页的输入体验优化与验证技巧5.1 用前端本地规则兜底不依赖机器人接口图灵接口不稳定或密钥过期时聊天页不能直接瘫掉。一个实用的兜底方案是内置一组本地规则请求失败时从规则表里匹配关键词返回预设回复。常见做法是维护一个fallbackRules数组元素包含pattern正则和reply字符串。fetchReply捕获异常后先走本地匹配匹配不到再返回默认文案。const fallbackRules [ { pattern: /你好|您好|hi|hello/i, reply: 你好我是测试机器人 }, { pattern: /名字|你是谁/i, reply: 我叫老五一个跑在 H5 里的测试机器人 }, { pattern: /天气/i, reply: 天气接口未接入试试问我点别的 } ]; function localReply(text) { for (const rule of fallbackRules) { if (rule.pattern.test(text)) return rule.reply; } return 这个问题我还没学会你换个说法试试; }关键词匹配虽然简单但在测试版阶段够用。判断逻辑是正则逐条尝试匹配用户输入命中就返回对应回复全部未命中则返回兜底句。注意pattern里的i标志表示忽略大小写能同时覆盖「你好」和「Hello」。这个兜底规则表放在前端只适合测试用途正式项目应该下沉到后端避免前端包体携带大量业务回复文本。5.2 在 Vue Devtools 里验证消息流把页面跑起来后验证重点不是「能不能聊」而是数据层的变化是否符合预期。打开 Vue Devtoolsvue.js devtools (v5)插件在 Chrome 扩展商店可以安装选中根组件观察messages数组发送一条消息后数组应该立即多出两个对象一个是type: 0的用户消息一个是type: 1且status: loading的机器人占位消息请求返回后第二个对象的content从空变为机器人文案status变为success。如果messages数组没有变化问题出在事件绑定或v-model如果数组变化但页面没变问题出在渲染层检查v-for的key和模板分支。验证界面层时打开浏览器手机模拟器分别测试三种情况空消息点击发送不应产生气泡输入正常文本后用户气泡靠右、机器人气泡靠左断网后点击发送机器人气泡显示兜底文案而不是长时间无响应。scrollToBottom是否生效可以这样测连续发送 20 条消息观察滚动条是否自动停在底部如果停在中间说明scrollTop赋值时机太早$nextTick不够用的话可以加一个setTimeout延迟 50ms。5.3 一个顺手好用的技巧Enter 键发送与 ShiftEnter 换行测试版页面的发送按钮点击逻辑没问题但桌面端调试时鼠标点按钮很别扭。给输入框补一个键盘事件就能顺手解决监听keydown事件按下 Enter 且不按 Shift 时触发mit()按 ShiftEnter 时正常换行。实现方式是在 input 的keydown.enter.prevent上做拦截但这会让 ShiftEnter 也触发发送所以要在事件处理函数里判断event.shiftKey。input idinputs typetext v-modelinputMsg keydown.enterhandleEnter placeholder按 Enter 发送ShiftEnter 换行 /handleEnter(event) { if (event.shiftKey) return; event.preventDefault(); this.mit(); }handleEnter的逻辑是event.shiftKey为真时直接return让输入框保持默认行为进行换行否则preventDefault阻止 Enter 的默认行为避免触发表单提交或换行转调mit()发送。注意这里用的是keydown.enter而不是keyup.enter因为keydown触发更早配合preventDefault才能有效阻止默认换行。如果用的是textarea做输入框这个技巧的必要性会更明显。这个交互细节虽然不起眼但在测试聊天功能时能省下不少鼠标操作算是一个高频使用的体验优化点。本文还有配套的精品资源点击获取
返回列表