《拍拍二手》微信小程序开发经验谈

avatar
UX @京东
原文链接: jdc.jd.com

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

项目介绍

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

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

  • 拍拍首页
  • 拍拍群首页
  • 一键转卖列表页
  • 商品发布页
  • 商品详情页
  • 订单详情页
  • 我的(发布、卖出、收藏)

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

Video Player下载文件

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

项目预研

在此项目之前,我们有过几个小程序的经验,所以项目启动时,我们便采用“前端驱动业务”的方式,推动业务童鞋提前申请小程序依赖的资质,如:小程序账号、名称备案、支付资质、腾讯地图日访问量等等。
同时,区别于以往我们做过的小程序,本次项目将拍拍二手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 在手机中有偏差

<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下的兼容问题
Video Player下载文件

文本输入常用的标签无非就是 input、textarea,当我们使用这两个标签做一些文本编辑时在 ios 下遇到了3个问题,它们分别是:

  1. 当页面有遮罩层时,无法遮盖 textarea 的文字内容。
  2. 在 ios 系统下,修改 textarea、或者 input 里面的文本内容,如果在文本中修改,光标会跑到最后面。
  3. 在 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、页面快速点击可以重复触发
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;
      }     
 }

调用方法:

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() 方法

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

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同时执行
Video Player下载文件

列表页,执行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 提示。看完这些,对于微信小程序你还有什么疑问呢?如果有问题欢迎留言,我们一起探讨!

JDC前端-小程序开发小组(林如风、hanyuxinting、fishsif、上善若水、小学生、s s、贾慧斌)联合编辑。