天天看點

不吹牛,完爆ant design的定位元件——floating-ui

作者:小滿隻想睡覺

前言

因為要寫react定位元件(這不是标題黨,就是完爆ant design的定位元件,你應該看到一半就會同意我的觀點),如下圖:

不吹牛,完爆ant design的定位元件——floating-ui

紅框部分是用絕對定位放在按鈕上面的,你們B端用的主流元件庫都是這樣實作的,它是很多元件的基礎元件,比如下圖:

下拉框元件

不吹牛,完爆ant design的定位元件——floating-ui

select元件

不吹牛,完爆ant design的定位元件——floating-ui

還有什麼DataPicker,TreeSelect,Dropdown元件等等的下拉框都是以定位元件為基礎的。

這個元件實作的複雜度在哪

上面提到,這不過就是一個絕對定位嘛(我們假設紅框部分的的dom絕對定位是相較于body元素),我們拿最簡單的情況來看,如下圖,如何把紅框部分渲染到按鈕”更多“的下方呢?

不吹牛,完爆ant design的定位元件——floating-ui

我們可以計算更多按鈕的getBoundingRect(),傳回

const reference = {
  top: xx, // 按鈕距離浏覽器頂部的距離
  left: xx, // 按鈕距離浏覽器左邊的距離
  width: xx, // 按鈕的寬:沒有padding
  height:xx,// 按鈕的高:沒有padding
  ...等等其他屬性
}
複制代碼           

是以紅框部分左上角的坐标就輕易的計算出來了,上面的資料在reference對象上,是以借助reference的定位,我們計算紅框部分的下拉框的定位是在哪

{
    position: 'absolute',
    top: reference.top + window.pageYOffset // 豎直方向滾動距離 + reference.height
    left: reference.left + window.pageXOffset // 橫向滾動距離 
}
複制代碼           

為啥是上面這麼計算呢,假如沒有滾動條滾動,那麼紅框部分的絕對定位的top,是不是等于按鈕的距離浏覽器頂部的高度 + 本身的高度,這個沒問題吧?

然後,如果滾動條滾動了的話,是不是要在上面top的基礎上加上這段距離,就是紅框部分在文檔流絕對定位的top。

好了,到此為止,就是最基本的定位元件的邏輯了,我們接下來看複雜點!

複雜度1

還是拿上面的圖的紅色部分下拉框來說,下拉框一般是在下面,但是我可以定位到上邊吧?左邊,右邊也沒啥吧,再過分點,右上,左下?定位元件要處理對吧

複雜度2

假設,我們定位在下面,我想向左偏移8px,向下偏移3px咋辦,你是不是應該有暴露一個口子

複雜度3

假設我定位在下面,那麼我一直滾,馬上就要滾動到看不見下拉框了,如下圖

不吹牛,完爆ant design的定位元件——floating-ui

此時我想讓定位在上面,能不能自動幫我處理?如下圖:

不吹牛,完爆ant design的定位元件——floating-ui

複雜度4

是不是還有可能超出浏覽器視口了,如下圖:

不吹牛,完爆ant design的定位元件——floating-ui

我們想自動處理,遇到超出就自動變為下方樣子:

不吹牛,完爆ant design的定位元件——floating-ui

複雜度5

此時我定位了一次,但是有可能滾動容器不是window,是另一個div,這個計算咋辦?還有,是不是我滾動的時候,我要監聽滾動事件,還要監聽浏覽器resize事件,因為我定位的值可能會變?為啥呢,我們上面複雜度3是不是自動幫我們在滾動的時候調整位置

是以你不監聽滾動事件你咋知道要調整位置了?

還有很多細枝末節,比如浏覽器相容性等等。

國内元件庫怎麼實作這個功能

目前阿裡的ant design和位元組的arco design都是自己實作的,我們拿arco來看(ant内部叫rc-trriger元件,arco叫trriger元件),面向過程的代碼,看的我頭皮發麻。。。我截個圖:

不吹牛,完爆ant design的定位元件——floating-ui

上面的代碼屬于把我們提到的複雜度全部揉在了一起。

floating-ui為啥代碼品質比ant高

它是以中間件的形式去處理的,思路是什麼呢?它假設最開始有一個 computePosition函數,我們假設上面提到的複雜度都沒有,也就是不考慮的前提下,我們怎麼計算定位元件的坐标,也就是我們最前面的圖裡說的,紅色框部分絕對定位的的的top值和left值:

API如下:

computePosition(要挂載的dom節點,下拉框元件,參數...)
複制代碼           

然後我們剛才提到的複雜度,它分别用中間件的形式去處理,比如複雜度2,是想定位之後還有點偏移量,floating-ui咋做的呢

import {computePosition, offset} from '@floating-ui/dom';

// referenceEl: 要挂載的dom節點
// floatingEl:下拉框元件(或者說想要挂載到上面referenceEl的dom元素)
computePosition(referenceEl, floatingEl, {
  middleware: [offset(10)],
});
複制代碼           

如上,offset就是一個中間件,offset(10),就是向左偏移10px

好了,如果想處理複雜度3呢,我們用另一個中間件

