做者:Horace
声明:本项目只做为学习使用,欢迎你们一块儿交流~
ps:新人小白一枚,若有错误,欢迎指出~
过年有大把的时光,为什么一直宅在家里不出家门看着电脑,这到底是道德的沦丧仍是人性的泯灭...这一切都还得从一只蝙蝠提及... html
咳咳,好了不皮了,言归正传。微信推出的小程序可谓是轻量又强大,因此最近我也开始了小程序的学习,学了挺多也看了不少文档,但总以为本身没学到什么,感受很迷茫。正所谓实践出真知,因此我选择了从高仿别人的小程序开始,选来选去最后选择了京东优选这个小程序(绝对不是由于它的界面清爽!)。vue
废话很少说,咱先来搞一波图片看看,点这里查看更多图片 git
这个项目我使用的是普通的开发,把全部的数据都放在了json-server中模拟。
可能不少人会以为很奇怪,但这是由于我发现easy mock的网站常常打不开请求失败很是的不方便,因此我暂时没有选择mock数据,后期有时间我会把数据挪到easy mock上。github
|-jd_recommend 项目名 |-api 模拟数据接口 |-db.json 模拟的数据 |-assets 资源文件 |-icons 图标资源 |-images 图片资源 |-components 组件模块 |-navigationBar 自定义导航栏 |-toast 自定义toast |-stepper 有赞vant步进器组件 |-... 其余小程序所需组件 |-pages 项目页面 |-about 关于页面 |-account 个人订单页面 |-afterMarket 售后类型页面 |-appointment 个人预定页面 |-buy 填写订单信息页面 |-commentDetail 评论详情页面 |-discount 优惠券页面 |-explore 发现页面 |-feedback 反馈页面 |-fix 售后页面 |-goodsDetail 值得买优惠详情页面 |-index 首页 |-jd 京东商品详情页面 |-login 登陆页面 |-orderDetail 订单详情页面 |-seller 客服页面 |-service 退换/售后页面 |-shopCart 购物车页面 |-user 我的中心页面 |-style 公共样式 |-comment.wxss 评论区样式 |-goodsCard.wxss 商品卡片样式 |-nav.wxss 导航栏样式 |-orderCard.wxss 订单卡片样式 |-popright.wxss 筛选框样式 |-popup.wxss 上拉菜单样式 |-utils 公共模块 |-util.js promise封装接口 app.js 全局js app.json 全局json配置 app.wxss 全局wxss
大部分人写小程序确定要涉及修改navigationBar的title,微信小程序开发内置了这个组件,能够直接在app.json中配置。可是,自带的navigationBar的样子是固定的,你确定见过长成下面这样的navigationBar:数据库
相比平时常见的navigationBar,它左上角多了一个返回主页的按钮,这对于有多级页面的小程序来讲是很是必要的,否则访问的层级太深用户不知道怎么返回主页。然而,小程序开发自带并无这个样子的,好在能够自定义,接下来咱们就来自定义一个。json
首先,咱们构建一下页面的结构:小程序
<!-- components/navigationBar/index.wxml --> <view class='nav-wrap' style='height: {{height*2 + 20}}px;'> <!-- 导航栏 中间的标题 --> <view class='nav-title' style='line-height: {{height*2 + 44}}px;'>{{navbarData.title}}</view> <view style='display: flex; justify-content: space-around;flex-direction: column'> <!-- 导航栏 左上角的返回按钮和home按钮 --> <!-- 其中wx:if='{{navbarData.showCapsule}}' 是控制左上角按钮的显示隐藏,首页不显示 --> <view class='nav-capsule' style='height: {{height*2 + 44}}px;' wx:if='{{navbarData.showCapsule}}'> <!-- 左上角的返回按钮,wx:if='{{!share}}'空制返回按钮显示 --> <view bindtap='_navback'> <image src='../../assets/icons/back.png' mode='aspectFill' class='back-pre'></image> </view> <view class='navbar-v-line' wx:if='{{!share}}'></view> <view bindtap='_backhome'> <image src='../../assets/icons/back_home.png' mode='aspectFill' class='back-home'></image> </view> </view> </view> </view>
这就是一个很普通的页面结构,值得注意的是,它的高度是根据获取的设备的高度来肯定的。
接下来又到了切图仔上线的时候了(误):微信小程序
/* components/navigationBar/index.wxss */ /* 顶部要固定定位 标题要居中 自定义按钮和标题要和右边微信原生的胶囊上下对齐 */ .nav-wrap { position: fixed; width: 100%; top: 0; background: #fff; color: #000; z-index: 9999999; border-bottom: 1rpx solid #EFEFF4; } /* 标题要居中 */ .nav-title { position: absolute; text-align: center; max-width: 400rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; font-size: 36rpx; color: #2c2b2b; /* font-weight: 600; */ } .nav-capsule { display: flex; align-items: center; margin-left: 30rpx; width: 140rpx; justify-content: space-between; height: 100%; } .navbar-v-line { width: 1px; height: 32rpx; background-color: #e5e5e5; } .back-pre, .back-home { width: 32rpx; height: 36rpx; margin-top: 4rpx; padding: 10rpx; } .nav-capsule .back-home { width: 36rpx; height: 40rpx; margin-top: 3rpx; }
// components/navigationBar/index.js const app = getApp() Component({ properties: { navbarData: { //navbarData 由父页面传递的数据,变量名字自命名 type: Object, value: {}, // observer: function (newVal, oldVal) { } } }, data: { height: '', //默认值 默认显示左上角 navbarData: { showCapsule: 1 } }, attached: function () { // 定义导航栏的高度 方便对齐 this.setData({ height: app.globalData.height }) }, methods: { // 返回上一页面 _navback() { wx.navigateBack() }, //返回到首页 _backhome() { wx.switchTab({ url: '/pages/index/index', }) } } })
京东优选小程序这里的两个按钮都是返回首页,我在开发的时候以为不对劲,因此我改过来了。
在这里还去取了一下全局定义的变量,也就是获取的设备顶部窗口的高度(不一样设备窗口高度不同,根据这个来设置自定义导航栏的高度),在app.js中要定义一下:api
app.js App({ onLaunch: function () { ...... wx.getSystemInfo({ success: (res) => { this.globalData.height = res.statusBarHeight } }) }, globalData: { ... height: 0 } })
记得自定组件的时候必定要在json中写成自定义组件promise
// components/navigationBar/index.json { "component": true }
接下来就是调用该组件了
<navigationBar navbar-data='{{navbarData}}'></navigationBar>
别忘了在要引用页面的json中引入该组件
"usingComponents": { "navigationBar": "../../components/navigationBar/index" }
Toast一样也是小程序开发已经作好给你用的了,虽然它能够支持替换里面的图标,可是你会发现很鸡肋的一点是,若是你想显示两行文字你就没办法作到了。我在开发过程当中也搜索过相关的实现方法,找到了大部分是说在要换行的文字后背加上rn就能实现了,可是我本身亲测无效,因此实在忍不住也本身作了一个。
<!-- components/toast/index.wxml --> <!-- 距离顶部高度由业务须要动态肯定 --> <view class='mask' hidden="{{hide}}" style='top: {{toastData.top}}'> <image class="image" src='../../assets/icons/{{toastData.icon}}.png' mode='aspectFit'></image> <view class="info"> <view class='info1' wx:if="{{toastData.info1 != ''}}">{{toastData.info1}}</view> <view class="info2" wx:if="{{toastData.info2 != ''}}">{{toastData.info2}}</view> </view> </view>
/* components/toast/index.wxss */ .mask { width: 440rpx; height: auto; border-radius: 20rpx; position: fixed; left: 155rpx; z-index: 1000; background: rgba(0, 0, 0, 0.6); text-align: center; padding-bottom: 30rpx; } .image { z-index: 1000; width: 80rpx; height: 80rpx; padding-top: 30rpx; padding-bottom: 20rpx; } .info1, .info2 { color: #ffffff; font-size: 32rpx; } .info { display: flex; flex-direction: column; justify-content: center; align-items: center; }
// components/toast/index.js Component({ properties: { //定义组件属性 toastData: { //用来显示提示信息 type: Object, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型) value: { icon: 'success' } // 属性初始值(可选),若是未指定则会根据类型选择一个 }, }, data: { hide: true }, methods: { showToast: function () { let that = this; that.setData({ hide: false }); }, hideToast: function (e) { let that = this; setTimeout(function () { that.setData({ hide: true }); }, 2000); } } })
这里给组件定义了两个方法,是用来显示和隐藏Toast的。这里要注意一下,调用给自定义组件定义方法要先在页面上获取该组件
<toast id="toast" toast-data="{{toastData}}"></toast>
Page({ data: { toastData: { // toast须要的参数 icon: "success", info1: "加入购物车成功", top: "50%" } }, onReady() { this.toast = this.selectComponent("#toast"); } })
而后在须要触发Toast的事件中写上这两句:
this.toast.showToast() this.toast.hideToast()
所谓导航,也是很常见了,就是根据选择栏目的不一样,显示不一样的类别内容。例如:
功能要求:
实现它的功能并不难,直接sroll-view往上怼。我的以为,京东优选在这里有一点不足的地方就是,若是点击了偏右侧的导航栏目的话,导航条不会跟着右移显示后面的项目,可能它的开发者有不同的想法吧。
<view class="navigator"> <scroll-view scroll-x="true" class="nav" scroll-left="{{navScrollLeft}}" scroll-with-animation="{{true}}"> <block wx:for="{{navData}}" wx:for-index="id" wx:for-item="navItem" wx:key="id"> <view class="nav-item {{currentTab == id?'active':''}}" data-name="{{navItem.name}}" data-current="{{id}}" bindtap="switchNav"> {{navItem.name}} </view> </block> </scroll-view> </view>
经过js能够实现动态的填放数据,这里设置的current就是当前选择的栏目,能够根据这个改变样式等。
switchNav(e) { const cur = e.currentTarget.dataset.current; // Number let currData = [] // console.log(cur.toString()); if (cur === 0) { currData = this.data.goods } else { this.data.goods.forEach(val => { if (val.category === cur.toString()) { currData.push(val) } }) } this.setData({ currentTab: cur, category: cur, currData }); }
若是是要实现点击以后自动向点击的方法滑出显示更多的内容,能够经过动态改变navScrollLeft的值去实现,这里我就不细说了,不过我在实现的时候仍是花了一番功夫,实现的不是很好因此就没有放在代码里,若是你之后想作出这种效果的导航栏建议去网上搜一搜demo看懂了以后借过来用一用,毕竟传说程序猿最高的境界是复制粘贴,狗头(误)
这两个比较类似,只是拉出的位置不同,这里我就举一个筛选框的例子,咱们先看看它长啥样:
咱们先看看结构,这里我省略了中间的一些内容:
<!-- 点击筛选弹出的选择菜单 --> <view class="float {{isRuleTrue?'isRuleShow':'isRuleHide'}}"> <view class="animation-element" animation="{{animation}}"> ...中间本身放的具体内容... <!-- 底部的两个按钮 --> <view class='bottom'> <view class="animation-reset" bindtap="reset">重置</view> <view class="animation-button" bindtap="success">肯定</view> </view> </view> </view>
/* 筛选弹框 */ /* 弹框的布局 */ .isRuleShow { display: block; } .isRuleHide { display: none; } .float { height: 100%; width: 100%; position: fixed; z-index: 999; top: 0; left: 0; /* 弹出后背景的颜色 */ background-color: rgba(0, 0, 0, 0.5); padding-left: 30rpx; padding-left: 30rpx; /* margin-top:80rpx; */ } .animation-element { width: 600rpx; height: 100%; padding-left: 30rpx; padding-right: 30rpx; background-color: #ffffff; border: 1px solid #f3f0f0; position: absolute; right: -550rpx; box-sizing: border-box; } .bottom { width: 600rpx; height: 110rpx; font-size: 32rpx; padding-top: 55rpx; position: absolute; bottom: 0; left: 0; right: 0; display: flex; } .animation-reset { width: 50%; height: 100%; line-height: 50%; text-align: center; padding-top: 55rpx; border-top: 1px solid #EFEFF4; } .animation-button { width: 50%; height: 100%; line-height: 50%; color: #fff; text-align: center; background-color: #ED7358; padding-top: 55rpx; }
重点是它的显示和隐藏事件,须要用到animation,若是有不熟悉animation,能够去参考一些资料,或者是官方文档。一样,我也去掉了我实现其余业务的一些内容。
showSelect() { // 显示选择菜单 this.setData({ isRuleTrue: true }) // 左偏移245 step表示一个动做的开始 this.animation.translate(-245, 0).step() this.setData({ animation: this.animation.export() }) }, success: function () { // 关闭选择菜单 this.setData({ isRuleTrue: false, selected: true }) this.animation.translate(0, 0).step() this.setData({ animation: this.animation.export() }) },
要实现这样的效果并不困难,须要本身思路清晰,不能被绕进去了。实现加入购物车并不难,细节是购物车图标右上角的数字要根据加入购物车的数量进行动态的改变,还要注意若是是同一件商品就不须要添加新的,只须要修改原来的数量。
在这里我使用的是小程序的wx.setStorage()实现的:
<view class='bottom'> <view class="animation-reset" bindtap="addCart">加入购物车</view> <view class="animation-button" bindtap="buy">当即购买</view> </view>
addCart() { // 加入购物车 this.setData({ toastData: { // toast须要的参数 icon: "success", info1: "加入购物车成功", top: "50%" } }) this.toast.showToast() this.toast.hideToast() this.hideModal() // 真正实现添加购物车的部分 let cartData = wx.getStorageSync('cart') || []; let count = 0 cartData.map(val => { if (val.title === this.data.currData[0].title && val.type === this.data.choose_value) { val.num += this.data.num count++ // 标记是否有找到相同的商品 } }) if (count === 0) { // 没找到 添加新的商品信息进购物车 let data = { id: this.data.currData[0]._id, title: this.data.currData[0].title, weight: "0.78kg", type: this.data.choose_value, num: this.data.num, price: this.data.currData[0].plain_price, img: this.data.currData[0].thumb, discount: 20, select: true // 是否选中,方便后续计算总价 } cartData.push(data) } // 刷新购物车图标上的数量 let allNum = 0 cartData.forEach(val => { allNum += val.num }); this.setData({ allNum }) wx.setStorage({ key: 'cart', data: cartData }) },
这里你能够根据本身的开发来决定方式,若是你使用的是云开发的话,能够选择把数据存进云数据库里。
这也是一个老生常谈的功能,当你滑到页面比较后的位置的时候须要快速回顶。这里要记住,用swiper实现。首先是在页面上撸一个回到顶部的图标出来:
<!-- 滑动一段距离后显示返回顶部的按钮 --> <scroll-view class="bigWrap" scroll-y="true" scroll-top="{{scrollTop}}" bindscroll="scroll" style="position: absolute; left: 0; top:0; bottom: 0; right: -999rpx;"> <view class="goTop" bindtap="goTop" wx:if="{{&& floorstatus}}"> <image class="icon_goTop" src="../../assets/icons/back_to_top.png"></image> </view> </scroll-view>
{{scrollTop}}用来表示滑动的时候距离顶部的位置。它的样式也很简单,使用固定定位把它定在屏幕上,这里必定要注意页面的层级,否则它可能会被其余组件给遮挡掉!
/* 回到顶部 */ .goTop { position: fixed; bottom: 200rpx; right: 20rpx; width: 65rpx; height: 65rpx; border: 1px solid #DDDDDD; border-radius: 50%; background-color: #fff; text-align: center; } .icon_goTop { width: 40rpx; height: 40rpx; padding-top: 12rpx; padding-left: 2rpx; }
goTop(e) { // 回到顶部 this.setData({ scrollTop: 0 }) }
你确定也注意到了,当滑到了必定距离的时候它才显示出来,这就要靠swiper绑定的滚动事件了:
scroll(e) { // 滚动事件 // 容器滚动时将此时的滚动距离赋值给 this.data.scrollTop let floorstatus = false if (e.detail.scrollTop > 300) { floorstatus = true } this.setData({ floorstatus }) }
功能大体先说这么一点,可能在大牛看起来都是些很容易不起眼的功能,可是对应我这个初学者来讲仍是有点困难的,但愿若是有大牛看了个人一些功能的实现以后我不会被骂死。
作太小程序开发或者是vue等开发的人必定听过事件冒泡这个名词:子元素的事件触发了父元素的事件,例如点击事件。我就是那个幸运鹅,我在开发的时候就遇到了这个状况。
在购物车中点击商品能够跳转商品详情,可是我一开始把跳转事件绑定在了每一个商品卡片上,这样就致使了点击修改商品数量的时候修改了数字可是也会直接跳转商品详情,好比下面这样...
这就很不友好了,用户体验不好,关于事件冒泡,微信小程序的解决方法是把bindtap替换成catchtap,这样能够阻止子元素事件向上冒泡。
然而巧的是,我就是那个最幸运的鹅,步进器我用的是有赞Vant Weapp组件库里的,我搜索了不少资料都没有找到有效的解决方案,差点就放弃使用组件库了,好在最后发现京东优选小程序购物车绑定的跳转事件是在商品的图片和标题上。
这一点仍是比较重要的,因此你们在开发的时候必定要考虑事件的冒泡,这也是我把它放在最后来写的缘由。
最后,我想说的是小程序开发真的不容易,开发一个好的小程序更是须要考虑性能和用户体验的方方面面。当我以为本身第一个小程序差很少要完工的时候真的要跳起来唱joyful了(误)。做为一个程序猿真的不容易,难怪是个容易掉发的群体。但好在愿意分享技术的人不少,在此次开发的过程当中我也查阅了不少的资料、社区和文档。小程序的学习我也不会停下脚步,这个项目还有很是多作的很差的地方,我发出来也是但愿你们和我进行交流分享,后期我也会继续完善优化这个小程序项目。但愿个人做品能够对那些初学小程序的人有所帮助。
最后附上个人github项目地址:https://github.com/tearill/jd... 若是你以为这个项目还不错或者是对你有所帮助的话欢迎star,你点亮的每个star将都是我前进的动力!