大眾點評點餐小程式開發經驗 - 選單聯動設計

美團點評點餐發表於2017-03-05

作者介紹:李超,美團點評前端開發工程師,2年WEB開發經驗,現在是美團點評點餐團隊的一員。

  在我們團隊的小程式開發經驗系列多篇文章釋出以後,你是否對小程式檢視層(大眾點評點餐小程式開發經驗 - 檢視層),邏輯層(大眾點評點餐小程式開發經驗 - 邏輯層),API(官方API文件)等有更為深入的學習和了解呢?
“紙上談兵”很容易,“打好勝仗”才是關鍵。今天由我來為大家分享在實際開發“大眾點評點餐小程式”中遇到的問題和解決方案。

效果展示

大眾點評點餐小程式開發經驗 - 選單聯動設計
靜態效果展示圖

大眾點評點餐小程式開發經驗 - 選單聯動設計
動態效果展示圖

頁面佈局

  如果你看過我們的系列文章, 應該對我們的產品形態有了初步瞭解。我們是做點餐選單服務,選單需要分類,需要購物車模組,那麼典型的''型佈局是我們的首選。

大體結構為:頂部商家名稱,可能會出現黃色橫條提示模組;下方左側為導航選單欄;下方右側為每個選單分類包含的菜品展示列表;底部可能出現購物車模組。
看到這裡,再結合上面的圖片,你應該對選單頁的結構有比較具象的瞭解。
下面從產品角度說下具體的互動細節。

產品需求

  • 頂部要求顯示商家名稱,有分享功能;
  • 下方左側、右側可分開滾動,滾動左側不影響右側,滾動右側左側隨之聯動高亮顯示所在的選單分類;
  • 點選下方左側導航選單欄,高亮顯示被點選的選單分類,下方右側對應分類詳情模組頂部與右側滾動區的頂部重合(類似於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.less

我們在開發中使用工具對檔案實時編譯:

 `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事件,也就無法完成聯動設計。

滾動區域高度

  我們知道使用scroll-view需要指定高度,那麼這個高度值該怎麼算出來,以什麼樣的方式設定呢?
這裡我就不詳細的說明其用法了,直接看 scroll-view文件

注意兩點:

  • 必須使用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更加具有吸引力。這些都是需要我們做細緻的思考的。


本文對你有幫助?歡迎掃碼加入前端學習小組微信群:

大眾點評點餐小程式開發經驗 - 選單聯動設計

相關文章