前两周想必大家都看到了京东发布拍拍二手交易平台的新闻,「拍拍二手」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 在手机中有偏差
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个问题,它们分别是:
-
当页面有遮罩层时,无法遮盖 textarea 的文字内容。
-
在 ios 系统下,修改 textarea、或者 input 里面的文本内容,如果在文本中修改,光标会跑到最后面。
-
在 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中思路可以传递:
-
通过 navigateTo 或 redirecTo,在 url 里面传递
-
把变动的参数放到缓存中,然后更新缓存。这种方法显然不好,缓存中会有多个参数。
-
通过 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
(
|