前两周想必大家都看到了京东发布拍拍二手交易平台的新闻,「拍拍二手」APP也正式上线。与此同时我们也紧锣密鼓的进行着「拍拍二手」微信小程序的开发。整个过程痛并快乐着,体会着采坑的痛苦,和跳出坑之后的喜悦。
项目介绍
「拍拍二手」主要有三大业务:回收、优品和个人闲置交易。京东“将以平台化的运营思路,整合回收、检测、再加工、销售等逆向供应链资源,做品质二手。”,而基于微信有庞大的社交关系链,利于产品的推广,直接面对用户,助力自身业务等优点。公司于是决定推出微信小程序版的「拍拍二手」。
微信小程序的的主要页面有:
- 拍拍首页
- 拍拍群首页
- 一键转卖列表页
- 商品发布页
- 商品详情页
- 订单详情页
- 我的(发布、卖出、收藏)
我们打开小程序,看一段操作的视频:
可谓是麻雀虽小五脏俱全。
项目预研
在此项目之前,我们有过几个小程序的经验,所以项目启动时,我们便采用“前端驱动业务”的方式,推动业务童鞋提前申请小程序依赖的资质,如:小程序账号、名称备案、支付资质、腾讯地图日访问量等等。
同时,区别于以往我们做过的小程序,本次项目将拍拍二手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 的时候 我们这样写的
com-lab span{
padding-left:30rpx;
}
.sel-box{
padding-left:30rpx
}
在 iphoneX 中就会产生如上图的偏差,修改方式也简单
com-lab{ padding-left:30rpx; }
.sel-box{ padding-left:30rpx }
去掉了 span 标签的 padding 而改到了外层的 view 中这样偏差就没有了,可第一种写法在浏览器中也是对的,为什么在 ios 手机中有这种偏差呢,我觉得可能是编译时候小程序的语法造成的,所以在做页面重构的时候尽量减少这些差别。
3、iphoneX 适配微信底部操作区问题
大家知道 iPhoneX 手机打开刘海模式后,有安全区的概念,而我们需要把展示内容都放在安全区域内,所以需要对底部的黑色 Home Indicatorzuo 做处理,否则会遮挡住文字。首先是在JS代码中区分一下机型
wx.getSystemInfo({
success: function(res) {
if(res.model.toLowerCase().indexOf('iphone x') != -1) {
me.globalData.isIpx = true;
}
}
});
然后在wxss中做一下样式的处理
fix-ipx-tabbar-bottom {
bottom: 66rpx;
}
.fix-ipx-tabbar-bottom::after {
content: '';
position: fixed;
bottom: 0rpx;
height: 66rpx;
width: 100%;
background: #FFF;
}
这样的处理方式并没有什么难度,关键在于我们要知道 iphoneX 手机存在着这样的一个问题,那么未来国产手机的会不会有新的造型,我们同样可以用这样的方法去处理,简单有效的才是好的。
4、wx.showModal点击遮罩层触发确定,ios 中提示文字后面有一块白色背景
因为模态窗口是小程序的api,暂无修改样式入口,我们直接复用了我们编写的 ModalDialog 组件,替换了该方法。
小程序现有组件缺陷
1、文本输入在ios下的兼容问题
文本输入常用的标签无非就是 input、textarea,当我们使用这两个标签做一些文本编辑时在 ios 下遇到了3个问题,它们分别是:
- 当页面有遮罩层时,无法遮盖 textarea 的文字内容。
- 在 ios 系统下,修改 textarea、或者 input 里面的文本内容,如果在文本中修改,光标会跑到最后面。
- 在 ios 系统下 textarea 会增加一个 padding,而我们怎么怎么用过样式控制都不能去掉这个 padding。
我们拿商品描述为例,它使用的文本输入标签是 textarea,下面是一段 wxml 代码:
<view class="des-msg">
<span>描述</span>
<textarea bindinput="charactersDesc"
class="{{desshow == 1 ? 'shows' : 'hidden'}} {{postData.devicesType == 2 ? 'iosText' : 'andText'}}"
name="charactersDesc"
maxlength="1001"
placeholder="描述一下商品吧"
value="{{postData.charactersDesc}}" />
</view>
问题1:我们的解决方案是当有遮罩层产生是增加一个名为 shows 的 class,使这个标签隐藏起来,而不是消失。如果我们使用 wx:if=“{{}}” 这样的方式会删除掉这个标签,如果在修改 textarea 内容时没有同步更新 postData.charactersDesc 当在产生这个标签时候里面的内容时之前生成的。
写到这里有的人肯定会想为什么我们不在修改内容过程中同步更新 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。
//终端数据类型
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
})
}
})
2、页面快速点击可以重复触发
描述:小程序在页面间的跳转会有延迟,这就给了用户有快速点击两次的机会,如果不加以处理这太可怕了。想想你会同时打开两次同一个页面,它不仅给用户带来了不好的体验,也给了不是可以无限增加的路由更多卡死的机会,和通过路由判断 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;
}
}
调用方法:
let app = getApp();
Page({
xxx:function(e){
if(app.preventMoreTap(e)) {
return ;
}
//跳转
}
})
3、页面间重复跳转几次之后锁死
描述:发布商品这个页面,在拍拍二手里面算是一个中部流程的模块,上下游页面的跳转很频繁,甚至内部的分类也是跳转到一个新的页面。而且每个页面间的跳转我们都需要传递一系列的信息。显而易见按照官方文档我们会选择 navigateTo 、redirecTo 这两种方式。
使用 navigateTo 做页面跳转,只能跳转10次,第11次就会没有反应。而用 redirecTo 页面,当点击左上角触发回退按钮的时候,返回的页面不再是发布页面了,是其他的页面。
首先我们举个场景:当我们跳转使用 navigateTo, 由发布页 跳转 分类页 ,分类页选择一个分类 跳转回发布页,连续重复几次发现页面不动了。这是因为 navigateTo 跳转回把当前页面的信息加入到路由中,然后再跳转页面,把跳转的页面也放到了路由中,这个时候使用 getCurrentPages() 函数,我们可以得到一个数组,数组长度为2。当这个长度变成5的时候页面就不能跳转了。
显然这样是不可以的。如果使用 redirecTo 这个方法是可以解决跳转卡死的问题,但是如果这时候点击页面左上角的返回,我们发现它并没有像我们期待的一样返回到商品发布页面,而是返回到了商品发布的前一个页面。
如果使用 navigateBack 这个方法,我们发现不能够在页面的跳转中传参数,但显然这是一个好的思路,我们接下来只要解决传参的问题就可以了,小程序参数有3中思路可以传递:
- 通过 navigateTo 或 redirecTo,在 url 里面传递
- 把变动的参数放到缓存中,然后更新缓存。这种方法显然不好,缓存中会有多个参数。
- 通过 getCurrentPages() 获取一个数组对象取上个页面的序列然后使用 setData() 方法
8var pages = getCurrentPages();
var prevPage = pages[pages.length - 2];
prevPage.setData({
classId: id,
classifyName2: className,
classTags: classtags
})
wx.navigateBack()
综上所述第3种思路传递参数是最好的。这样就实现了两个页面之间的来回跳转,点击左上的返回也能够从分类回到商品发布页面。值得注意的是使用第3中方法我们需要确定pages[pages.length – 2];
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循环进行上传:
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
})
}
})
写到这里是有问题的,我们使用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]){
setTimeout(function(){
wx.uploadFile({
url : xxx,
filePath:tempFilePaths[0] ,
name: 'xxx',
formData: {
'user': 'test'
},
success: obj.success,
fail:obj.fail
})
},1000)
}
})
之后我们要处理的仅仅是按照序列把服务返回的信息更新到 data 里面,如果成功了就把等待上传替换成上传的图片,如果失败,就换成上传失败的图片,还可以通过这种情况设置重新上传图片,现在图片上传的功能完成了。
这些天我们遇到的坑
1、 图片上传总是失败网络不通
当我们所有的组件封装完毕,预览版没有问题而在预发版中发现图片总是出现上传失败的问题,这大多是 uploadFile 合法域名中没有添加上传图片的合法域名。如果遇到上传或者请求数据不通的情况,首先要检查一下我们的域名。
2、 range 数据未加载完 picker 绑定事件
我希望去实现如上图所示滑动选择,微信小程序很贴心的给我们封装了 picker 组件。
<picker bindchange="bindPickerChange" value="{{index}}" range-key="logisticsName" range="{{logisticsArray}}" >
<view class="picker">
<label>快递公司:</label>
<span>{{logisticsArray[index].logisticsName}}</span>
</view>
</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}。
现在看上去一切正常,由于设计稿有默认值“请选择快递公司”。很简单的思路,我们设置一个初始数组。然后再查询快递公司接口返回数据后进行拼接就可以了。
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
})
}
}
})
眼尖的你有没有发现什么问题?以为一切如期进行时,测试同学给我截了下面这个图。在接口数据没有返回时,去对 picker 进行 bindChange。就会只有一个请选择快递公司,其他的都没有。也就是用户操作必须在数据返回之后,这就取决于接口返回的速度。
按照以往的处理方法,我们可能会在数据返回回来之后再进行一个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同时执行
列表页,执行onPullDownRefresh(下拉刷新)时触发了分页所用到的onReachBottom(页面上拉触底事件处理函数),产生冲突。而我们可以通过增加一个参数去解决这个冲突
onReachBottom: function () {
// 到页面底部时,请求列表
if (!this.data.noMoreData && !this.data.isPullDown) {
this.setData({
currentPage: ++this.data.currentPage
});
this.getCollectList(this.data.currentPage);
}
}
4、组件open-data格式问题
这个严格说不算是组件缺陷,更应该是文档缺陷。
//错误的写法
<open-data type="groupName" open-gid="xxxxxx"></open-data>
//正确的写法
<open-data type="groupName" open-gid="xxxxxx"/>
5、下拉刷新三个白点的默认样式不展示
由于页面背景色也是白的,就导致看不到那三个点了。第一种方法是修改背景色,但是对当前样式的影响比较大;采用的是第二种方法,在已经添加下拉刷新页面对应的json文件中添加”backgroundTextStyle”: “dark”,就能看到三个白色的点了。
我们开发的小程序组件
项目过程中我们开发了很多自定义组件,例如:警告弹窗、搜索栏、底部状态栏、tab菜单、计算器、带确定取消的弹窗,我们以下面这个组件为例
Toast 和 ModalDailog 组件
小程序提供的 showToast、showModalDialog 的方法,因为设计风格问题,不能满足我们的需求,且它们只支持少数字符的展示(在ipx兼容测试时,我们还发现了文字白色背景的问题),所以我们一直采用自己封装的组件。
组件的创建和使用如下。
<template name="confirm">
<view class="jdc-confirm">
<view class="jdc-confirm__content">
.
.
.
</view>
</view>
</template>
引用这个模板
<import src="../template/template.wxml" />
<template is="toast" wx:if="{{toastShow}}" data="{{...toastData}}"></template>
在 JS 里面进行控制
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 提示。看完这些,对于微信小程序你还有什么疑问呢?如果有问题欢迎留言,我们一起探讨!
原文地址:https://jdc.jd.com/archives/7409