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对象数据就可以了。