@floating-ui/vue 版本演进解析:从 Vue 2 兼容时代到 Vue 3 原生响应式的升级指南
【免费下载链接】floating-uiA JavaScript library to position floating elements and create interactions for them.项目地址: https://gitcode.com/GitHub_Trending/fl/floating-ui
本文以@floating-ui/vue(Floating UI 的 Vue 官方封装包)的 CHANGELOG.md 为主线,梳理该包从 1.0.3 到 2.0.0 的全部版本变更,并结合仓库源码逐一解释每个变更背后的实现细节。读完本文,你将掌握:2.0.0 破坏性变更的影响范围与迁移路径、MaybeReadonlyRefOrGetter响应式 API 的正确用法、isPositioned与open状态的联动机制,以及组件实例(ComponentPublicInstance)作为 ref 时的边界处理逻辑。
版本总览:一次主版本跃迁与十余次补丁迭代
@floating-ui/vue是 Vue 生态中用于定位浮动元素(tooltip、popover、dropdown 等)的核心库,底层定位算法由@floating-ui/dom提供。其 CHANGELOG 记录了一条清晰的技术演进路线:
| 版本 | 类型 | 核心变更 |
|---|---|---|
| 2.0.0 | Major | 移除vue-demi依赖,终止对 Vue 2 及 Vue < 3.3.0 的支持 |
| 1.1.11 | Patch | 依赖升级:@floating-ui/dom@1.7.6、@floating-ui/utils@0.2.11 |
| 1.1.5 | Patch | 修复useFloating在open为false时错误地将isPositioned置为true |
| 1.1.0 | Minor | useFloating支持MaybeReadonlyRefOrGetter类型参数 |
| 1.0.4 | Patch | 重写isComponentPublicInstance的实现 |
| 1.0.3 | Patch | 导出.d.mts类型文件;组件类型 ref 渲染为空时不再抛错 |
从依赖关系看(见 packages/vue/package.json),该包始终以@floating-ui/dom与@floating-ui/utils为运行时依赖,并通过workspace:^与仓库内其他包保持同步发布,这也是 1.1.x 阶段密集出现"Update dependencies"补丁的原因——底层 DOM 包每有一次修复,Vue 封装层都会跟随发版。
2.0.0 破坏性变更:告别 vue-demi,全面转向 Vue 3 原生响应式
变更内容与动机
2.0.0 是 CHANGELOG 中唯一的 Major 版本,其变更说明非常明确:
breaking: drop the abandoned and soon-to-be-deprecated
vue-demipackage(见 packages/vue/CHANGELOG.md),ending support for Vue 2 and Vue <3.3.0
vue-demi曾是一个桥接库,让同一份代码能同时运行在 Vue 2 与 Vue 3 之上。但由于该包已被原作者弃用(CHANGELOG 中注明了来自 vueuse/vue-demi 的 Deprecation Warning),继续依赖它意味着长期的兼容性风险。移除vue-demi后,@floating-ui/vue的代码可以不再为 Vue 2 保留双份 API 分支,彻底拥抱 Vue 3 的原生响应式体系。
源码层面的印证
peerDependencies中"vue": ">=3.3.0"的约束(见 packages/vue/package.json)是这一变更最直接的落地证据。3.3.0 这个版本门槛并非随意选择——Vue 3.3 引入了全局可用的toValueAPI,而useFloating的实现正建立在它之上。
在 useFloating.ts 中,所有选项参数都通过toValue统一解包:
const openOption = computed(() => toValue(options.open) ?? true); const middlewareOption = computed(() => toValue(options.middleware)); const placementOption = computed(() => toValue(options.placement) ?? 'bottom'); const strategyOption = computed(() => toValue(options.strategy) ?? 'absolute'); const transformOption = computed(() => toValue(options.transform) ?? true);toValue是 Vue 3.3+ 提供的解包函数,能同时处理"普通值、ref、getter 函数"三种形态,这正是 1.1.0 引入MaybeReadonlyRefOrGetter类型的基础。vue-demi时代则必须自行判断isRef再手动.value,两套逻辑的代码复杂度差别明显。
迁移建议
从 1.x 升级到 2.0.0 需要满足两个前置条件:
- 项目必须运行在Vue >= 3.3.0之上;
- 若项目仍基于 Vue 2 或更早的 Vue 3 版本,应停留在 1.1.x 分支,并关注 Vue 版本的升级计划。
除此之外,useFloating的公开 API(参数与返回值)在 2.0.0 中没有额外变更,迁移成本主要集中在运行时环境而非代码改写。
响应式 API 进化:MaybeReadonlyRefOrGetter(1.1.0 / 1.1.1)
类型定义
1.1.0 为useFloating的选项参数引入了一种更灵活的类型。其定义位于 types.ts:
export type MaybeReadonlyRef<T> = T | Readonly<Ref<T>>; export type MaybeReadonlyRefOrGetter<T> = MaybeReadonlyRef<T> | (() => T);这意味着placement、strategy、middleware、open、transform等选项现在支持三种传参形态:
- 普通值:
placement: 'bottom'; - 只读 ref:
placement: toRef(props, 'placement'); - getter 函数:
placement: () => props.placement。
1.1.1 的补丁fix: ensure MaybeReadonlyRefOrGetter works in earlier versions of Vue则是对该类型在更早 Vue 3 版本下 TypeScript 类型推导兼容性的修正,说明这一能力在设计之初就考虑了vue-demi尚未移除时的跨版本可用性。
底层解包逻辑
MaybeReadonlyRefOrGetter之所以能成立,完全依赖toValue的三态解包能力(见 useFloating.ts)。被解包后的值会被包装成computed,并通过watch订阅变化:
watch([middlewareOption, placementOption, strategyOption, openOption], update, { flush: 'sync', });flush: 'sync'保证选项变化后立即触发重定位,而不是等待下一个 tick——对于 Tooltip 这类对出现时机敏感的场景,这一细节直接影响交互手感。
实测验证
仓库测试(packages/vue/test/index.test.ts)为 getter 形态提供了完整的验证用例:
updates floating coords when placement is a getter function(L144-L177):placement: () => props.placement,props 从bottom改为right后,坐标断言从(0, 5)变为(5, 0);updates floating coords when middleware is a getter function(L179-L209):middleware: () => props.middleware变化后,y 坐标从 0 变为 10;updates floating position when strategy is a getter function(L211-L238);resets isPositioned on open change and open is a getter function(L298-L325)。
同时也有回退默认值的用例:placement变为undefined时回退到'bottom'(L362-L389),strategy变为undefined时回退到'absolute'(L391-L418)——这与源码中?? 'bottom'/?? 'absolute'的默认值处理完全一致。
行为修复:isPositioned与open状态的正确联动(1.1.5)
修复动机
1.1.5 的补丁说明为:
fix(useFloating): avoid setting
isPositionedto true whenopenis false
isPositioned用于告知消费者"浮动元素是否已经完成定位",是入场动画(如淡入、位移)的常见触发条件。旧实现存在一个场景缺陷:浮动元素在关闭状态(如退出动画期间)仍保持挂载,此时若因布局变化触发重算,isPositioned会被错误置为true,导致下一次打开时入场动画状态异常。
修复实现
源码 useFloating.ts 的注释完整记录了这一修复的意图:
/** * The floating element's position may be recomputed while it's closed * but still mounted (such as when transitioning out). To ensure * `isPositioned` will be `false` initially on the next open, avoid * setting it to `true` when `open === false` (must be specified). */ isPositioned.value = open !== false;同时reset函数保证关闭时主动复位:
function reset() { if (!openOption.value) { isPositioned.value = false; } }并通过watch(openOption, reset, {flush: 'sync'})在open变化时同步执行。
测试覆盖
测试文件中有三个用例直接验证该行为(packages/vue/test/index.test.ts):
updates isPositioned on open change(L240-L267):open从false切到true时,isPositioned从false变为true;resets isPositioned on open change(L269-L296):open从true切到false时复位为false;does not set isPositioned to true when open is false(L327-L360):即使strategy从absolute变为fixed触发了重定位,只要open为false,isPositioned始终保持false。
组件实例处理:isComponentPublicInstance与空渲染保护(1.0.3 / 1.0.4)
为什么需要 unwrap
Vue 中模板 ref 指向的可能是真实 DOM 元素,也可能是组件实例(ComponentPublicInstance)。@floating-ui/dom的computePosition只接受 DOM 元素或虚拟元素,因此@floating-ui/vue必须先把组件实例解包为真实的$el。这一职责由 unwrapElement.ts 承担:
function isComponentPublicInstance( target: unknown, ): target is ComponentPublicInstance { return target != null && typeof target === 'object' && '$el' in target; } export function unwrapElement<T>(target: MaybeElement<T>) { if (isComponentPublicInstance(target)) { const element = target.$el as Exclude<MaybeElement<T>, ComponentPublicInstance>; return isNode(element) && getNodeName(element) === '#comment' ? null : element; } return target as Exclude<MaybeElement<T>, ComponentPublicInstance>; }1.0.4 的fix: change isComponentPublicInstance implementation正是改进了这里的判断方式。旧实现可能依赖instanceof或框架内部属性,新实现使用'$el' in target的结构化探测,对跨包、跨构建场景更稳健。而unwrapElement中对#comment节点的特殊处理,则对应 1.0.3 的修复do not throw when component type reference or floating renders nothing——组件render返回null时,Vue 会在$el位置插入注释节点,若直接把它当作定位元素传给 DOM 层必然出错,这里将其规范化为null从而静默跳过定位。
测试覆盖
packages/vue/test/index.test.ts 用一组用例完整覆盖了组件 ref 的边界场景:
allows to use with component type reference(L742-L767)与allows to use with component type floating(L769-L794):组件作为引用元素/浮动元素均可正常定位;does not throw when component type reference renders nothing(L796-L816)与对应 floating 用例(L818-L838):render() { return null; }时不再抛错;does not throw when ... "$el" is null(L840-L888):即使通过expose显式暴露$el: null,也保持静默。
类型与发布工程改进:.d.mts与副作用标记(1.0.3)
1.0.3 的另一项变更是chore: exports .d.mts types。从 packages/vue/package.json 可以看到当前完整的 exports 映射:
"exports": { "./package.json": "./package.json", ".": { "import": { "types": "./dist/floating-ui.vue.d.mts", "default": "./dist/floating-ui.vue.mjs" }, "types": "./dist/floating-ui.vue.d.ts", "module": "./dist/floating-ui.vue.esm.js", "default": "./dist/floating-ui.vue.umd.js" } }ESM 导入路径单独提供.d.mts类型声明,配合"sideEffects": false(见 packages/vue/package.json),确保在支持 tree-shaking 的打包器中,未使用的导出可以被安全摇除。构建产物通过rollup.config.mjs产出 UMD、ESM 与 mjs 三种格式,覆盖 CDN 直接引入、打包器 ESM 导入等不同使用场景。
依赖升级节奏:1.1.2 至 1.1.11 的密集补丁
从 1.1.2 到 1.1.11,@floating-ui/vue连续发布了 10 个 Patch 版本,绝大多数变更都是跟随底层依赖发版:
| 版本 | 依赖升级 |
|---|---|
| 1.1.11 | @floating-ui/dom@1.7.6、@floating-ui/utils@0.2.11 |
| 1.1.10 | @floating-ui/dom@1.7.5 |
| 1.1.9 | @floating-ui/dom@1.7.4 |
| 1.1.8 | @floating-ui/dom@1.7.3 |
| 1.1.7 | @floating-ui/utils@0.2.10、@floating-ui/dom@1.7.2 |
| 1.1.6 | @floating-ui/utils@0.2.9 |
| 1.1.5 | @floating-ui/utils@0.2.8(另有 isPositioned 修复) |
| 1.1.4 | @floating-ui/utils@0.2.7 |
| 1.1.3 | @floating-ui/utils@0.2.6 |
| 1.1.2 | @floating-ui/utils@0.2.5 |
| 1.1.1 | 修复MaybeReadonlyRefOrGetter在更早 Vue 版本的兼容性 |
这种跟随发版模式源于 packages/vue/package.json 中的 workspace 依赖声明:@floating-ui/dom与@floating-ui/utils均以workspace:^引用,仓库内任一底层包的修复都会同步到 Vue 封装层。对使用者而言,这意味着升级@floating-ui/vue的补丁版本即可自动获得 DOM 定位引擎的所有修复,无需手动锁定底层包的版本。
升级决策速查
综合 CHANGELOG 与源码,可以给出如下版本决策参考:
- Vue >= 3.3.0 的新项目:直接使用
2.0.0,享受无vue-demi的纯净 Vue 3 原生实现; - 仍停留在 Vue 2 / Vue 3 < 3.3.0 的项目:锁定
1.1.x最新补丁版本,此时 1.1.0 的MaybeReadonlyRefOrGetter、1.1.5 的isPositioned修复、1.0.3/1.0.4 的组件实例处理均已可用; - 正在使用
1.0.x的项目:至少升级到 1.1.x,以获得 getter 形态的响应式选项、isPositioned关闭态修复,以及更完善的组件 ref 边界处理; - 所有版本:
useFloating的调用方式保持一致——传入reference、floating两个模板 ref 与选项对象,接收x、y、placement、strategy、middlewareData、isPositioned、floatingStyles与update(见 types.ts),2.0.0 未改变这些公开契约。
如需进一步查看实现细节,可深入阅读 useFloating.ts(定位核心)、types.ts(全部公开类型)、unwrapElement.ts(组件实例解包)、arrow.ts(箭头中间件封装)以及 test/index.test.ts(全部行为验证用例)。
【免费下载链接】floating-uiA JavaScript library to position floating elements and create interactions for them.项目地址: https://gitcode.com/GitHub_Trending/fl/floating-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考