大众点评点餐小程序开发经验 - 菜单联动设计

做者介绍:李超,美团点评前端开发工程师,2年WEB开发经验,如今是美团点评点餐团队的一员。html

  在咱们团队的小程序开发经验系列多篇文章发布之后,你是否对小程序视图层(大众点评点餐小程序开发经验 - 视图层),逻辑层(大众点评点餐小程序开发经验 - 逻辑层),API(官方API文档)等有更为深刻的学习和了解呢?
“纸上谈兵”很容易,“打好胜仗”才是关键。今天由我来为你们分享在实际开发“大众点评点餐小程序”中遇到的问题和解决方案。前端

效果展现

静态效果展现图

动态效果展现图

页面布局

  若是你看过咱们的系列文章, 应该对咱们的产品形态有了初步了解。咱们是作点餐菜单服务,菜单须要分类,须要购物车模块,那么典型的''型布局是咱们的首选。json

大致结构为:顶部商家名称,可能会出现黄色横条提示模块;下方左侧为导航菜单栏;下方右侧为每一个菜单分类包含的菜品展现列表;底部可能出现购物车模块。
看到这里,再结合上面的图片,你应该对菜单页的结构有比较具象的了解。
下面从产品角度说下具体的交互细节。小程序

产品需求

  • 顶部要求显示商家名称,有分享功能;
  • 下方左侧、右侧可分开滚动,滚动左侧不影响右侧,滚动右侧左侧随之联动高亮显示所在的菜单分类;
  • 点击下方左侧导航菜单栏,高亮显示被点击的菜单分类,下方右侧对应分类详情模块顶部与右侧滚动区的顶部重合(相似于html中#id的锚点功能);
  • 滚动下方右侧菜品分类详情时,当该分类详情模块顶部接触到滚动区域的顶部,左侧对应的导航菜单栏高亮;
  • 若左侧高亮的导航菜单不在可视区域:
    • 当高亮的导航菜单顶部在左侧scroll-view滚动区上方(被遮住了),则将该高亮导航菜单滚动至将高亮导航栏的顶部与左侧可滚动区域顶部重合(高亮菜单为滚动区的第一个分类);
    • 当高亮的导航菜单在左侧scroll-view滚动区可视区下方,将高亮导航菜单滚动到屏幕中央区域(微小误差能够接受,主要看用户体验。);
  • 顶部下方可能会出现黄条提示文案模块;
  • 底部上方可能会出现购物车模块;
  • 顶部黄条提示文案模块吸顶,底部购物车模块吸底;
  • 须要适配各类不一样机型。

关键技术罗列

  这里须要指出:产品在设计成稿以前,咱们已经对小程序支持的功能作了细致的调研,在确保能够经过技术手段实现产品需求的前提下才肯定UI以及交互设计。微信小程序

  • 从产品兼容性角度出发,咱们考虑使用微信小程序的rpx做为UI设计的尺寸。该尺寸和rem很是相似,不一样点在于其对基准尺寸的设定。rem使用文档根元素设定的尺寸做为基准尺寸,而rpx使用iphone6(s)手机屏幕宽度为基准定出1rpx对应的宽度,该动态尺寸对设备的兼容性更加友好;
  • 微信自带scroll-view UI组件,并提供一系列组件状态操做接口;
  • scroll-view组件滚动时触发scroll事件,返回的event对象各项长度属性均使用px做单位;

代码编译

src
├── menu.html
├── menu.js
├── menu.json
└── menu.lessapi

咱们在开发中使用工具对文件实时编译:微信

`menu.html`->`menu.wxml`
 `menu.less`->`menu.wxss`复制代码

为方便代码维护以及平常的开发习惯,咱们支持了less语法,引入了Promise数据结构

wxml页面布局

### menu.html 
<page>
    <view class="menu-content">
        <view class="yellow-bar">
            // 黄色横条提示模块
        </view>
        <scroll-view class="scroll-view-left" height="{{ windowScrollHeight }}" scroll-into-view="{{ leftToView }}" scroll-top="{{ leftScrollTop }}">
            // 左侧分类导航
        </scroll-view>
        <scroll-view class="scroll-view-right" height="{{ windowScrollHeight }}" scroll-into-view="{{ rightToView }}">
            // 右侧分类详情
        </scroll-view>
        <view class="cart-bar">
            // 购物车模块
        </view>
    </view>
</page>复制代码

  这里着重考虑两个scroll-view结构设计,左右的布局结构可使用Css样式属性float或者是Css3flex;另外黄条提示模块和购物车模块使用fixed属性搞定。
微信官方文档介绍,使用scroll-view组件,必须指定高度。
实践结果:使用scroll-view能够不指定高度,页面有滚动区存在,问题是滚动时没法触发scroll事件,也就没法完成联动设计。app

滚动区域高度

  咱们知道使用scroll-view须要指定高度,那么这个高度值该怎么算出来,以什么样的方式设定呢?
这里我就不详细的说明其用法了,直接看 scroll-view文档less

注意两点:

  • 必须使用px做单位
  • 必须在scroll-view上显式的指定其height属性

在获取滚动区高度windowScrollHeight以前,考虑其影响因素:

  • 设备高度
  • 黄条文案提示模块的存在
  • 购物车模块的存在
  • rpx->px的转换

设备高度能够经过微信官方api getSystemInfo接口API获取。

那么,该何时调用接口?
首先这是一个异步API接口,另外其直接受系统权限控制的影响,基于这两点因素,其结果返回的时机就不是肯定的。
咱们能够在小程序启动时在onLaunch中调起该API,而后将获取的结果放入到全局变量globalData中。而globalData是挂在在全局App上的属性,对全部页面都可见。

getSystemInfo 结果数据结构

sysInfo Object {
    errMsg:"getSystemInfo:ok"
    language:"zh_CN"
    model:"iPhone 6"
    pixelRatio:2
    platform:"devtools"
    system:"iOS 10.0.1"
    version:"6.3.9"
    windowHeight:627
    windowWidth:375
}复制代码

这里的windowHeight, windowWidth指的是屏幕高度和宽度,且使用的单位是px

获取sysInfo

// app.js
// 注意这里的wxp为咱们对wx的封装,它继承wx的全部属性,特色是若调起wx的异步api函数将返回一个Promise实例。
 getSysInfo: function() {
        let that = this;
        if(that.globalData && that.globalData.sysInfo 
            && that.globalData.sysInfo.windowHeight) {
            // 将结果封装成Promise,后续可统一使用`then`方法
            return Promise.resolve(that.globalData.sysInfo);
        }
        return wxp.getSystemInfo()
            .then(res => {
                that.globalData.sysInfo = res;
                return res;
            })
            .catch(e => {
                // 能够尝试弹出框或toast
                console.error('[getSystemInfo]', e);
            });
   },

// menu.js 
onLoad: function() {
    app.getSysInfo().then((sysInfo)=> {
        // transform rpx -> px and calculate scroll-view height.
    }
}复制代码

计算fixed元素高度

  黄条文案提示模块,购物车模块的高度都是已知的。但你们是否注意到我以前提到的设计细节:全部的元素统一使用rpx作单位,而这里须要使用px做单位,必需要作rpx->px的转换。

rpx尺寸对照表

rpx->px装换

var yellowBarRpxHeight = 50;  // 黄色文案提示模块高度
    var percent = app.data.sysInfo.windowHeight / 375; // 当前设备1rpx对应的px值
    var yellowBarHeight = Number(yellowBarRpxHeight * percent).toFixed(2);复制代码

你们对除数375是否有疑问呢, 该比值是否会受到设备实际像素点的影响呢? 答案:不会。
一样的道理能够获得购物车模块的高度cartBarHeight
经过公式:
windowScrollHeight = windowHeight - yellowBarHeight - cartBarHeight
计算得出两个scroll-view的滚动高度。

左->右联动

点击左侧导航菜单栏,右侧定位到对应的分类菜品详情。
经过查看scroll-view文档发现可使用scroll-into-view属性;该组件自动定位右侧须要滚动到的具体位置。
给左侧导航菜单栏绑定tap事件监听函数,事件触发后获取event对象的currentTarget属性,取出渲染时存放在该节点上的分类id,用此id做为惟一标识定位右侧分类详情,设置右侧scroll-viewscroll-into-view属性,这时其会将右侧scroll-viewid属性值为该值的节点滚动到滚动区域的顶部(相似于html中的#id锚点功能)。

Tap事件监听函数

// menu.js
   bindLeftTap (e) {
        // 因为事件是冒泡的,因此不肯定点击操做是在哪一个元素上触发的,但currentTarget表示当前绑定事件对应的节点,即可准确获取该节点上的dataset
        let dataset = e && e.currentTarget && e.currentTarget.dataset;
        var LEFT_TO_RIGHT_SUFFIX = "l2r-";
        if(!dataset || !dataset.id) return;
        // target
        this.setData({
            highlightCategoryId: dataset.id, // 左侧高亮的导航菜单栏
            rightToView: LEFT_TO_RIGHT_SUFFIX + dataset.id, // 更新右侧的scroll-to-view属性。
        });
    }复制代码
  • LEFT_TO_RIGHT_SUFFIX"是什么东西?其为全局定义的常量,只是为了方便你们阅读,才将其写入函数内部,用做id拼接,保证惟一性。
  • 在开发阶段曾经尝试直接将获取到的id做为rightToView的值,也就是设定右侧scroll-viewscroll-into-view属性,发现右侧scroll-view不会滚动到指定的高度。猜测可能由于获取到的dataset.id是一个数字类型字符串,其内部使用===方式致使不匹配。
  • 设置scroll-into-view引发的滚动操做一样会触发scroll事件。

右->左联动

   右→左联动是整个页面设计最核心的部分。因为小程序没法获取元素的宽高,位置信息,对滚动右侧实现左侧联动效果带来挑战。

如何准确的获取右侧滚动到的具体分类,并让左侧导航菜单栏相应分类高亮,且在可视的范围内?

在设计阶段,咱们和设计同窗确认右侧每一个菜品详情模块高度固定,分类小灰条高度固定,这样咱们就能够根据已有的数据结构计算出每一个元素距离文档区顶部的高度。(请参考下图红框圈出内容分别对应分类小灰条,菜品模块详情)

单个菜品分类详情

// PER_BAR_HEIGHT 分类小灰条的高度
// PER_ITEM_HEIGHT 单个菜品详情的高度
var sumScrollHeight = 0;
var assistantCategories = spuMenuSet.map(it => {
    let unitHeight = PER_BAR_HEIGHT + (it.spuMenuItemList && it.spuMenuItemList.length ) * PER_ITEM_HEIGHT;
    it.scrollHeight = sumScrollHeight;
    sumScrollHeight += unitHeight;
    return it;
});复制代码

左侧导航菜单栏高亮分类切换的边界条件为右侧分类菜单详情的分类小灰条顶部与右侧滚动区顶部重合。

经过计算出每一个分类小灰条距离文档顶部的高度scrollHeight,在每次滚动事件触发时,比较当前滚动的高度与分类小灰条的scrollHeight,就可肯定当前在哪一个分类菜单详情区域内,从而实现左侧分类导航栏的高亮。

机器偏差

  在测试时发现,有些机型滚动下方右侧scroll-view时,在边界条件出现时并不会完成左侧导航菜单栏高亮分类的切换,每每存在10-100px的偏差。从产品角度,这种偏差是不能容忍的。我的并不肯定是什么缘由致使偏差的出现,但看起来并无很是好的解决办法。
那么能用什么方案减小偏差呢? 个人实现思路是"人工干预自动校订"。

人工干预自动校订

仔细分析滚动事件返回的event对象

Object
    currentTarget:Object
    detail:Object
        deltaX:0
        deltaY:-971
        scrollHeight:24737
        scrollLeft:0
        scrollTop:2409
        scrollWidth:295
        __proto__:Object

    target:Object
        dataset:Object
            __proto__:Object
            id:""
            offsetLeft:0
            offsetTop:38
        __proto__:Object
    timeStamp:13932
    type:"scroll"
    __proto__:Object复制代码

特别留意detail中的scrollHeight

滚动事件会给出整个scroll-view文档内容的高度,这个高度值很是关键,咱们彻底能够经过计算:
scrollHeight = 单个菜品详情高度 * 菜品总数 + 单个分类小灰条高度 * 分类小灰条总数

因为单个菜品详情高度与单个分类小灰条高度的高度比是肯定的,因此上面的方程式为一元方程,计算出单个菜品详情高度单个分类小灰条高度,更新每一个分类小灰条距离文档顶部的距离scrollTop值。
经测试发现,左侧导航菜单栏高亮分类的切换精度很是高,并且兼容性很好。

左侧高亮分类跳错问题

在实际开发中, 我还发现一个问题: 左侧有分类A、B、C,点击分类B,分类B高亮,右侧定位到分类B的详情区域,随之左侧高亮分类切换到A上。
你们是否想到是什么缘由致使的? 在上面讲解scroll-view属性时我提到过一句话:

设置scroll-into-view引发的滚动操做一样会触发scroll事件

这里点击左侧分类,右侧因为scroll-into-view触发了滚动事件,而相应的滚动事件监听函数函数,计算得出当前高亮的导航菜单栏为A,更新页面的data将高亮分类切换到了A上。
解决方案: ① 修改边界条件,但在不一样机器上存在细微差异,咱们没法准确的设置偏差范围;毕竟元素宽高都是咱们算出来的;② 限制右侧的scroll事件函数的执行。
推荐使用第二种方式。思路:若点击左侧导航菜单栏,设定全局锁定状态,若锁定则不右→左的联动操做,再解除锁定状态。

分类导航栏的可视问题

  经过上面“右→左”联动,咱们已经可让左侧随着右侧滚动而高亮,问题是: 左侧也是一个scroll-view,如何保证高亮的分类在可视区呢?具体的交互逻辑请看前面的产品需求

高亮分类在可视区下方

高亮分类在可视区上方

监听右侧滚动事件,判断当前在哪个分类上,肯定该分类在左侧scroll-view的文档高度,判断是否须要滚动左侧scroll-view
能够经过scroll-viewscroll-into-view或者scroll-top属性完成滚动。

// 这里是伪代码实现
var index = mapId2index(id); //将id转换为对应分类的index值
var perCateHeight = 40; // 左侧每一个分类高度为40
var leftScrollTop = 0; // 左侧scroll-view滚动的高度
var windowScrollHeight = 1440; // 这个值为屏幕高度,可经过getSystemInfo获取到
var cHeight = index * perCateHeight; // 当前分类距离文档顶部的scrollTop值
if( cHeight - leftScrollTop - windowScrollHeight > 0) {
    // 高亮的区域在屏幕底部
    leftScrollTop = cHeight - windowScrollHeight / 2; //左侧scroll-view向上滚动半个屏幕高度
    leftToView = null; // 不使用scroll-into-view 属性, 必须置空, 不然会优先应用该属性而不是leftScrollTop
} else if (cHeight - leftScrollTop < 0) {
    // 高亮的区域在屏幕顶部之上,设置scroll-into-view属性
    leftToView = id;
    leftScrollTop = cHeight; // 须要记录下当前scroll-view滚动高度,以便下次使用
} else {
    leftToView = null;
}复制代码

注意点: 若同时设置了scroll-into-viewscroll-top属性,优先使用scroll-into-view属性, 故这里若使用scroll-top属性滚动时须要将scroll-into-viwe属性置空。

优化

联动功能开发完以后,遇到了性能瓶颈。因为复用以前C端的数据接口,接口中存在大量无用的对象属性,而这个数据结构直接做为页面渲染的data数据。
推荐的作法就是简化data数据结构,只存放影响页面渲染的数据,这样作可以大幅度下降UI渲染时间,给用户更加流畅的体验。

总结

微信小程序算是2016年-2017年里很是火的一门新技术了。
如何使用已经支持的功能特性来设计、开发产品是保障项目顺利完成的重要环节。
而在开发过程当中,专一细节实现,吃透API文档,让用户感觉到咱们开发小程序的诚意,而不是在作粗糙的产品复制。

感觉

在小程序发布那段时间,总能看到各类对小程序将来的设想,有悲观的,有观望的,也有激进的。我我的认为,“赶鸭子上架”的思路并不可取,必须清楚本身的产品定位。你的产品是否知足“一次性消费”理念,内容是否不足以吸引用户下载你的APP,是否比你的H5更加具备吸引力。这些都是须要咱们作细致的思考的。


本文对你有帮助?欢迎扫码加入前端学习小组微信群:

相关文章
相关标签/搜索