import {computePosition, flip} from '@floating-ui/dom';
 
computePosition(referenceEl, floatingEl, {
  middleware: [flip()],
});
複制代碼           

這樣就自動處理了,是不是很簡單啊

其實所有這些複雜度的解決方案,在floating-ui裡都是以中間件的形式去處理的,還可以傳多個中間件解決多個問題。

中間件的形式好在哪

那麼我們就可以自定義很多中間件了,也就是你的元件不僅僅提供了很多功能,解決了很多常用的問題,你還允許使用者寫代碼去拓展,試問,現在哪個元件庫的代碼是這麼寫的?沒有吧?

代碼中間件原理

我們先看看floating-ui的computePosition API是怎麼實作的,它是floating-ui的核心方法,是串聯所有中間件的基礎。

下一篇寫完整的源碼(很晦澀,估計也沒幾個人看,是以這期就不寫了),了解起來說實話,你不熟悉原生dom的話有點困難,比如說為啥這個庫要用window.pageYoffset而不是document.body.scrollTop去擷取浏覽器html元素的滾動距離,因為document.body.scrollTop固定為0,取不到。。。

核心思路講解:我們還是拿下圖做類比

不吹牛,完爆ant design的定位元件——floating-ui
let {x, y} = 求出紅色框裡的下拉框絕對定位的x坐标和y坐标

// 記錄原始placement
  let statefulPlacement = placement;
  // 所有中間件導出的值都挂載到下面的對象上
  let middlewareData: MiddlewareData = {};
  
  // 資料經過middleware的處理
  // middleware是一個數組,存放所有中間件,就是我們上面說的處理每一個複雜度的對象
  for (let i = 0; i < middleware.length; i++) {
   // name是中間件的名字,fn是處理複雜度的邏輯
    const {name, fn} = middleware[i];
   
   
   // 通過把最前面計算的x,y經過fn的處理,得到了新的x,y的值
   // data是指傳回的資料,想讓後面的中間件也能通路到的資料
    /**
     * 每個middleware需要傳回
     * x 新的x坐标
     * y 新的y坐标
     * data 
     * reset
     */
    const {
      x: nextX,
      y: nextY,
      data,
      reset,
    } = await fn({
      /**
       * 每個middleware收到的參數
       * x 目前的x坐标
       * y 目前的y坐标
       * initialPlacement 最初傳入的placement
       * placement 
       * middlewareData middleware傳回的額外資料
       */
      x,
      y,
      initialPlacement: placement,
      placement: statefulPlacement,
      strategy,
      middlewareData,
      rects,
      platform,
      elements: {reference, floating},
    });

    x = nextX ?? x;
    y = nextY ?? y;

    // 每次處理後的資料想要讓後面的中間件通路,就需要挂載到middlewareData對象
    // 這個對象非常好啊,用name隔離了作用域
    middlewareData = {
      ...middlewareData,
      [name]: {
        ...middlewareData[name],
        ...data,
      },
    };

   rest的處理邏輯。。。省略,不是很重要
   
   

複制代碼           

最後return出被處理完的x,y坐标,或者自動幫我們監聽滾動事件和resize事件,然後拿着x,y就可以賦在css的絕對定位的top和left上,實作了定位。

每次處理後的資料想要讓後面的中間件通路,就需要挂載到middlewareData對象,這個對象非常好啊,用name隔離了作用域,這就是比koa這個架構處理的高明之處,koa裡的ctx對象就像一個垃圾桶,什麼屬性都往上面挂載,挂載太多了,你也不知道是哪個中間件挂載的

是以floating-ui的處理思路給我打開了新的思路!nice!!!

中間件如何寫

源碼再開一篇文章寫,這裡看看就好,不用過多去關系代碼

export const offset = (value: Options = 0): Middleware => ({
  name: 'offset', // 中間件名字
  options: value,  // 傳給中間件的值
  async fn(middlewareArguments) { // 中間件處理函數
    const {x, y} = middlewareArguments;
    const diffCoords = await convertValueToCoords(middlewareArguments, value);

    return {
      x: x + diffCoords.x,
      y: y + diffCoords.y,
      data: diffCoords,
    };
  },
});

複制代碼           

本文結束,是以如果市面上的元件庫的每個元件都是這個形式暴露給使用者,就是提供插件式的自定義的中間件,那麼整個元件庫的拓展性可以說碾壓市面上國内所有的react的元件庫了

之前寫的react元件如下:

  • Affix元件: react元件庫源碼+ 單測解析(Affix 固釘元件)
  • Form元件:實作一個比ant-design更好form元件,可用于生産環境!
  • GridLayout元件:秒殺ant design布局元件
  • Button和ButtonGroup 按鈕元件: react元件庫源碼+ 單測解析(Button和ButtonGroup 按鈕元件)
  • 月曆元件: react 月曆元件上
  • 日元件拖拽邏輯部分: react 月曆元件下
  • Modal元件:實作一個比ant功能更豐富的Modal元件
  • Portal元件:react如何把元件渲染到任意另一個元件内?
  • 實作Anchor元件: # react元件庫系列:實作Anchor元件