記一次 Ant Design Menu元件的使用與深入

songjp發表於2019-02-23

1. 需求

最近專案中要修改原有的選單,專案UI為antd,antd的導航選單長這樣:

記一次 Ant Design Menu元件的使用與深入

看著挺好的,完美對齊,但是當把我的選單文案填入之後發現:

記一次 Ant Design Menu元件的使用與深入

左右不對齊,這也太醜了吧,這要是放任不管要被懟的。

2. 排查問題

開始審查元素,先檢視官方demo正常能對齊的樣式:

記一次 Ant Design Menu元件的使用與深入

再看下我的demo不能對齊的樣式:

記一次 Ant Design Menu元件的使用與深入

發現我的選單裡少了一個 min-width ,也就是說antd在某一步給官方的demo新增了style屬性,而沒有給我的選單新增。

為什麼不給我的加!!!?

直接來吧,先來一個MutationObserver,詳情看MDN文件。

你是不是想在 antd 給那個 ul 標籤新增 style="min-width" 的時候告訴你一下?甚至能打個斷點來除錯下,但不知道怎麼操作?直接上程式碼:

var ele = document.getElementById('item_2$Menu')   // 先找出該元素
var config = {attributes: true, attributeFilter: ['style']}
var callback = function (mutationsList) {
  console.log(mutationsList)
}
var observer = new window.MutationObserver(callback)
observer.observe(ele, config)
複製程式碼

上面這段程式碼就是說,在 item_2$Menustyle 屬性發生變化的時候,列印下 mutationsList 。於是在我將滑鼠移入選單的時候,列印了以下內容:

記一次 Ant Design Menu元件的使用與深入

這有什麼用呢?別急,說明滑鼠移入,會執行到這裡的程式碼,那麼,不如打個斷點?

記一次 Ant Design Menu元件的使用與深入

滑鼠移入時,瀏覽器停在了斷點上,右邊的 call stack 呼叫棧顯示正在執行 callback 函式,看它的下面 adjustWith ,也就是說程式碼先執行 adjustWith ,然後觸發了我們的斷點。我們接著點開 adjustWith ,發現以下程式碼:

記一次 Ant Design Menu元件的使用與深入

看來就是這段程式碼導致的。在瀏覽器中檢視不方便,都是編譯之後的程式碼,轉戰 Vscode ,檢視我們的node_modules目錄,先找到這個檔案 node-modules/rc-menu/es/SubMenu.jsadjustWidth 方法:

this.adjustWidth = function () {
  /* istanbul ignore if */
  if (!_this3.subMenuTitle || !_this3.menuInstance) {
    return;
  }
  var popupMenu = ReactDOM.findDOMNode(_this3.menuInstance);
  if (popupMenu.offsetWidth >= _this3.subMenuTitle.offsetWidth) {
    return;
  }

  /* istanbul ignore next */
  popupMenu.style.minWidth = _this3.subMenuTitle.offsetWidth + 'px';
};
複製程式碼

原來它會先判斷寬度,如果 popupMenu 的寬度大於父級的 Title 寬度,就會直接返回,小於的時候才會加上 min-width 屬性。

這麼費勁總算找到了!

但是,找到了然後呢?問題是我咋去對齊?既然文案長度不一致,那麼居中對齊好了。

3. 解決

繼續檢視 antd 的文件,看看有沒有什麼引數方法遺漏了,發現個Menu文件小角落有個 More options in rc-menu ,點進去之後發現了一片更廣闊的世界 ( 其實我之前就問過同事知道antd還依賴於 react-component ,這個庫才是antd元件具體的實現 ) 。

經過一番查詢,找到一個props叫做 builtinPlacements 很可疑,描述是 Describes how the popup menus should be positioned(描述popup的選單如何被定位) ,引數為 dom-align 的配置物件,繼續檢視 dom-align介紹 發現這就是一個處理定位的小庫,處理 domA(sourceNode) 和 domB(targetNode) 的位置關係:

const alignConfig = {
  points: ['tl', 'tr'],        // align top left point of sourceNode with top right point of targetNode
  offset: [10, 20],            // the offset sourceNode by 10px in x and 20px in y,
  targetOffset: ['30%','40%'], // the offset targetNode by 30% of targetNode width in x and 40% of targetNode height in y,
  overflow: { adjustX: true, adjustY: true }, // auto adjust position when sourceNode is overflowed
};

domAlign(domA, domB, alignConfig);
複製程式碼

這樣,就能讓domA的 左上角(tl) 和domB的 右上角(tr) 對齊。直覺告訴我 builtinPlacements 屬效能解決我的對齊問題。

