ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

uni-app 全局悬浮按钮可拖动实现与跨端挂载方案

uni-app 全局悬浮按钮可拖动实现与跨端挂载方案 做移动端项目尤其是那种工具型、服务型的产品全局悬浮按钮几乎是绕不开的一环用户在任何页面都想一键唤起客服、一键回到顶部、一键打开快捷入口。可 uni-app 的页面模型跟传统 Web 不太一样它没有真正意义上的全局 DOM 层App.vue的模板在小程序端根本不参与渲染。很多人第一次做uniapp 全局悬浮按钮可拖动代码写完发现只能在当前页面晃悠切个页面按钮就消失了回头再改一轮才发现问题出在挂载方式上而不是拖动逻辑上。我前后在三个项目里做过这个东西从最早 H5 端硬挂document.body到后来的 easycom 组件复用再到 App 端用 subNVue 做真正脱离页面的浮层坑踩得比较全。这篇就把这件事从头到尾拆一遍怎么让按钮到处都在怎么让它拖得跟手怎么处理点击和拖动的冲突以及各端那些文档里不写但你一定会遇到的差异。代码可以直接抄参数我会把计算过程写清楚改几个数值就能落到你自己的项目里。不管你是刚接触 uni-app 的新手还是已经做过几个上线项目的开发者只要涉及悬浮层、拖动交互、跨端兼容这几块应该都能捞到点有用的东西。1. 先想清楚uniapp 里的全局到底全局到什么程度1.1 页面级悬浮和全局悬浮的真实差异很多人对全局的理解是写一次所有页面都能看见这个理解没错但 uni-app 的实现路径和 Web 完全不同。Web 里你只要拿到document.bodyappendChild一个position: fixed的节点它天然就是全局的页面路由怎么变都不影响。uni-app 编译到小程序之后每个页面是独立的渲染层页面之间不共享 DOMposition: fixed的参照系是当前页面的视口页面一销毁节点跟着销毁。所以 uni-app 里所谓的全局悬浮本质上只有两条路一是组件复用式的伪全局——把按钮封装成组件在每个需要的页面里都挂一份视觉上看起来它一直在二是脱离页面容器的真全局——App 端用 subNVue 原生子窗体H5 端用根组件承载让浮层活在页面之外。这两条路的成本、性能、可维护性差别很大选之前得先明确你的产品到底需要哪一种。我的判断标准很简单如果按钮只是大部分页面都要比如电商首页、分类、购物车、我的这四个 tab 页那组件复用完全够用维护成本也低如果按钮要在所有页面出现包括二级详情页、活动页、WebView 页而且切换时不能有任何闪烁或位置重置那就得考虑真全局方案。前面那种情况占八成后面那种通常是客服入口、调试面板这类强需求。1.2 三条技术路线怎么选把常见的做法列一下你对着选就行。方案适用端是否真全局实现成本主要限制easycom 组件 每页一行标签全端否低每个页面都要引入切页时状态重置App.vue 模板承载条件编译H5是中小程序端 App.vue 模板不渲染仅 H5 可用subNVue 原生子窗体App是高只能 App 端与 vue 页面通信要走事件mixin 全局事件驱动全端半中依然依赖页面存在页面栈深时容易错乱表格里最右边那列是重点。很多人一上来就想搞一套代码全端真全局实际上不现实因为小程序端的渲染机制决定了页面之外没有可插入的公共层自定义 tabBar 除外但它只能待在底部。我现在的做法是分层主体用 easycom 组件保证全端一致App 端和 H5 端各加一层条件编译做增强代码用#ifdef隔开互不影响。提示不要试图在App.vue的template里写悬浮按钮然后指望小程序端生效。小程序端的App.vue只是个逻辑入口模板部分会被编译丢弃你写多少都不会显示。这个坑我见过不止一个团队踩。还有一点值得说清楚真全局方案里浮层的位置状态是不是要跨页面保持比如用户把按钮拖到了屏幕右下角切到下一个页面它还应该在右下角而不是回到默认位置。组件复用方案默认做不到这一点因为每个页面是新的组件实例data重新初始化。要解决就得把位置存到全局比如uni.setStorageSync或者挂到getApp().globalData上在onShow里读回来。这个细节看着小但直接影响体验的一致性后面第 5 章我会给具体代码。2. 从零搭一个可拖动的悬浮按钮组件2.1 目录结构与 easycom 配置先建目录。我习惯把所有通用组件放在components下一个组件一个文件夹命名跟标签名保持一致这样 easycom 自动扫描能直接命中不用手写引入语句。components/ └── float-btn/ └── float-btn.vue如果你不想依赖自动扫描比如项目里组件很多扫描有性能顾虑可以在pages.json里显式声明{ easycom: { autoscan: true, custom: { ^float-btn$: /components/float-btn/float-btn.vue } } }配置完之后任何页面的模板里直接写float-btn /就能用不用import、不用components注册。这一步看着不起眼但它是伪全局方案能不能低成本的关键——如果每个页面都要写三行引入代码那推广到几十个页面就是灾难后面新人接手也容易漏。注意easycom 的custom规则是正则匹配标签名^float-btn$里的短横线不用转义但如果你组件名里有.之类的特殊字符就得处理。另外修改pages.json后要重新编译热更新有时候不生效别以为是规则写错了。2.2 模板结构与样式基准组件模板本身很简单一个view加可选的图标和文字。这里有个取舍用image还是用uni-icons之类的字体图标我倾向用image或者直接slot让调用方决定内容因为字体图标在小程序端的加载时机不好控制首屏可能出现短暂空白而悬浮按钮往往在首屏就要出现。template view v-ifvisible classfloat-btn :class{ is-dragging: dragging } :stylepositionStyle touchstarthandleTouchStart touchmove.stop.preventhandleTouchMove touchendhandleTouchEnd touchcancelhandleTouchEnd clickhandleClick image v-ificon :srcicon classfloat-btn__icon / text v-else classfloat-btn__text{{ text }}/text /view /template样式部分有三个细节必须处理不然体验会差一截。.float-btn { position: fixed; z-index: 999; display: flex; align-items: center; justify-content: center; border-radius: 50%; background-color: #2979ff; box-shadow: 0 4px 16px rgba(41, 121, 255, 0.4); transition: left 0.25s ease-out, top 0.25s ease-out; user-select: none; } .float-btn.is-dragging { transition: none; opacity: 0.9; } .float-btn__icon { width: 50%; height: 50%; }第一transition必须加但拖动过程中必须去掉。加它是为了松手后的吸附回弹有动画去掉它是因为拖动时如果还带过渡手指移动和按钮移动之间会有肉眼可见的延迟那种拖不动拖起来黏糊糊的感觉九成是这个原因。我在早期项目里被这个坑了很久一直以为是setData太慢最后发现是 CSS 过渡在捣乱。第二user-select: none是给 H5 端准备的。桌面浏览器或者部分安卓浏览器里按住拖动会触发文字选择选中一片蓝色背景非常难看。加这一行能压掉。第三z-index不要贪大。999通常够用写99999反而可能盖住一些必须显示的原生弹层比如小程序的授权弹窗。层级这事后面第 4 章会展开原生组件的层级规则和普通view完全不是一套。2.3 touch 事件驱动的拖动核心逻辑拖动这件事的原理其实特别朴素按下时记住手指位置和按钮位置移动时算出位移差把新位置写到按钮上。难的不是原理是各个端的坐标字段不统一、频繁赋值带来的性能问题以及边界和吸附的处理。先看数据结构和初始化。data() { return { visible: true, dragging: false, left: 0, top: 0, startX: 0, startY: 0, startLeft: 0, startTop: 0, moveDistance: 0, btnSize: 0, // 按钮尺寸px edgeGap: 0, // 距屏幕边缘的间距px windowWidth: 0, windowHeight: 0, safeBottom: 0, // 底部安全区高度px inited: false } }初始化放在mounted里。这里要算的东西不少我一个个说清楚。mounted() { const sys uni.getSystemInfoSync() this.windowWidth sys.windowWidth this.windowHeight sys.windowHeight // rpx 转 px这样后续所有计算都在 px 体系里不用来回换 this.btnSize uni.upx2px(this.size) this.edgeGap uni.upx2px(this.edgeGapRpx) // 底部安全区iPhone X 以上机型 home indicator 占的那块 if (sys.safeArea sys.safeArea.bottom) { this.safeBottom sys.screenHeight - sys.safeArea.bottom } // 默认位置按比例落在屏幕右下区域具体比例可配 this.left this.windowWidth * this.leftRatio - this.btnSize / 2 this.top this.windowHeight * this.topRatio - this.btnSize / 2 this.inited true }uni.upx2px这个 API 值得单独提一句。它是按 750 设计稿的基准把 rpx 换算成 px底层依赖当前设备的windowWidth。为什么不在样式里直接用 rpx非要转成 px因为拖动是运行时计算算出来的left、top是 px 值如果你样式里混着 rpx 和 px边界判断就会出偏差。统一到 px 体系最省心。代价是横竖屏切换时不会自动重算如果你的 App 支持横屏得在onResize里再跑一遍初始化。接下来是拖动三个事件。handleTouchStart(e) { if (!this.draggable) return const touch e.touches[0] this.startX touch.clientX this.startY touch.clientY this.startLeft this.left this.startTop this.top this.moveDistance 0 this.dragging true }, handleTouchMove(e) { if (!this.draggable || !this.dragging) return const touch e.touches[0] const dx touch.clientX - this.startX const dy touch.clientY - this.startY // 记录最大位移用来区分点击还是拖动 const dist Math.sqrt(dx * dx dy * dy) if (dist this.moveDistance) this.moveDistance dist let left this.startLeft dx let top this.startTop dy // 边界钳制 left Math.min(Math.max(left, this.edgeGap), this.windowWidth - this.btnSize - this.edgeGap) top Math.min(Math.max(top, this.edgeGap), this.windowHeight - this.btnSize - this.edgeGap - this.safeBottom) this.left left this.top top }, handleTouchEnd() { if (!this.dragging) return this.dragging false // 位移太小判定为点击不做吸附 if (this.moveDistance 6) return // 左右吸附按钮中心在屏幕左半边就贴左边否则贴右边 const centerX this.left this.btnSize / 2 if (centerX this.windowWidth / 2) { this.left this.edgeGap } else { this.left this.windowWidth - this.btnSize - this.edgeGap } this.$emit(dragend, { left: this.left, top: this.top }) }坐标字段这里有个兼容性的坑要说。小程序和 H5 端的 touch 对象里clientX、clientY、pageX、pageY都有但语义不一样clientX是相对于视口的位置pageX是相对于文档的位置页面滚动了之后这两个值会分叉。悬浮按钮是position: fixed参照系是视口所以必须用clientX/clientY。如果用pageX页面往下滚 300px你按下去按钮会瞬间跳走 300px。这个 bug 我调了大半天才定位到因为当时的页面很短没滚动的情况下两者数值一样测不出问题。App 端 nvue 页面的 touch 事件对象结构跟 vue 页面不同字段名有可能是pageX/pageY而且取坐标的方式要用e.touches[0]或者e.changedTouches[0]。如果你的组件要跑在 nvue 里建议加一层兜底const touch e.touches[0] || e.changedTouches[0] const x touch.clientX ! undefined ? touch.clientX : touch.pageX const y touch.clientY ! undefined ? touch.clientY : touch.pageY虽然啰嗦但跨端项目里这种兜底很值能省掉一轮真机调试。2.4 惯性、边界吸附与安全区计算吸附逻辑我在handleTouchEnd里写了最简版本只看左右两边。要不要做上下吸附看你产品形态。如果按钮会跟底部 tabBar 打架那给它加个只能待在右边缘的约束更省事——把left强制为右边缘值只允许纵向拖动。很多 App 的客服悬浮球就是这个形态用户拖到左边缘会自动弹回右边看着像有磁力其实是简单的一次赋值。安全区的计算前面提了sys.screenHeight - sys.safeArea.bottom得到的是底部不可用区域的高度。iPhone 14 这类机型大概是 34px 左右安卓大部分是 0。这个值不加的话按钮能被拖到 home indicator 下面用户根本点不到。同样顶部也要留一点余量尤其是小程序端右上角有胶囊按钮你拖上去会被盖住// 顶部留出状态栏 胶囊区域的高度 const topLimit sys.statusBarHeight 44statusBarHeight从getSystemInfoSync里直接拿44 是小程序胶囊按钮所在区域的经验高度这个数值在大部分机型上够用。严谨点可以用uni.getMenuButtonBoundingClientRect()拿到胶囊的真实位置来算但要注意这个 API 只在微信小程序端可用其他端调用会报错或者返回空记得包一层条件编译。关于惯性滑动我的建议是别做。悬浮按钮不是列表用户拖动它是为了把它挪到不碍事的位置一次性精准落位比甩一下飞出去更符合预期。而且惯性动画要自己维护速度衰减、边界碰撞代码量翻倍跨端表现还难统一。真要做用transition配合transform: translate3d做个小幅回弹就够了别上物理引擎那一套。另外补一个性能细节。touchmove的触发频率在高端机上能到 60-120 次每秒每次触发都赋值this.left、this.top在小程序端意味着每次都触发一次setData页面复杂的时候会明显卡顿。优化思路有两条一是用requestAnimationFrame节流把同一帧内的多次触发合并成一次赋值二是干脆别用data存位置改用transform直接操作节点样式。第二条在 H5 端可行小程序端受限于架构做不了所以老老实实节流更实际。handleTouchMove(e) { if (this.rafId) return this.rafId setTimeout(() { this.rafId null this.doMove(e) }, 16) }用setTimeout模拟 16ms 节流虽然不像真正的 rAF 那么精准但在小程序端足够用实测能把手感从发涩提升到基本跟手。3. 把组件变成全局的三种挂载方式3.1 每页一行标签的复用方案这是主力方案。配合 easycom每个页面模板末尾加一行float-btn clickonFloatClick /就完事。听着有点笨但它的好处特别实在状态隔离干净每个页面自己控制按钮的显示隐藏、自己的点击回调不会出现详情页的按钮触发了首页逻辑这种鬼问题。而且调试直观哪个页面有问题就看哪个页面。成本在于维护。项目有 60 个页面就得改 60 处。缓解办法是用mixin把公共逻辑抽出来页面里只留一行标签// mixins/float-btn.js export default { methods: { onFloatClick() { // 默认行为打开客服 uni.navigateTo({ url: /pages/service/service }) } } }页面里mixins: [floatBtnMixin]标签里绑定onFloatClick需要特殊处理的页面再单独覆盖这个方法。至于标签本身那行确实没法自动注入这是 uni-app 页面模型的限制。实在页面太多可以用脚本批量处理pages目录下的.vue文件把标签插到/template前面写完就删脚本比手动改快得多。还有个小问题切页面时按钮位置会重置到默认比例位置。如果你希望它记住上次拖到哪把位置存到全局// 拖动结束时 uni.setStorageSync(floatBtnPos, { left: this.left, top: this.top }) // 初始化时 const saved uni.getStorageSync(floatBtnPos) if (saved saved.left) { this.left saved.left this.top saved.top }用storage而不是globalData是因为 App 冷启动后globalData会清空storage能持久化。代价是用户卸载重装前的所有会话都共享这个位置如果屏幕旋转或者换了设备分辨率存下来的坐标可能越界所以读取之后要再跑一遍边界钳制。3.2 App 端 subNVue 全局浮层如果你的产品是 App 为主且按钮必须全场景常驻那 subNVue 是唯一正解。它是原生子窗体独立于 vue 页面渲染页面怎么跳转都不影响它而且因为是原生层性能比 vue 浮层好。配置写在pages.json里注意subNVues是挂在某个页面下的但它显示之后可以跨页面存在{ path: pages/index/index, style: { app-plus: { subNVues: [ { id: floatBtn, path: pages/float-btn/float-btn, type: popup, style: { position: absolute, width: 56px, height: 56px, left: 300px, top: 500px, background: transparent } } ] } } }调用的时候// #ifdef APP-PLUS const subNVue uni.getSubNVueById(floatBtn) subNVue.show(fade-in, 200) // #endifsubNVue里的页面单独写本质是一个 nvue 文件position用absolute不要用fixed因为它的容器就是那个原生窗体本身。拖动逻辑跟前面基本一样但坐标参照系是窗体内部处理边界的时候要拿窗体的尺寸而不是屏幕尺寸传参可以通过uni.$emit/uni.$on或者直接在subNVue的style里改left/topsubNVue.setStyle({ left: ${newLeft}px, top: ${newTop}px })这条路能跑通但成本不低多一个 nvue 文件要维护跟主页面通信只能走事件调试的时候日志分散在两个上下文里。我的建议是除非你的产品经理明确要求按钮必须在所有页面常驻且不能闪否则先用方案一别一上来就上 subNVue。3.3 H5 端条件编译挂载到 bodyH5 端有个讨巧的写法直接在App.vue的模板里写组件用条件编译把其他端排除掉。template view classapp-root !-- #ifdef H5 -- float-btn :left-ratio0.9 :top-ratio0.7 clickonFloatClick / !-- #endif -- /view /templateH5 端的App.vue模板会真实渲染而且它位于页面路由容器之外所以切路由的时候按钮不会销毁重建这就是真正意义上的全局。有两点要注意一是层级App.vue的容器在某些 uni-app 版本里会被页面容器覆盖如果发现按钮不见了往z-index上调或者把组件挂到body上二是样式作用域App.vue里的样式是全局的别在这里写跟页面同名的类名。如果想做得更彻底可以在 H5 端用Vue.extend动态挂载// #ifdef H5 import Vue from vue import FloatBtn from /components/float-btn/float-btn.vue const FloatBtnCtor Vue.extend(FloatBtn) const instance new FloatBtnCtor({ propsData: { leftRatio: 0.9 } }).$mount() document.body.appendChild(instance.$el) // #endif这样做的好处是完全脱离 uni-app 的组件树不受任何父级overflow、transform影响。缺点是propsData是静态的后续改配置要通过instance.$props或者实例上的方法用起来没那么顺手。我的做法是只把位置状态持久化和全局点击回调这两件事交给动态实例视觉内容还是用组件模板折中一下。4. 端上差异与高频坑实录4.1 点击和拖动的冲突判定这是最容易被忽略又最容易出问题的一环。touchstart→touchmove→touchend之后浏览器或者小程序还会补发一个click事件。如果用户只是想拖一下结果松手时顺带触发了点击页面就跳走了体验非常糟。解决办法就是前面代码里的moveDistance。在handleClick里做最后一道拦截handleClick() { if (this.moveDistance 6) return this.$emit(click) }阈值选 6px 是个经验值。太小了手指轻微抖动就被判定成拖动用户会觉得按钮点不动太大了用户拖了一小段又会被判定成点击。6px 在手机屏幕上差不多是半个字符的宽度实测下来误判率最低。如果你想更严谨可以结合时长判断位移小于 6px 且按住时间小于 300ms 才算点击。还有一个平台差异小程序的click事件触不发跟touchmove有没有被catch有关。我们在模板上写了touchmove.stop.prevent.stop编译到小程序端是catchtouchmove它会阻止事件冒泡但不会阻止click的补发。所以手动拦截这步不能省。提示组件内部的handleClick里用了this.$emit(click)这意味着父组件要监听自定义事件float-btn clickxxx /。如果你在父组件写的是click.native那就重复了会触发两次。Vue 2 和 Vue 3 在这一点上的行为也不一样Vue 3移除了.native修饰符。项目如果正在从 Vue 2 转 Vue 3这块要重点回归。4.2 页面滚动、tabbar 与原生组件遮挡拖动的时候页面跟着滚这个问题在安卓上特别明显。原因是touchmove默认会传递给滚动的父容器。模板上的.stop.prevent能解决大部分情况但如果按钮是放在scroll-view里面的scroll-view自己会监听滚动你得在按钮的touchmove上明确return false或者给scroll-view加:scroll-y!dragging拖动期间直接禁用滚动。小程序端还有一类问题来自原生组件。video、map、canvas这类组件的层级在早期是高于普通view的悬浮按钮飘到它们上面会被盖住拖都拖不动。新版基础库已经支持同层渲染了但如果你要兼容低版本就得用cover-view包一层或者干脆避开这些区域。这个坑在视频类、地图类应用里特别常见。底部 tabBar 的遮挡要单独说。原生的 tabBar不管小程序还是 App层级是最高的position: fixed; bottom: 20px的按钮会被它整块盖住。正确的做法是用windowHeight做边界uni.getSystemInfoSync()返回的windowHeight已经扣掉了原生 tabBar 的高度所以边界钳制里直接用this.windowHeight - this.btnSize - this.edgeGap就是安全的。但如果是自定义 tabBarwindowHeight就不准了得自己减去自定义 tabBar 的高度。再提一个热词里常见的场景弹出层打开时底部页面还能滚。这跟悬浮按钮关系不大但处理思路一致——给遮罩层加touchmove.stop.prevent在弹层存在的期间把page-meta的overflow锁住。小程序端还可以用page-container组件它自带滚动锁定。4.3 多端兼容速查表把前面零散的点整理成表方便你对着排查。现象高发端原因处理办法按钮切页消失小程序页面级渲染节点随页面销毁每页引入组件或用 subNVue拖动延迟、黏手全端拖动时 CSS transition 未关闭.is-dragging下置transition: none按下去按钮跳走全端用了pageX而非clientX坐标统一取clientX/clientY拖动卡顿小程序高频setData16ms 节流合并赋值按钮拖到屏幕外全端未做边界钳制用windowWidth/windowHeight钳制被底部栏盖住小程序/App原生 tabBar 层级最高用windowHeight做下边界被视频、地图盖住小程序低版本原生组件层级用cover-view或避开该区域松手触发页面跳转全端click补发未拦截moveDistance阈值判断文字被选中变蓝H5拖动触发文本选择user-select: none按钮跑到 home 条下面iOS未处理底部安全区减去safeArea差值这张表我在项目里贴过 README新人上手的时候省了大量沟通成本。你也可以把它当自测清单每做完一个页面就过一遍。5. 参数化设计与业务扩展5.1 把配置项和事件暴露出去组件要能被复用就得把可变的部分做成props。我给的这个组件暴露了这些参数props: { visible: { type: Boolean, default: true }, icon: { type: String, default: }, text: { type: String, default: }, size: { type: Number, default: 56 }, // rpx edgeGapRpx: { type: Number, default: 20 }, // rpx距边缘间距 draggable: { type: Boolean, default: true }, leftRatio: { type: Number, default: 0.9 }, // 初始横坐标比例 topRatio: { type: Number, default: 0.7 }, // 初始纵坐标比例 snapEdge: { type: Boolean, default: true } // 松手后是否吸附到边缘 }这里有两个设计决定值得解释。第一尺寸和间距用rpx传、内部转px因为调用方是按设计稿标注的设计稿是 rpx 体系但内部计算必须是 px。第二初始位置用比例而不是绝对像素这样在不同分辨率的机型上按钮落点相对一致。如果你传绝对像素在 1080p 手机上位置刚好在 720p 手机上就偏到屏幕外了。事件方面至少暴露三个click点击、dragend拖动结束带上最终坐标、dragstart可选。dragend带上坐标特别有用调用方可以拿它做位置持久化或者判断用户是不是把按钮拖到了某个感应区域里。5.2 几个真实业务场景的接法场景一一键回顶。最经典的用法按钮点一下uni.pageScrollTo({ scrollTop: 0, duration: 300 })。注意如果是scroll-view容器要用scroll-view自己的scroll-top属性控制pageScrollTo对它无效。还有个细节按钮应该在页面滚动超过一屏后才出现监听onPageScroll记录scrollTop超过阈值再visible true。不过onPageScroll在部分端上触发频率很高建议在回调里只比较阈值不要做复杂计算。场景二全局客服入口。这种通常是真全局需求因为用户在任何页面都可能想找客服。做法是把按钮的点击事件统一指向同一个方法方法内部判断当前页面栈决定是navigateTo到客服页还是直接打开一个弹层。如果客服是第三方 SDK 的悬浮球那还要处理跟自家按钮的层级冲突我的做法是自家按钮在第三方 SDK 初始化完成后隐藏避免两个球打架。场景三调试面板入口。开发阶段挂一个悬浮按钮点开弹出请求日志、缓存清理、环境切换这些功能。这种就完全不需要跨页面常驻每页引一个组件、用process.env.NODE_ENV development控制显示就够了。生产包里记得彻底去掉或者用#ifdef条件编译保证代码不被打包进去。场景四多标签快捷操作。按钮点开之后展开三到四个小按钮呈扇形或者竖排。这种要注意展开后的元素也要能拖动或者至少不超出屏幕。我的做法是展开时把主按钮先吸附到边缘然后子按钮朝屏幕内侧展开这样永远不会超出边界。展开动画用transform: scale加opacity比改宽高流畅因为前者能走 GPU 合成。最后再补一句关于状态同步的。悬浮按钮经常需要跟页面状态联动比如购物车里有商品时按钮上显示角标。如果用的是每页引入组件的方案角标数量得从全局状态读Vuex 或者 Pinia 都行但要注意在小程序端页面onShow时会重新读一次否则从购物车页返回首页角标数量可能还是旧的。这种看起来不起眼的同步问题是上线之后用户反馈最多的一类。我个人在这些项目里最大的体会是悬浮按钮这东西技术难度真不高难的是别想着一套代码打天下。先把每页复用组件这个基本盘做扎实把拖动的手感、边界、点击判定这三件事调到没有明显瑕疵再根据实际产品需求决定要不要上 subNVue 或者 H5 动态挂载。反过来一上来就追求全端真全局最后往往是四端表现都不一样改一处崩三处。如果你现在正卡在某个具体的端上先看第 4 章那张表八成能从里面找到对应原因。
返回列表