专栏名称: 京东设计中心JDC
专业,创造力,激情,设计。京东用户体验设计部门,致力于创造更美好的电子商务购物体验。
51好读  ›  专栏  ›  京东设计中心JDC

JDC丨京东设计中心 - 《拍拍二手》微信小程序开发经验谈

京东设计中心JDC  · 公众号  ·  · 2018-01-10 17:38

正文

前两周想必大家都看到了京东发布拍拍二手交易平台的新闻,「拍拍二手」APP也正式上线。与此同时我们也紧锣密鼓的进行着「拍拍二手」微信小程序的开发。整个过程痛并快乐着,体会着采坑的痛苦,和跳出坑之后的喜悦。

项目介绍

「拍拍二手」主要有三大业务:回收、优品和个人闲置交易。京东“将以平台化的运营思路,整合回收、检测、再加工、销售等逆向供应链资源,做品质二手。”,而基于微信有庞大的社交关系链,利于产品的推广,直接面对用户,助力自身业务等优点。公司于是决定推出微信小程序版的「拍拍二手」。

微信小程序的的主要页面有:

  • 拍拍首页

  • 拍拍群首页

  • 一键转卖列表页

  • 商品发布页

  • 商品详情页

  • 订单详情页

  • 我的(发布、卖出、收藏)

我们打开小程序,看一段操作的视频:


可谓是麻雀虽小五脏俱全。

项目预研

在此项目之前,我们有过几个小程序的经验,所以项目启动时,我们便采用“前端驱动业务”的方式,推动业务童鞋提前申请小程序依赖的资质,如:小程序账号、名称备案、支付资质、腾讯地图日访问量等等。
同时,区别于以往我们做过的小程序,本次项目将拍拍二手C2C的整体流程移植到小程序平台,并实现以微信群为载体的交易体验。在需求评审过程中,我们大致遇到以下几个问题,并进行了技术预研。预研结果我们将在技术难点部分展开解说。

技术架构

在现有小程序的框架基础上,我们丰富了自定义组件,新增了基础类库,引入了SASS、Eslint在小程序里的应用。这里简单抛出几点:

  1. 因受限于小程序包大小的限制(开发时包大小限制为2M);我们对静态图片资源也做了优化,并将大部分图标放在了CDN,小程序直接访问网络资源。

  2. SASS的使用,既是沿用我们现有的PC、M端的重构方式(大家都已熟稔于心),也大大提升了小程序开发的效率。

  3. ESLint 的应用,采用我们设置的代码规范,为我们的代码输出做了把关。

此外,鉴于小程序路由跳转层级的限制(最初是5级),我们细化了每个流程的路由跳转方案。

技术难点

以下,我们将重点解析在项目中遇到的疑难问题和解决方案。我们从小程序包大小、兼容性问题、现有组件缺陷、这些天我们遇到的坑、我们开发的小程序组件、为业务提供备选方案等角度一一举例解析。

小程序包大小限制

为了达到代码不超过2M,为了小而全,我们在开发过程中就必须去思考如何减少代码量,同时提高用户体验。如何提高小程序的代码复用率,同时还要降低它们的耦合。

首先,我们采用前后端分离的方式,前后端约定接口文档;也放弃了传统前端出静态页再套页面、模板开发的方式,前端直接依据接口规范模拟数据后重构+开发;

第二,在开发前我们做了很多的探讨,从几十张设计稿中归纳可以通用的模块,编写了很多通用组件;在数据处理方面编写了很多公共方法,提炼到 util 类中;

第三,我们将静态资源雪碧图化、tiny后,发布到CDN上,小程序里依赖图标的元素直接引用网络资源。

小程序兼容问题

小程序在兼容性方面有一些已知问题,在文档中已明确指出,但最近新出的iPhone X,文档尚不全面,我们这次也对该机型做了测试,并整理出我们遇到的一些兼容性问题,希望可以对大家有所帮助。

首先给大家看一张图片,它存在两个问题,下面我一一介绍它们的处理方式:

1、border-radius 设定后在 iphoneX 中元素的边框显示不全

遇到这个问题的时候只需要把 rpx 改成 px 即可。其实不只是小程序有这类问题,在 M端开发过程中如果使用 rem 这种单位都难以避免会造成这样。