接下來繼續除錯,先介紹個除錯工具 (同事告訴我的) 。想想,之前我要除錯前端程式碼都是一堆的 console.log ,要麼在瀏覽器 source 中打斷點除錯,也可以除錯 node_modules 裡面的程式碼(也是從同事那裡學來的),但是缺點是程式碼都是被編譯打包過的,可讀性不好。於是有 Vscode 的外掛 Debugger for Chrome,有了這個外掛之後,可以直接在 Vscode 裡面給前端js程式碼打斷點!。

接下來可能比較跳躍:

直接找到node_modules下的 rc-menu/es/submenu 目錄,就是react-component下的menu元件。先搜尋資料夾內搜尋 builtinPlacements 這個詞,看下哪幾個地方用到了。發現如下:

  1. rc-menu/es/submenu.js
...
var builtinPlacements = props.builtinPlacements;
...
React.createElement(
    Trigger,
    {
        ...
        builtinPlacements: _extends({}, placements, builtinPlacements),
        ...
    }
複製程式碼

原來Submenu拿到傳入的 builtinPlacements 用來建立 Trigger 了,繼續找 Trigger 發現:

  1. rc-trigger/es/index.js
Trigger.prototype.getPopupAlign = function getPopupAlign() {
    ...
    var builtinPlacements = props.builtinPlacements;
    ...
    return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign)
};

...
var align = _this5.getPopupAlign();
...
return React.createElement(
      Popup,
      _extends({
        align: align,
    })
)
複製程式碼

得,又拿來建立 Popup 了,不過先記住這個函式 getAlignFromPlacement(builtinPlacements, prefixCls, align, alignPoint):

  1. rc-trigger/es/Popup.js
import Align from 'rc-align';
...
React.createElement(
  Align,
  {
    ...
    align: align
    ...
  },
}
複製程式碼

拿去建立 Align 了:

  1. rc-align/es/Align.js
import { alignElement, alignPoint } from 'dom-align';  // 你總算出來了
...

var align = _this$props.align
...
if (element) {
  result = alignElement(source, element, align);
} else if (point) {
  result = alignPoint(source, point, align);
}
...
複製程式碼

所以大概關係是 Antd Menu => rc-menu => rc-trigger => rc-align => dom-align ...

其中你在 Menu 傳入的 builtinPlacements 引數,會在rc-trigger中被當做引數傳入 getAlignPopupClassName(builtinPlacements, prefixCls, align, alignPoint) ,得到的結果最終會被傳入到 dom-alignalignElement 或者 alignPoint 中。

但是 builtinPlacements 這個引數怎麼傳值呢?

  1. function getAlignFromPlacement()

記一次 Ant Design Menu元件的使用與深入

也就是說我們需要傳入類似這樣的物件:

builtinPlacements: {
    bottomLeft: {
        // alignConfig物件
        points: ['tl', 'tr'],
        offset: [10, 20],
        ...
    },
    leftTop: {
        ...
    }
}
複製程式碼

並且可知: getAlignFromPlacement(builtinPlacements, placementStr, align) 中的 placementStr 此時為 bottomLeft ,所以我們的Menu變成了:

<Menu builtinPlacements={
    {
        bottomLeft: 
        {
            points: ['tc', 'bc'], // 子選單的 "上中" 和 對應選單的title "下中" 對齊。
            overflow: {
              adjustX: 1,
              adjustY: 1
            },
            offset: [0, 5]
        }
    }
}>
{this.renderMenuItems(menuItems)}
</Menu>
複製程式碼

至於 placementStr 的值 bottomLeft ,其實是:

rc-menu/es/SubMenu.js

var popupPlacementMap = {
  horizontal: 'bottomLeft',
  vertical: 'rightTop',
  'vertical-left': 'rightTop',
  'vertical-right': 'leftTop'
};

var popupPlacement = popupPlacementMap[props.mode];
// 這個值最終作為"placementStr"的值,水平選單Menu的"mode""horizontal"時,"placementStr"即為"bottomLeft"複製程式碼

bottomLeft , rightTop 的具體位置圖我猜可以參考Antd的 Popconfirm

最終效果圖:

記一次 Ant Design Menu元件的使用與深入

4. 總結

本文針對工作中的遇到的一個選單元件對齊問題,粗略講到了除錯思路,antd元件結構,dom-align, MutationObserver 和 Debuggr for Chrome外掛,涉及程式碼並不複雜, 希望讀者看完能有些許收穫。

感謝我那些無所不知的同事們。

相關文章