前两周想必大家都看到了京东发布拍拍二手交易平台的 新闻 ,「拍拍二手」 APP 也正式上线.与此同时 我们 也紧锣密鼓的进行着「拍拍二手」微信小程序的开发.整个过程痛并快乐着,体会着采坑的痛苦,和跳出坑之后的喜悦.
项目介绍
「拍拍二手」主要有三大业务:回收,优品和个人闲置交易.京东 "将以平台化的运营思路,整合回收,检测,再加工,销售等逆向供应链资源,做品质二手.",而基于微信有庞大的社交关系链,利于产品的推广,直接面对用户,助力自身业务等优点.公司于是决定推出微信小程序版的「拍拍二手」.
微信小程序的的主要页面有:
拍拍首页
拍拍群首页
一键转卖列表页
商品发布页
商品详情页
订单详情页
我的(发布,卖出,收藏)
我们打开小程序,看一段操作的视频:
Video Player 下载文件
可谓是麻雀虽小五脏俱全.
项目预研
在此项目之前,我们有过几个小程序的经验,所以项目启动时,我们便采用 "前端驱动业务" 的方式,推动业务童鞋提前申请小程序依赖的资质,如:小程序账号,名称备案,支付资质,腾讯地图日访问量等等.
同时,区别于以往我们做过的小程序,本次项目将拍拍二手 C2C 的整体流程移植到小程序平台,并实现以微信群为载体的交易体验.在需求评审过程中,我们大致遇到以下几个问题,并进行了技术预研.预研结果我们将在技术难点部分展开解说.
技术架构
在现有小程序的框架基础上,我们丰富了自定义组件,新增了基础类库,引入了 SASS,Eslint 在小程序里的应用.这里简单抛出几点:
因受限于小程序包大小的限制(开发时包大小限制为 2M);我们对静态图片资源也做了优化,并将大部分图标放在了 CDN,小程序直接访问网络资源.
SASS 的使用,既是沿用我们现有的 PC,M 端的重构方式(大家都已熟稔于心),也大大提升了小程序开发的效率.
ESLint 的应用,采用我们设置的代码规范,为我们的代码输出做了把关.
此外,鉴于小程序路由跳转层级的限制(最初是 5 级),我们细化了每个流程的路由跳转方案.
技术难点
以下,我们将重点解析在项目中遇到的疑难问题和解决方案.我们从小程序包大小,兼容性问题,现有组件缺陷,这些天我们遇到的坑,我们开发的小程序组件,为业务提供备选方案等角度一一举例解析.
小程序包大小限制
为了达到代码不超过 2M,为了小而全,我们在开发过程中就必须去思考如何减少代码量,同时提高用户体验.如何提高小程序的代码复用率,同时还要降低它们的耦合.
首先,我们采用前后端分离的方式,前后端约定接口文档;也放弃了传统前端出静态页再套页面,模板开发的方式,前端直接依据接口规范模拟数据后重构 + 开发;
第二,在开发前我们做了很多的探讨,从几十张设计稿中归纳可以通用的模块,编写了很多通用组件;在数据处理方面编写了很多公共方法,提炼到 util 类中;
第三,我们将静态资源雪碧图化,tiny 后,发布到 CDN 上,小程序里依赖图标的元素直接引用网络资源.
小程序兼容问题
小程序在兼容性方面有一些已知问题,在文档中已明确指出,但最近新出的 iPhone X,文档尚不全面,我们这次也对该机型做了测试,并整理出我们遇到的一些兼容性问题,希望可以对大家有所帮助.
首先给大家看一张图片, 它存在两个问题,下面我一一介绍它们的处理方式:
1,border-radius 设定后在 iphoneX 中元素的边框显示不全
遇到这个问题的时候只需要把 rpx 改成 px 即可.其实不只是小程序有这类问题,在 M 端开发过程中如果使用 rem 这种单位都难以避免会造成这样.
2,iphoneX 中 view 设定 padding-left 在手机中有偏差
<view class="com-lab ">
<span>运费</span>
分类
</view>
<view class="sel-box">
</view>
这段代码很简单,我们看到运费有个 span 标签包裹,分类没有,而在写 wxss 的时候 我们这样写的
在 iphoneX 中就会产生如上图的偏差,修改方式也简单
.com-lab span{
padding-left:30rpx;
}
.sel-box{
padding-left:30rpx
}
去掉了 span 标签的 padding 而改到了外层的 view 中这样偏差就没有了,可第一种写法在浏览器中也是对的,为什么在 ios 手机中有这种偏差呢,我觉得可能是编译时候小程序的语法造成的,所以在做页面重构的时候尽量减少这些差别.
.com-lab{ padding-left:30rpx; }
.sel-box{ padding-left:30rpx }
3,iphoneX 适配微信底部操作区问题
大家知道 iPhoneX 手机打开刘海模式后,有安全区的概念,而我们需要把展示内容都放在安全区域内,所以需要对底部的黑色 Home Indicatorzuo 做处理,否则会遮挡住文字.首先是在 JS 代码中区分一下机型
然后在 wxss 中做一下样式的处理
wx.getSystemInfo({
success: function(res) {
if(res.model.toLowerCase().indexOf('iphone x') != -1) {
me.globalData.isIpx = true;
}
}
});
.fix-ipx-tabbar-bottom {
bottom: 66rpx;
这样的处理方式并没有什么难度,关键在于我们要知道 iphoneX 手机存在着这样的一个问题,那么未来国产手机的会不会有新的造型,我们同样可以用这样的方法去处理,简单有效的才是好的.
}
.fix-ipx-tabbar-bottom::after {
content: '';
position: fixed;
bottom: 0rpx;
height: 66rpx;
width: 100%;
background: #FFF;
}
4,wx.showModal 点击遮罩层触发确定,ios 中提示文字后面有一块白色背景
因为模态窗口是小程序的 api,暂无修改样式入口,我们直接复用了我们编写的 ModalDialog 组件,替换了该方法.
小程序现有组件缺陷
1,文本输入在 ios 下的兼容问题
Video Player
下载文件
文本输入常用的标签无非就是 input,textarea,当我们使用这两个标签做一些文本编辑时在 ios 下遇到了 3 个问题,它们分别是:
当页面有遮罩层时,无法遮盖 textarea 的文字内容.
在 ios 系统下,修改 textarea,或者 input 里面的文本内容,如果在文本中修改,光标会跑到最后面.
在 ios 系统下 textarea 会增加一个 padding,而我们怎么怎么用过样式控制都不能去掉这个 padding.
我们拿商品描述为例,它使用的文本输入标签是 textarea, 下面是一段 wxml 代码:
<view class="des-msg">
<span>描述</span>
问题 1:我们的解决方案是当有遮罩层产生是增加一个名为 shows 的 class,使这个标签隐藏起来,而不是消失.如果我们使用 wx:if="{{}}" 这样的方式会删除掉这个标签,如果在修改 textarea 内容时没有同步更新 postData.charactersDesc 当在产生这个标签时候里面的内容时之前生成的.
<textarea bindinput="charactersDesc"
class="{{desshow == 1 ? 'shows' : 'hidden'}} {{postData.devicesType == 2 ? 'iosText' : 'andText'}}"
name="charactersDesc"
maxlength="1001"
placeholder="描述一下商品吧"
value="{{postData.charactersDesc}}" />
</view>
写到这里有的人肯定会想为什么我们不在修改内容过程中同步更新 postData.charactersDesc 呢?这个是因为问题 2 的描述,这样会产生一个 bug 在 ios 系统里面.所以我们是隐藏而不是删除这个标签.
问题 2:我们需要把用户输入的内容记录下来,记录的内容时存储到了 charactersDesc,textarea 的 value 也是用的 charactersDesc,这样就造成了这个 bug, 而我在 textarea 里面绑定的事件是 bindinput 而不是 bindblur,是不是想如果用 bindblur 就没有问题了.
理想是美好的,现实是残酷的,ios 系统很不友好的给我们带来了这个麻烦,当我们在真机测试时候发现在小键盘输入时候 textarea 明明没有失去焦点,可控制台 console.log 不停的打印.也就是说每次输入都会触发 bindblur,看到这里我们内心是凌乱的.关于这个问题的解决我是这样处理的在 data 里面新建了一个 tempCharactersDesc 用来寄存你修改的内容已做他用.例如标签重新渲染.
问题 3:这个问题我们只能通过判断机型通过 {{postData.devicesType == 2 ?'iosText':'andText'} 来选择不同的 class.
2,页面快速点击可以重复触发
//终端数据类型
wx.getSystemInfo({
success: function(res) {
let types = 0;
if (res.system.split(' ')[0] == "iOS") {
types = 2;
}
if (res.system.split(' ')[0] == "Android") {
types = 1;
}
$that.setData({
['postData.devicesType']: types
})
}
})
Video Player
下载文件
描述: 小程序在页面间的跳转会有延迟,这就给了用户有快速点击两次的机会,如果不加以处理这太可怕了.想想你会同时打开两次同一个页面,它不仅给用户带来了不好的体验,也给了不是可以无限增加的路由更多卡死的机会,和通过路由判断 route 来源的函数带来了不必要的隐患.
通过 app.js 里面的 App() 注册一个一个全局的函数,然后在涉及到触发跳转的地方调用这个方法,就可以阻止重复点击触发了,下面是具体的处理方法
调用方法:
globalLastTapTime:0,
preventMoreTap:function(e){
var globaTime = this.globalLastTapTime;
var time = e.timeStamp;
if(Math.abs(time-globaTime) < 500 && globaTime != 0) {
this.globalLastTapTime = time;
return true;
} else{
this.globalLastTapTime = time;
return false;
}
}
3,页面间重复跳转几次之后锁死
let app = getApp();
Page({
xxx:function(e){
if(app.preventMoreTap(e)) {
return ;
}
//跳转
}
})
描述:发布商品这个页面,在拍拍二手里面算是一个中部流程的模块,上下游页面的跳转很频繁,甚至内部的分类也是跳转到一个新的页面.而且每个页面间的跳转我们都需要传递一系列的信息.显而易见按照官方文档我们会选择 navigateTo ,redirecTo 这两种方式.
使用 navigateTo 做页面跳转,只能跳转 10 次,第 11 次就会没有反应.而用 redirecTo 页面,当点击左上角触发回退按钮的时候,返回的页面不再是发布页面了,是其他的页面.
首先我们举个场景:当我们跳转使用 navigateTo, 由发布页 跳转 分类页 ,分类页选择一个分类 跳转回发布页,连续重复几次发现页面不动了.这是因为 navigateTo 跳转回把当前页面的信息加入到路由中,然后再跳转页面,把跳转的页面也放到了路由中,这个时候使用 getCurrentPages() 函数,我们可以得到一个数组,数组长度为 2.当这个长度变成 5 的时候页面就不能跳转了.
显然这样是不可以的.如果使用 redirecTo 这个方法是可以解决跳转卡死的问题,但是如果这时候点击页面左上角的返回,我们发现它并没有像我们期待的一样返回到商品发布页面,而是返回到了商品发布的前一个页面.
如果使用 navigateBack 这个方法,我们发现不能够在页面的跳转中传参数,但显然这是一个好的思路,我们接下来只要解决传参的问题就可以了,小程序参数有 3 中思路可以传递:
通过 navigateTo 或 redirecTo,在 url 里面传递
把变动的参数放到缓存中,然后更新缓存.这种方法显然不好,缓存中会有多个参数.
通过 getCurrentPages() 获取一个数组对象取上个页面的序列然后使用 setData() 方法
综上所述第 3 种思路传递参数是最好的.这样就实现了两个页面之间的来回跳转,点击左上的返回也能够从分类回到商品发布页面.值得注意的是使用第 3 中方法我们需要确定 pages[pages.length - 2];
var pages = getCurrentPages();
var prevPage = pages[pages.length - 2];
prevPage.setData({
classId: id,
classifyName2: className,
classTags: classtags
})
wx.navigateBack()
4,批量上传图片服务请求次数少于真实添加图片的个数
当我写到这个问题的时候,心情是复杂的,关于图片这块的处理,小程序给我们提供了 chooseImage,previewImage,getImageInfo 可以让我们选择图片,预览图片,对于上传同样有一个方法 uploadFile.首先举一个单图片上传的例子:
是不是感觉很简单.这么简单的代码怎么会有坑呢?往往涉及到图片上传的时候我们是多张图片的上传,上传过程中还需要有显示等待上传,上传失败,成功了还要把上传的图片回显.
wx.chooseImage({
count:1,
sizeType : ['compressed'],
success : function(res) {
let tempFilePaths = res.tempFilePaths;
wx.uploadFile({
url : xxx,
filePath:tempFilePaths[0] ,
name: 'xzInputFile',
formData: {
'user': 'test'
},
success: obj.success,
fail:obj.fail
})
})
批量上传我们想到的是把需要上传的图片用 for 循环进行上传:
写到这里是有问题的,我们使用 for 循环,uploadFile 可能会在 0.001ms 内访问服务器,造成循环 5 次,而真正访问服务器的次数少于 5 次的情况.我们对这段代码进行改造加入一个 setTimeout 延时函数,可以有效的避免快速请求服务器.
wx.chooseImage({
count:12,
sizeType : ['compressed'],
success : function(res) {
let tempFilePaths = res.tempFilePaths;
for(let i = 0,index ; src = tempFilePaths[i]){
wx.uploadFile({
url : xxx,
filePath:tempFilePaths[0] ,
name: 'xxx',
formData: {
'user': 'test'
},
success: obj.success,
fail:obj.fail
})
}
})
之后我们要处理的仅仅是按照序列把服务返回的信息更新到 data 里面,如果成功了就把等待上传替换成上传的图片,如果失败,就换成上传失败的图片,还可以通过这种情况设置重新上传图片, 现在图片上传的功能完成了.
wx.chooseImage({
count:12,
sizeType : ['compressed'],
success : function(res) {
let tempFilePaths = res.tempFilePaths;
for(let i = 0,index ; src = tempFilePaths[i]){
setTimeout(function(){
wx.uploadFile({
url : xxx,
filePath:tempFilePaths[0] ,
name: 'xxx',
formData: {
'user': 'test'
},
success: obj.success,
fail:obj.fail
})
},1000)
}
})
这些天我们遇到的坑
1, 图片上传总是失败网络不通
当我们所有的组件封装完毕,预览版没有问题而在预发版中发现图片总是出现上传失败的问题,这大多是 uploadFile 合法域名中没有添加上传图片的合法域名.如果遇到上传或者请求数据不通的情况,首先要检查一下我们的域名.
2, range 数据未加载完 picker 绑定事件
我希望去实现如上图所示滑动选择,微信小程序很贴心的给我们封装了 picker 组件.
<label>快递公司:</label>
<picker bindchange="bindPickerChange" value="{{index}}" range-key="logisticsName" range="{{logisticsArray}}" >
<view class="picker">
Range 属性的类型为 Array 或 Object Array,默认值是 [].Range-key 属性的类型为 String ,当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容. Value 属性的类型为 Number ,默认值是 0.Value 的值表示选择了 range 中的第几个(根据索引值).bindchange 用来对 picker 进行事件绑定,value 改变时触发 change 事件, event.detail = {value: value}.
<span>{{logisticsArray[index].logisticsName}}</span>
</view>
</picker>
现在看上去一切正常,由于设计稿有默认值 "请选择快递公司".很简单的思路,我们设置一个初始数组.然后再查询快递公司接口返回数据后进行拼接就可以了.
眼尖的你有没有发现什么问题?以为一切如期进行时,测试同学给我截了下面这个图.在接口数据没有返回时,去对 picker 进行 bindChange.就会只有一个请选择快递公司,其他的都没有.也就是用户操作必须在数据返回之后,这就取决于接口返回的速度.
data: { logisticsArray: [{
logisticsCode: "",
logisticsName: "请选择快递公司"
}]
}
var _self = this;
wx.request({ url: 'getLogisticsArray',
//仅为示例,并非真实的接口地址
data: {},
header: { 'content-type': 'application/json'
},
success: function(res) {
if (res.success) {
var logisticsCompany = _self.data.logisticsArray.concat(res.data); _self.setData({ logisticsArray: logisticsCompany
})
}
}
})
按照以往的处理方法,我们可能会在数据返回回来之后再进行一个 render 方法.让 dom 进行更新,但现在用户已经在操作界面了,显然这样不合理.所以思路就是必须让接口返回数据之后,才允许用户操作.
但是,傲娇的用户就不.那我也傲娇一次,我不显示看他操作啥.确定思路之后,分析一下.原本有初始 logisticsArray , length 为 1.数据返回之后,length > 1 .从这个方向改,这是就需要和 wxml 文件进行配合了.
实现起来很简单,主要是一个惯性思维的小转变.既解决了问题,同时又保障了用户体验.
<view wx:if = "{{logisticsArray.length > 1}}">
<picker></picker>
</view>
<view wx:else>
<image src="loading.png0"></image>
</view>
3,onReachBottom 与 onPullDownRefresh 同时执行
Video Player
下载文件
列表页,执行 onPullDownRefresh(下拉刷新)时触发了分页所用到的 onReachBottom(页面上拉触底事件处理函数),产生冲突.而我们可以通过增加一个参数去解决这个冲突
4,组件 open-data 格式问题
onReachBottom: function () {
// 到页面底部时,请求列表
if (!this.data.noMoreData && !this.data.isPullDown) {
this.setData({
currentPage: ++this.data.currentPage
});
this.getCollectList(this.data.currentPage);
}
}
这个严格说不算是组件缺陷,更应该是文档缺陷.
5,下拉刷新三个白点的默认样式不展示
//错误的写法
<open-data type="groupName" open-gid="xxxxxx"></open-data>
//正确的写法
<open-data type="groupName" open-gid="xxxxxx"/>
由于页面背景色也是白的,就导致看不到那三个点了.第一种方法是修改背景色,但是对当前样式的影响比较大;采用的是第二种方法,在已经添加下拉刷新页面对应的 json 文件中添加 "backgroundTextStyle": "dark",就能看到三个白色的点了.
我们开发的小程序组件
项目过程中我们开发了很多自定义组件,例如:警告弹窗,搜索栏,底部状态栏,tab 菜单,计算器,带确定取消的弹窗,我们以下面这个组件为例
Toast 和 ModalDailog 组件
小程序提供的 showToast,showModalDialog 的方法,因为设计风格问题,不能满足我们的需求,且它们只支持少数字符的展示(在 ipx 兼容测试时,我们还发现了文字白色背景的问题),所以我们一直采用自己封装的组件.
组件的创建和使用如下.
引用这个模板
<template name="confirm">
<view class="jdc-confirm">
<view class="jdc-confirm__content">
.
.
.
</view>
</view>
</template>
在 JS 里面进行控制
<import src="../template/template.wxml" />
<template is="toast" wx:if="{{toastShow}}" data="{{...toastData}}"></template>
我们通过简单模板构建了一个可复用的弹窗,从而解决了小程序原生弹窗的问题.
data: {
confirmData: {
visible: false,
title: '',
message: 'xxxx',
leftTxt: 'xxx',
rightTxt: 'xxx'
}
},
submit:function(){
......
},
cancel:function(){
......
}
}
为业务提供备选方案
落地页 - 唤起 app 的实现方式
在小程序里唤起 APP,从唤起的实现协议来看,小程序不支持,小程序目前只支持 https,不支持其他自定义协议,所以唤起 app 的 scheme 方式不疾而终.
当然我们可以跟业务说,这个小程序无法实现,再见!但是我们是技术,寻找解决方案才是终极目的.如果不能唤起 APP,也可以尝试把 APP 的链接暴露吧?但小程序不支持外链,所以我们的方案,就是提供给用户落地页的二维码,提示用户保存并扫码下载.
这是一个不算高明也有风险的方案,但目前可以解决落地页唤起 APP 的方式.
未来小程序开发探索
对小程序未来开发的一些构想
1. 开发工具的整合
在本次开发中,我们已逐步引用了 SASS,ESlint 等工具来辅助开发,未来我们会整合更多的工具,例如使用 css-sprite 整合雪碧图实现图片处理,以提升我们的开发效率.
2. 实现一套适用自己的 UI 及组件
我们会将更多公共组件和方法进行提取,并完成适用自己公司风格的 UI 和组件,应用于更多未来的小程序中.
当然,要做的事情还很多,我们会继续努力,发现更多有趣的实现~
终版感悟
贾慧斌:只有经历了才会懂.
s s: 百尺竿头,更进一步,再'进'已嵌套 10 层.
上善若水:实践出真知吧.
fishsif:希望微信开发者工具越来越好..
hanyuxinting:继续在小程序的路上一路高歌一路前行~~
小学生:写小程序,玩小程序.探索不一样的产品体验.
林如风:痛并快乐着,假如再给我一次机会,我会更好.
一路荆棘遍布,蓦然回首,已是花开两旁.相信再次开发小程序的项目会比较轻松,总之不要因为小程序是在微信中运行就会觉得兼容性很好,恰恰相反,因为小程序诞生到现在时间才有短短的一年,所以还有很多的不足,我们在使用小程序给我们提供的组件时一定要注意这些组件下方的 tip 提示.看完这些,对于微信小程序你还有什么疑问呢?如果有问题欢迎留言,我们一起探讨!
JDC 前端 - 小程序开发小组(林如风,hanyuxinting,fishsif,上善若水,小学生,s s,贾慧斌)联合编辑.
来源: https://juejin.im/entry/5a549e636fb9a01cbf383874