2、iphoneX 中 view 设定 padding-left 在手机中有偏差

1

2

3

4

5

6

class = "com-lab " >

运费

class = "sel-box" >

分类

这段代码很简单,我们看到运费有个 span 标签包裹,分类没有,而在写 wxss 的时候 我们这样写的

1

2

3

4

5

6

.com-lab span {

padding-left : 30rpx ;

}

.sel-box {

padding-left : 30rpx

}

在 iphoneX 中就会产生如上图的偏差,修改方式也简单

1

2

.com-lab { padding-left : 30rpx ; }

.sel-box { padding-left : 30rpx }

去掉了 span 标签的 padding 而改到了外层的 view 中这样偏差就没有了,可第一种写法在浏览器中也是对的,为什么在 ios 手机中有这种偏差呢,我觉得可能是编译时候小程序的语法造成的,所以在做页面重构的时候尽量减少这些差别。

3、iphoneX 适配微信底部操作区问题

大家知道 iPhoneX 手机打开刘海模式后,有安全区的概念,而我们需要把展示内容都放在安全区域内,所以需要对底部的黑色 Home Indicatorzuo 做处理,否则会遮挡住文字。首先是在JS代码中区分一下机型

1

2

3

4

5

6

7

wx . getSystemInfo ({

success : function ( res ) {

if ( res . model . toLowerCase (). indexOf ( 'iphone x' ) != - 1 ) {

me . globalData . isIpx = true ;

}

}

});

然后在wxss中做一下样式的处理

1

2

3

4

5

6

7

8

9

10

1

. 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个问题,它们分别是:

  1. 当页面有遮罩层时,无法遮盖 textarea 的文字内容。

  2. 在 ios 系统下,修改 textarea、或者 input 里面的文本内容,如果在文本中修改,光标会跑到最后面。

  3. 在 ios 系统下 textarea 会增加一个 padding,而我们怎么怎么用过样式控制都不能去掉这个 padding。

我们拿商品描述为例,它使用的文本输入标签是 textarea,下面是一段 wxml 代码:

1

2

3

4

5

6

7

8

9

class = "des-msg" >

描述

class = "{{desshow == 1 ? 'shows' : 'hidden'}} {{postData.devicesType == 2 ? 'iosText' : 'andText'}}"

name = "charactersDesc"

maxlength = "1001"

placeholder = "描述一下商品吧"

value = "{{postData.charactersDesc}}" />

问题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。

1

2

3

4

5

6

7

8

9

10

11

12

//终端数据类型

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() 注册一个一个全局的函数,然后在涉及到触发跳转的地方调用这个方法,就可以阻止重复点击触发了,下面是具体的处理方法

1

2

3

4

5

6

7

8

9

10

1

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 ;

}

}

调用方法:

1

2

3

4

5

6

7

8

9

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中思路可以传递:

  1. 通过 navigateTo 或 redirecTo,在 url 里面传递

  2. 把变动的参数放到缓存中,然后更新缓存。这种方法显然不好,缓存中会有多个参数。

  3. 通过 getCurrentPages() 获取一个数组对象取上个页面的序列然后使用 setData() 方法


1

2

3

4

5

6

7

8

var 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。首先举一个单图片上传的例子:


1

2

3

4

5

6

7

8

9

10

11

12

13

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循环进行上传:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

1

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 延时函数,可以有效的避免快速请求服务器。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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 组件。

1

2

3

4

5

6

< picker bindchange = "bindPickerChange" value = "{{index}}" range-key = "logisticsName" range = "{{logisticsArray}}" >

< view class = "picker" >

< label > 快递公司: < /label>

{{logisticsArray[index].logisticsName}} pan >

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}。

现在看上去一切正常,由于设计稿有默认值“请选择快递公司”。很简单的思路,我们设置一个初始数组。然后再查询快递公司接口返回数据后进行拼接就可以了。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

data : {

logisticsArray : [{ logisticsCode : "" , logisticsName : "请选择快递公司" }]

}

var _self = this ;

wx . request ({

url : 'getLogisticsArray' , //仅为示例,并非真实的接口地址

data : {},

header : {

'content-type' : 'application/json'

},

success : function (







请到「今天看啥」查看全文