https://medium.com/@FezVrasta/popper-js-v1-5e8b3acd888c
https://survivejs.com/blog/popper-interview/
本文譯自popper.js作者的一篇部落格
在過去,我為了在web app中更好地定位我的tooltips和popover,我會花幾個小時寫同樣的一段程式碼,不斷進行微調。每次我開始一個新的專案,總會根據不同的環境對定位有不同的需求。這種繁瑣直到我用emberjs開發一個大型應用時達到極致,這個專案中由於比較爛的UX設計決定,幾乎她想在每個元素上都支援hover出現一個popover!
在這個大專案中,我們開始使用bootstrap3的tooltip,並不斷地通過hack程式碼來實現新的功能。
在這個大專案中,一個重要的需求是:不允許將tooltip元素移動到body元素的直接兒子,因為如果這樣的話,我們的程式碼就會broken.
幾乎經過一年的專案開發並且維護我們的定製化實現,我決定使用Tetcher,因為他貌似非常強大和穩定。。不幸的是,玩了幾個小時後,發現他有一個重要的缺陷或者說限制:他會自動地修改dom節點,將popper移動到body的兒子位置。他會增加一些classes並且增加一些inline style,而這你幾乎無法控制!
於是我又到github上花了很多時間去查詢有沒有備選的方案,但是我最終一無所獲。難道是沒有人需要一個類似的library?或者還沒有人能夠比較好的抽象出來並實現一個可以單獨釋出的lib?
我決定花一個週末的事件重寫我們自己專案中使用的定位engine,這就是popper.js的又來。
在v0.x時代,popper僅僅是一個js檔案,謝了幾個函式,聚焦在保持lib小巧輕量級。我希望popperjs能夠有很好的擴充套件性,所以我決定使用middleware system.主要的想法是:計算元素的位置並且允許middleware來根據特定的需求來修改這個位置。
比如:一旦我們有了popper的position,並給了一些邊界約束,那麼modifier就可以檢查popper是否會overflow並且自動反轉位置以確保popper不會被視窗cut off.
在工作了一段時間 後,我發現我們需要更好的程式碼結構以便更好地能夠維護使用他,這就是v1.0版本釋出的初衷。
為了管理好程式碼,我決定切換到es6模組上來。我引入了rollup作為code bundler並使用babel作為transpiler,我也將自動化測試引擎從一個無頭的chrome setup切換到saucelabs cross-browser test suite上來。
在開發過程中,我決定各個功能模組一級modifier/middleware都放到他們自己的檔案中,這樣能在Libray中重用。
update流程:
我也重構了整個update流程,也就是每次popper需要更新元素的位置時需要呼叫的程式碼。
這個update process會在每一frame都被呼叫,也就是說大概60fps,這樣能夠保證位置修改的流暢連貫。為了實現這個連貫流暢的目標,整個update process的程式碼必須簡練,並且儘可能地避免直接訪問dom.下面是其工作流程
React, Vuejs等第三方view library的整合
我必須考慮為了支援react或者vuejs需要做些什麼,由於這些view library都會直接做dom操作,那麼popperjs應該做什麼設計的思考呢?v1.0將所有的dom操作都集中到一個modifier中,applyStyle
這將允許使用vuejs,react的使用者簡單地disable掉applyStyle並且替換為vuejs,或者react相容的函式。
Popper.js是如何工作的?
popper.js使用一個reference element(通常是一個button或者一個link)和一個popper element(任何你需要position的元素),popper.js找到這兩個元素的common offset parent,計算reference element相對於這個parent的位置,然後產生出一個座標集用於設定popper元素的position.就這麼簡單。
最困難的地方在於必須考慮到一些邊界條件,比如跨瀏覽器的相容性,box model的能力,必須考慮scrollable element等。簡單的用法:
new Popper(referenceElement, popperElement)
這段程式碼將會把popperElement放置到referenceElement的下發。而且,通過這段程式碼,你就可以訪問所有的內建功能。
1. 如果referenceElement非常靠近了viewport的底部,那麼popperElement將會被定位放置到referenceElement的上面,否則會出現popper部分不可見的問題;
2. 如果這兩個元素位於兩個不同的parents,那麼popper.js也將會對這種情況考慮周全,也能很好的定位放置popper元素。
3. 它能夠有效地處理scrollable elements和頁面的resize場景.
Popper.js和其他類似的解決方案有什麼不同呢?
主要的不同點在於該庫不需要直接操作dom。這有兩個好處:1.他不需要將popper節點移動到另外的context中,比如body的兒子,2.這樣很容易將popper.js整合到react,angular,vuejs等view library中。你可以像下面的程式碼一樣輕鬆地將dom manipulation delegate給vuejs:
new Popper(referenceElement, popperElement, { modifiers: { applyStyle: { enabled: false }, updateReactData: { order: 900, fn(data) { this.setState({ data }); return data; } }, }, });
上面的程式碼中,我們關閉了內建的applyStyle modifier,並且定義了我們客製化的modifier,該函式代理獲取計算出來的popper座標而將座標輸出到react元件中。
既然你具有了關於popper.js的所有knowledge,那麼你就可以對popper element施加任何你想要的樣式。
你可能注意到我的定製modifier返回data物件。這個物件是必須的,因為其他的modifier可能會在這個custom modifier之後執行,它們也需要使用這個data物件。這種鏈條方式的呼叫使得popper.js非常易於擴充套件。你可以注入任何定製化的函式在存續modifer之前或者之後執行,或者關閉部分modifer,或者修正其他的modifer的行為,而這隻需要通過修改這個data物件資料就可以了。