ARTICLE DETAIL

资讯详情

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

uniapp仿小米有品小程序开发实战:从骨架搭建到性能优化

uniapp仿小米有品小程序开发实战:从骨架搭建到性能优化 简介使用uni-app框架开发的仿小米有品微信小程序项目完整呈现电商小程序的典型业务闭环适合具备Vue基础并希望进阶跨端开发的开发者。项目还原了小米有品的商品陈列、分类搜索、购物车结算、订单管理、用户登录与微信支付等核心模块代码中涉及组件化开发、Vue语法与Vuex状态管理、网络请求封装、微信支付SDK接入、懒加载与分包优化等关键知识点。压缩包约882KB代码结构清晰可直接导入HBuilderX运行预览也可作为课程设计、毕业设计或前端实战训练的参考模板。目前已有336人学习下载借助该资源能够深入理解跨端开发中的多端适配差异、小程序性能优化手段以及从开发到上线的完整流程对正在学习uni-app或准备构建电商小程序的开发者很有帮助。1. 用 uniapp 仿小米有品为什么先做微信小程序端很多人把 uniapp 当成一套代码通吃三端的万能药但真动手仿过小米有品这类商城的人会有另一个结论先让微信小程序端跑顺比拿到 H5 和 App 的 Demo 重要得多。小程序天然没有 DOM数据更新要序列化走 setData很多在 H5 上毫无感觉的写法——一次性渲染 100 张商品图、频繁改深层对象、直接用 window——到小程序上就是卡顿、白屏或状态丢失。我会按 HBuilderX 建项目的常规流程把仿小米有品需要先定的页面结构、manifest 配置、条件编译、顶部导航栏承接以及商品列表和购物车的几个硬交互拆开讲最后落到抓包、分享和打包上。适合能写 Vue 但还没被小程序渲染机制坑过的开发者按步骤走能省几天的排查时间。2. 小程序端骨架搭建manifest 配置、tabBar 与分包策略2.1 tabBar 先于页面为什么首页、分类、购物车要被定义为四个固定入口打开一个新 uniapp 项目常见动作是直接写首页。但目标是仿小米有品的话建议先定义 tabBar。微信小程序的 tabBar 直接决定底部导航pages.json 里配置后HBuilderX 运行时才会在编译产物里生成正确的 app.json。漏掉 tabBar 会导致真机预览底部导航不出现或者点击切换时页面栈异常。电商项目里首页、分类、购物车、我的这四个一级页面应当全部放进 tabBar且不能放进分包否则微信会直接报错。{ pages: [ { path: pages/home/home, style: { navigationBarTitleText: 小米有品 } }, { path: pages/category/category, style: { navigationBarTitleText: 分类 } }, { path: pages/cart/cart, style: { navigationBarTitleText: 购物车 } }, { path: pages/user/user, style: { navigationBarTitleText: 我的 } } ], tabBar: { color: #999999, selectedColor: #ff6700, backgroundColor: #ffffff, list: [ { pagePath: pages/home/home, text: 首页 }, { pagePath: pages/category/category, text: 分类 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/user/user, text: 我的 } ] } }pagePath必须和pages里注册的路径完全一致text是底部按钮文案。想要图标时还要在每一项里加iconPath和selectedIconPath图片推荐 81px * 81px路径不能以/开头必须用相对路径。tabBar 的list数量不能超过 5 个顺序即显示顺序。一个很容易踩的坑tabBar 页面一旦被放进 subPackages微信开发者工具会直接编译失败报错信息通常只会说“未找到入口页面”定位时先查 tabBar 页面是否误入分包。2.2 manifest.json 的 mp-weixin 段appid、lazyCodeLoading 与权限声明pages.json 管页面manifest.json 管应用。HBuilderX 的可视化界面最终会落到 manifest.json 源码里微信小程序端相关配置集中在mp-weixin字段。仿商城项目里我常用的基线配置如下{ mp-weixin: { appid: wx1234567890abcdef, setting: { urlCheck: false }, usingComponents: true, lazyCodeLoading: requiredComponents, permission: { scope.userLocation: { desc: 用于获取您所在城市的门店推荐 } } } }appid换成微信公众平台申请的小程序 AppID。测试号能跑起来但涉及支付、地理位置、订阅消息时必须用正式 AppID。setting.urlCheck设为 false 后微信开发者工具不会校验请求域名开发阶段联调本地接口很省事但这份配置只影响开发工具行为不是线上豁免。发布前还是要在 mp 后台把 request 的合法域名全部配好否则真机上请求会被直接拦截。lazyCodeLoading: requiredComponents是微信小程序按需注入组件的开关首页信息流场景下能明显减小首包体积。permission里放的不是申请权限代码而是调用位置接口时的用途说明文案如果项目没用到uni.getLocation这段可以删掉留着反而会让微信审核问一次。2.3 分包策略把详情页和搜索页从主包里摘出去仿小米有品的典型流量路径是用户从分享卡片直接进入商品详情页所以详情页不应该占据主包体积。分包原则很直白tabBar 四页和公共组件留在主包二级页面全部进分包。pages.json 里增加subPackages: [ { root: pages/goods, pages: [ { path: detail/detail, style: { navigationBarTitleText: 商品详情 } }, { path: search/search, style: { navigationBarTitleText: 搜索 } } ] } ], preloadRule: { pages/category/category: { network: all, packages: [pages/goods] } }root是分包目录pages内部的路径相对root写。preloadRule里key是触发预加载的页面路径packages是目标分包名这里选择在分类页预加载商品分包因为商城用户大概率在分类页停留后点进详情。network参数可填all或wifi后者只在 wifi 下预加载能帮用户省流量。预加载不是免费的分类页启动时会多一部分下载开销。如果从首页直接进详情的路径更长可以把preloadRule的 key 改成首页或者去掉预加载等 Canvas 分析页面热力之后再做取舍。分包项设置值作用rootpages/goods决定分包目录名影响构建产物和预加载引用networkall / wifi预加载的网络条件wifi 更保守preloadRule页面路径对应 packages控制何时下载分包需要和用户路径匹配3. 仿有品页面的条件编译与顶部导航栏兼容处理3.1 条件编译一套代码里区分微信小程序、H5 与 Appuniapp 的跨端方案不是运行时抽象而是编译期预处理。写// #ifdef MP-WEIXIN这种注释时构建到对应平台只会保留匹配的代码块。这也是为什么 uniapp 推荐条件编译而不是运行时判断运行时 if 会把这几个平台的逻辑全部打进去条件编译则直接裁剪产物。典型场景是自定义导航栏小程序端需要给胶囊按钮让位H5 端不需要!-- #ifdef MP-WEIXIN -- view classcustom-nav小程序端自定义导航/view !-- #endif -- !-- #ifndef MP-WEIXIN -- view classh5-navH5 端不需要避开胶囊/view !-- #endif --#ifdef表示仅在指定平台编译#ifndef表示非指定平台编译。MP-WEIXIN是微信小程序的条件值H5、APP-PLUS同理。JS 里也支持// #ifdef MP-WEIXIN const isWeixin true; // #endif老项目从 uniapp vue2 转到 vue3 时条件编译注释可以原样保留真正要动的是生命周期写法vue2 里直接写在export default {}的onLoad在 vue3 组合式里要从dcloudio/uni-app导入onLoad再使用。条件编译对样式同样生效所以自定义导航栏的高度适配可以全部放进style里的条件块避免 js 里写一堆平台判断。3.2 胶囊按钮与导航栏高度为什么不能写死 44px仿小米有品时如果需要自定义顶部导航栏最忌讳把高度写死。微信小程序的胶囊按钮在不同机型上位置不同iPhone X 系列和普通安卓机的状态栏高度不同胶囊到顶部的距离也不同。常规做法是启动时读取胶囊信息动态计算导航栏高度// 获取自定义导航栏需要的高度 const getNavBarInfo () { const systemInfo uni.getSystemInfoSync(); const menuRect uni.getMenuButtonBoundingClientRect?.(); const statusBarHeight systemInfo.statusBarHeight || 20; let navBarHeight 44; if (menuRect) { navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height; } return { statusBarHeight, navBarHeight, menuHeight: menuRect?.height || 32, menuRight: menuRect ? systemInfo.windowWidth - menuRect.left : 0 }; };uni.getMenuButtonBoundingClientRect()只在微信小程序端存在所以用可选链调用H5 会跳过。statusBarHeight是状态栏高度navBarHeight是导航栏应占用的总高menuRight用于计算自定义按钮要离多远才不会被胶囊遮挡。这段逻辑建议放在公共模块里所有自定义导航页面共用一份结果不要每个页面各自调用同步 API否则会拖慢启动。返回字段含义典型用途statusBarHeight状态栏高度单位 px自定义导航栏的 padding-topnavBarHeight导航栏整体高度单位 px导航栏容器高度样式menuHeight胶囊按钮高度导航栏右侧按钮与胶囊对齐menuRight胶囊右边界到屏幕右侧距离右侧 icon 避让胶囊按钮如果不想自己写pages.json 里页面 style 可以直接设置navigationStyle: custom禁用原生导航栏。禁用后整个页面内容会顶到屏幕最上方必须用上面的高度结果做撑开否则搜索框会被状态栏盖住。3.3 页面生命周期与定时器清理onUnload 和 onHide 都要管商城首页的轮播图、倒计时和特价抢购区的自动切换总要用到setInterval。在 H5 里页面离开后定时器可能被浏览器兜底但在小程序里页面离开后如果定时器没清掉setData 和请求会在后台空转JavaScript 引擎长时间被占用时会触发内存告警。正确做法是页面onUnload里清理onLoad() { this.timer setInterval(() { this.setData({ currentBanner: this.currentBanner 1 }); }, 3000); }, onUnload() { if (this.timer) { clearInterval(this.timer); this.timer null; } }onLoad和onUnload是页面生命周期不是组件生命周期。如果倒计时写在自定义组件里要使用beforeUnmount或unmounted。微信小程序的页面栈里页面被覆盖时不会触发unload只会触发onHide所以高频率的轮播最好在onHide里清掉定时器onShow再重建。这样既能避免后台耗电也能保证重新回到页面时倒计时没过期或走样。4. 商品列表、购物车与长按拖拽交互的落地实现4.1 商品卡片列表image 的 mode 与 lazy-load 缺一不可仿小米有品的首页本质上是一个商品信息流。小程序端不是所有 H5 特性都有尤其 CSSbackground-image在部分安卓机型上会出现图片不显示、切页面才刷新的问题所以商品图一律使用image组件view classgoods-item v-foritem in goodsList :keyitem.id image classgoods-img :srcitem.image modeaspectFill lazy-load clickgoDetail(item.id) / text classgoods-name{{ item.name }}/text text classgoods-price{{ item.price }}/text /viewmodeaspectFill会在保持宽高比的前提下填满图片区域超出部分裁掉适合商品图统一尺寸展示。lazy-load让图片进入视口前不发起请求列表快速滚动时能少加载大量不可见区域图片。需要注意image组件默认有约 300px 的占位宽高外层容器没有确定宽度时会出现图片全部堆在左上角的情况。样式里应显式设置宽高并加display: block.goods-img { width: 100%; height: 360rpx; display: block; }商品价格不要用两个 text 拼接小数点电商场景后续要接入价格展示规范时文案、字号、颜色会频繁调整单独结构更容易维护。4.2 长按拖拽排序用 touch 事件而不是小程序里没有的拖放 API在购物车或自定义分类管理里做长按拖拽排序小程序没有 HTML5 的draggable需要靠touchstart、touchmove、touchend自己实现。第一次实现时最容易犯的错误是每个touchmove都 setData 整个数组这个事件的触发频率远超 setData 能承受的刷新率手指一抖页面就卡。我实践的方案是拖动过程中只更新被拖拽项的transform手指松开后一次性重排数据onTouchStart(e) { const index e.currentTarget.dataset.index; this.dragIndex index; this.startY e.touches[0].clientY; this.dragOffset 0; if (!this.rowHeightPx) { const query uni.createSelectorQuery().in(this); query.select(.goods-item).boundingClientRect(rect { this.rowHeightPx rect.height this.itemGapPx; }).exec(); } this.draging true; }, onTouchMove(e) { if (!this.draging) return; const diff e.touches[0].clientY - this.startY; this.dragOffset diff; this.setData({ dragTransform: translateY(${diff}px) }); }, onTouchEnd() { if (!this.draging) return; const targetIndex Math.min( Math.max(this.dragIndex Math.round(this.dragOffset / (this.rowHeightPx || 60)), 0), this.list.length - 1 ); if (targetIndex ! this.dragIndex) { this.moveItem(this.dragIndex, targetIndex); } this.draging false; this.setData({ dragTransform: }); }this.rowHeightPx首次触摸时通过createSelectorQuery读取真实高度避免按 750 设计稿的 rpx 盲目换算itemGapPx是列表项间距测量时要补上。Math.round让手指移动超过半行时才触发一次目标索引变化。moveItem内部通过splice重排数组并 setData整个拖拽过程中只执行一次 setData所以不会频繁触发渲染。被拖拽项的样式要绑定dragTransform例如:style{ transform: isDraging ? dragTransform : }。注意微信小程序里 transform 的字符串必须带单位。如果要做完整的“按住再拖动”touchstart里还要加一个约 350ms 的延时定时器模拟长按判定或者直接使用longpress事件触发进入拖拽模式。和movable-area搭配时被拖拽项会脱离原有列表插入位置的占位符要单独维护复杂度会明显上升普通排序列表用上面三件套就够了。4.3 购物车全选与价格汇总状态设计比组件更关键购物车看起来是复选框加合计但全选状态很容易写乱。我常用的结构是把选中项统一维护成selectedIds数组页面数据和 UI 状态分离data() { return { cartList: [], // 后端返回的购物车项 selectedIds: [] // 只保存商品 id }; }, computed: { allSelected() { return this.cartList.length 0 this.selectedIds.length this.cartList.length; }, totalPrice() { return this.cartList .filter(item this.selectedIds.includes(item.id)) .reduce((sum, item) sum item.price * item.count, 0); } }没有用item.selected这种散落在每条数据里的布尔值而是独立 id 数组。全选、反选、删除后重算都只需要操作一个数组不会出现某条数据更新后视图不同步的问题。computed 在 vue3 选项式写法里可以直接使用vue2 项目里修改数组后要注意响应式依赖用this.cartList [...this.cartList]触发更新。删除购物车项后还要从selectedIds里过滤掉对应 id否则判断全选时会出现长度不匹配。4.4 分类页左右联动scroll-view 的 scroll-into-view 和监听时机分类页是仿小米有品的重要入口左侧一级分类右侧是可滚动的多个分组。右侧滚动时左侧高亮跟随我用scroll-into-view做向右定位的交互比自己计算滚动距离稳定scroll-view classright-wrap scroll-y :scroll-into-viewscrollIntoId scroll-with-animation scrollonRightScroll view v-forgroup in groups :idgroup- group.id :keygroup.id !-- 分组内容 -- /view /scroll-viewscroll-into-view的值必须匹配滚动区域内某个子元素的id且 id 不能以数字开头。右侧滚动时监听scroll在onRightScroll里通过uni.createSelectorQuery().in(this).selectAll(.group-item)读取每个分组相对滚动容器的位置找到当前处于视口中间位置的分组再更新左侧高亮索引。一个容易踩的细节scroll-into-view在预设值没有变化时不会触发滚动连续点击同一个分类时需要先把值清空再在$nextTick里重新赋值。5. 上线前的性能排查与打包上架技巧5.1 抓包与真机调试先看 Network 面板再改代码请求发不出去时很多人第一反应是改代码重跑但更高效的顺序是先打开微信开发者工具的“真机调试”在 Network 面板里看请求头、响应和耗时。这个面板比 Charles 容易上手HTTPS 场景也省去证书配置。排查顺序固定为先看请求是否发出再看状态码最后看返回值。urlCheck只影响开发者工具真机上必须确认域名已经加到小程序后台的 request 合法域名。5.2 自定义分享好友path 参数决定回流量能不能跟踪仿小米有品的商品页要支持分享到微信好友和群聊。默认右上角菜单不会出现分享必须显式声明onShareAppMessageonShareAppMessage() { return { title: this.goods.name, path: /pages/goods/detail/detail?id this.goods.id, imageUrl: this.goods.image }; }path必须以/开头并带商品 id。用户点开分享卡片后小程序直接打开详情页onLoad(options)里能拿到options.id再请求详情接口。分享图的imageUrl需要是可访问的 CDN 地址不能是本地相对路径否则分享卡片会没有图。5.3 轮播图黑边、扫码进入与版本号递增首页轮播图偶尔出现黑边不是 uniapp 的问题是swiper-item里图片尺寸和轮播容器不一致。把图片固定为width: 100%; height: 300rpx;配合modeaspectFill可以解决。搜索入口需要扫商品码时直接用uni.scanCode成功回调里拿result字符串做商品码匹配再跳详情。发布前记得在 manifest.json 里递增版本号微信后台对相同版本号的上传会拒绝同时小程序后台的版本号要和应用内读取到的uni.getSystemInfoSync().version区分开前者是发布迭代用的后者是运行环境 SDK 版本。本文还有配套的精品资源点击获取
返回列表