記一次 Ant Design Menu組件的使用與深刻

1. 需求

最近項目中要修改原有的菜單,項目UI爲antd,antd的導航菜單長這樣:前端

看着挺好的,完美對齊,可是當把個人菜單文案填入以後發現:node

左右不對齊,這也太醜了吧,這要是聽任無論要被懟的。react

2. 排查問題

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

再看下個人demo不能對齊的樣式:github

發現個人菜單裏少了一個 min-width ,也就是說antd在某一步給官方的demo添加了style屬性,而沒有給個人菜單添加。chrome

爲何不給個人加!!!?api

直接來吧,先來一個MutationObserver,詳情看MDN文檔。瀏覽器

你是否是想在 antd 給那個 ul 標籤添加 style="min-width" 的時候告訴你一下?甚至能打個斷點來調試下,但不知道怎麼操做?直接上代碼:bash

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 。因而在我將鼠標移入菜單的時候,打印瞭如下內容:antd

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

鼠標移入時,瀏覽器停在了斷點上,右邊的 call stack 調用棧顯示正在執行 callback 函數,看它的下面 adjustWith ,也就是說代碼先執行 adjustWith ,而後觸發了咱們的斷點。咱們接着點開 adjustWith ,發現如下代碼:

看來就是這段代碼致使的。在瀏覽器中查看不方便,都是編譯以後的代碼,轉戰 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()

也就是說咱們須要傳入相似這樣的對象:

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

最終效果圖:

4. 總結

本文針對工做中的遇到的一個菜單組件對齊問題,粗略講到了調試思路,antd組件結構,dom-align, MutationObserver 和 Debuggr for Chrome插件,涉及代碼並不複雜, 但願讀者看完能有些許收穫。

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

相關文章
相關標籤/搜索