Skip to content

ADR 0008:Vue 2.7 适配


背景

主线(Vue 3)依赖 Teleport 同构树、createApp + appContext Host 桥接、MaybeRefOrGetter / toValue、flat onUpdate:* 等。Vue 2.7 有 Composition API,但缺 Teleport / Fragment / createApp,且 h、实例字段、默认 v-model 不同。

已定降级(见 §已决):2.7 不做 Teleportcontainer 直接包 content;换容器时因嵌套 diff 自然连带 remount content(非另定策略)。其余与 Vue 3 同类问题 行为对齐(D0.18)。清单项均已拍板;下文保留选项原文供追溯。

可行性(实现前评估):在上述降级与 D0.19–D0.22 工程约束下 可实现。分叉面集中在挂载 / 视图树 / h 翻译;主要不确定性在「同包 dist 导入安全」与 Host / defineLayer 读路径,用 D0.17 / D0.22 关键路径 CI 锁定(见 §风险与缓解)。

工程前置落地 ADR 0009(Vue 3 集成 → tests-vue3、只消费 dist),实现本篇 runtime 与 tests-vue2。本篇 搬迁现行 tests/integration(避免与 Vue 2 兼容搅在同一变更集)。


已决

ID决策说明
D0.1计划支持 Vue 2.7,状态 待适配未实现前不改 peerDependencies、不宣称可用
D0.2不做 Teleport(不自研 portal、不引 portal-vueh(container, …, { default: () => h(content) })
D0.3换容器后 content 会丢(相对 Vue 3 park)不是另做 remount API:嵌套树下 container.component 一变,Vue 2 diff 自然拆掉子树;openId 策略与 Vue 3 相同(D2.2 / D2.3)
D0.4Vue 3 主线 为 2.7 削弱 Teleport / park双端行为允许分叉
D0.5LayerNoContainer 在 2.7 维持同构 Teleport 树;props 全投影(D2.1-A)扁平 h(content),但 container props→content 与 Vue 3 同语义
D0.6容器 model / 更新事件按大版本(D4.1 / D4.2)见下表;默认 model 与 flat 更新键由 compat 注入(D0.20),非在 bind 内写 isVue2
D0.7同包同 API;适配首发预定 1.1.0(D1.1 / D1.2)peer: ^2.7 || ^3.3;公开函数名/签名一致;runtime 按 Vue 大版本分叉
D0.8Vue 2.7 挂载 / Host:宿主 Vue.extend + parent(D3.1–D3.3)provide/inject + 全局能力;对齐现行「一次 LayerApp、visible 显隐」;Host 窄接口见 D0.21
D0.9toValue / MaybeRefOrGetter 库内 polyfill(D5.1-A)语义对齐 ADR 0003;不依赖 @vueuse/core;不削弱 getter
D0.10defineLayer 内容根标记(D5.4-A)Vue 2.7:标在占位 vnode 的 data[LAYER_CONTENT];读 $vnode.data;不进 props/attrs
D0.11h / 事件平台适配落点(D4.3 / D8.2-C)bind-* 仍产出 Vue 3 flat;仅在 createLayerViewVNode / createLayerApph 前)翻译;不做 model/valueinput 二次推导(已在 D0.20 flat 键上体现)
D0.12源码组织:compat/vue2 + compat/vue3 + 聚合层 isVue2(D8.1)分端实现;业务只依赖聚合导出;不散落 isVue2;零运行时额外依赖(D8.3-B);导入安全见 D0.19
D0.13首期公开 API 全做;同名同形;行为差仅文档(D7.1 / D7.2-A)不砍能力、不子路径、不因版本 throw;见下「对外可感知差」
D0.14公开类型:同包一份 .d.ts + 本地 shim(D1.3-C)不双 types 入口;MaybeRefOrGetter 等自声明;只用两端共有的 Vue 类型名
D0.15官方 2.7 可运行示例暂不做(D6.1)首期以差异文档为准;playground-vue2 / Element UI demo 延后
D0.16容器 default 插槽:与 Vue 3 相同,无特殊处理(D2.4)假定容器会渲染 default;不做 fallback / 不强制关 append-to-body
D0.17CI / 文档 / EOL(D1.4-C / D1.5 / D1.6-A)Vue 2.7 关键路径 job(D0.22);一篇 Vue 2 兼容说明;best-effort 无 SLA
D0.18与 Vue 3 同类问题:行为对齐现行凡非 2.7 专有缺口(Teleport/model 默认/Host 挂载形态等已另决),实现与用户体感跟 Vue 3 一致;见下表
D0.19同包 dist 导入安全禁止对 Vue 3-only API 静态 named import;见下「D0.19」
D0.20默认 model / flat 更新键由 compat 注入澄清 D0.6 + D0.11:bind 仍 flat、不分叉文件;valueinput 体现在 flat 键
D0.21Host 经 compat 窄接口业务不直触 appContext / 2.7 raw 内部实例;配套 D0.8 / D3.5
D0.22Vue 2.7 测试:独立 workspace 包 tests-vue2只消费 vue-layerx dist;关键路径 + 入口冒烟;在根包双挂 vue@2;前置 ADR 0009

D0.18 对齐清单(原待决策项一次性收口)

ID决议(与 Vue 3 相同 / 实现适配不改语义)
D2.5无额外 wrapper;单根即 container / NoContainer 时 content
D2.6对外仍收「返回 VNode 的函数」;2.7 仅在 compat/h 侧转 slots
D3.4不专项处理 DevTools(parent 关联即可,同「不另搞一套」)
D3.5适配层封装 setup 实例探测(proxy / mounted);语义同现 bindHost(窄接口 D0.21
D3.7SSR:无 document 不 mount
D3.8挂载点:document.body
D4.4ref:仅 function / Ref;字符串忽略
D4.5不强制拆 attrs;尊重组件 inheritAttrs
D4.6异步等 content 支持范围同 Vue 3 文档/测试意图
D5.2类型本地处理;不因 emits 改用户语义
D5.3继续 Symbol InjectionKey
D5.5文档面向 2.7+;ADR 0007 静态 define 两端都不做
D6.2append-to-body 双挂:接受,与 Vue 3+Element 相同;兼容篇可提一句
D6.3仅 Vue 2.7+(不对标 composition-api+2.6)
D7.3keep-alive:同现文档(deactivated 不自动卸;Host 卸才 dispose)

D0.14 类型

  • 发布仍一个 "types": "./dist/index.d.ts";用户 import … from 'vue-layerx'要求 vue-layerx/vue2用 postinstall 改 types。
  • 对 Vue 3.3+ 才有的类型(如 MaybeRefOrGetter)在库内 本地声明 / shim(与 D0.9 polyfill 配套);公开表面优先依赖两端都有的 ComponentComponentPublicInstanceVNodeRefComputedRef 等。
  • 否决types 入口、typesVersions 扮 Vue 分支、自定义 exports condition。
  • 内部 Host / appContext 等不进入精确公开类型(放宽或仅实现侧使用;与 D0.21 窄接口一致)。

D0.13 API 范围与对外差异

首期 2.7 全部提供与 Vue 3 同名的公开能力:createLayer / useLayer / open / close / confirm / clone / bindHost / defineLayer / LayerTemplate / adapter / live 配置 / LayerNoContainer / SSR(无 DOM 跳过 mount)/ 打开后换容器(允许,自然 remount)。

日常用法(open / close / confirm / defineLayer / 插槽模板 / Host provide)在正确配置 model 与容器组件的前提下,对外体感应基本一致。仍建议文档点明的差主要是:

场景是否常踩说明
打开后改 container.component少见(进阶)Vue 3 可 park content;2.7 嵌套 diff 会丢 content state(D0.3)
默认 model换 UI 库时3→modelValue,2.7→value(D0.6);Element UI 等需显式 model: 'visible'
内部树形(有无 Teleport)通常无感Dialog 自身常 append-to-body;用户仍 open()

做:专用子路径、对「同名 API」按版本 throw、首期砍 LayerTemplate 等。行为差 只写文档 / changelog,不加强制 runtime 警告(可选日后加)。

D0.12 compat 结构

text
src/
  core/                 # fragment、store、bind-*、instance 编排等(无 isVue2)
  compat/
    env.ts              # isVue2(读 vue.version / 约定探测;不引 vue-demi)
    vue3/               # 现行 createLayerApp、createLayerViewVNode、toPlatformVNodeData(identity)…
    vue2/               # extend+parent、嵌套树、toPlatformVNodeData、data[LAYER_CONTENT]…
    index.ts            # 聚合:createLayerApp = isVue2 ? vue2 : vue3(只选函数引用)
  api/                  # 只 import compat 聚合,不直连 vue2/vue3

约束:

  • 分端实现,禁止单文件内大段 if (isVue2) { … } 交织业务。
  • 聚合层 静态绑定一次导出;热路径不再反复判断。
  • 同包一份 dist 可同时带上两套 compat(库小可接受);若日后要抠包体,再在构建期把 isVue2 define 成常量做 DCE(非本期必须)。
  • 引入 vue-demi / portal-vue(D8.3-B / D0.2)。
  • 导入安全见 D0.19:同包带两套 compat 可行,但 dist 不得让 Vue 2.7 消费路径静态 named import createApp / Teleport / toValue 等 Vue 3-only 符号。

D0.11 h 适配落点

  • 库内调 h( 的只有 createLayerViewVNode(container/content)与 createLayerAppLayerView + 挂载)。
  • bind-container-model / bind-close-on 不维护两套文件:继续产出 Vue 3 flat props(onDoneonUpdate:… / 在 D0.20 下也可能是 onInput),可作共享 core;在 bind 内写大段 if (isVue2)
  • D0.20 配合:默认 model 与「应产出的 flat 更新键」已由 compat 选好;本层 做 flat props / slots / ref / LAYER_CONTENT → 平台 h data,再二次猜测 value / input
  • Vue 2.7:在 h 前把 flat props 拆成 { props, on, scopedSlots, … }(建议抽共用 toPlatformVNodeData);顺带处理 refLAYER_CONTENTdata、slots 形态(D2.6)。
  • Vue 3:翻译层 identity,避免误伤现行路径。
  • .native 另开配置(首期只保证组件自定义事件)。

D0.8 挂载与 Host(Vue 2.7)

对齐现行 createLayerApp / bindHost 语义,不用 Vue 3 的 createApp + appContext 桥接。公开仍只有 bindHost();内部经 D0.21 窄接口,业务不直触 appContext

步骤做法
取 HostbindHost 在 setup 同步调用:经 compat 取 setup proxy(Options 的 this)。不要把 Vue 3 internal / 2.7 raw 内部实例当 parent
取构造器hostProxy.$root.constructor(实现时可再评估 hostProxy.constructor,即宿主应用那份 Vue;避免库内 import Vue from 'vue' 与应用双副本导致全局组件 / prototype 丢失
创建根const LayerCtor = HostVue.extend(LayerApp)一次;不要每次 openextend
挂载instance = new LayerCtor({ parent: hostProxy })$mount()(或挂到预置 el)→ 插入 document.body(与现行一致,见 D3.8)
卸载$destroy() + 摘除 DOM,对齐 app.unmount()
provide/inject仅靠 parent: hostProxy_provided 链;手拷 _provided
全局能力$message / Vue.use / 全局组件随 宿主构造器上的 extend 继承;逐项抄 prototype
显隐LayerApp 长期存活,用外部 reactive/visible 驱动;不是每次 opennew+extend(与「动态组件每次 mount」示例不同)
无 Host与 Vue 3 对齐:允许挂载 / open,此时无 parent(无页面 provide);全局仍取决于所用 Vue 构造器(无 Host 时回退策略实现定,须测)
晚绑定 / Host 卸下次 open 才 bake Host(开着换 Host 不热更新 provide / 不 destroy);Host onUnmounted → dispose 弹层

示意(非最终源码):

js
const hostProxy = getCurrentInstance().proxy // bindHost 时经 compat 保存
const HostVue = hostProxy.$root.constructor
const LayerCtor = HostVue.extend(LayerApp)

// createLayerApp.mount(首次需要 DOM 时)
const instance = new LayerCtor({ parent: hostProxy })
instance.$mount()
document.body.appendChild(instance.$el)

// unmount
instance.$destroy()
instance.$el.remove()

约束:

  • 双副本 vue 时注入链可能仍在、全局却丢——文档提醒应用 dedupe;库侧坚持宿主构造器 extend
  • DevTools:靠 parent 关联;不做 Vue 3 那套 appContext.app 特判(D3.4,接受多根)。
  • isMounted / _isMounted、proxy vs internal 等差异由 compat 封装(D0.21 / D3.5),须测。

D0.7 发布

  • 包名仍为 vue-layerx;用户 import { createLayer, … } from 'vue-layerx',Vue 2 / 3 调用面一致
  • 实现:compat/vue2 + compat/vue3 分端 + 聚合层 isVue2D0.12);另发 vue-layerx-vue2vue-demi 为依赖。
  • 版本:适配合并后发 1.1.0(相对现行 1.0.x 的 minor);changelog 写明 2.7 支持与行为差(无 Teleport、换容器 remount、model 默认等)。未实现前 package.json 版本与 peer 暂不改

D0.6 容器显隐绑定

Vue默认 modelprop更新事件
3modelValuemodelValue(或显式 model始终 update:${model}(flat:onUpdate:${model}
2.7valuemodelmodel === 'value'input(flat:onInput);否则update:${model}(如 visible + update:visible
text
# Vue 3(现行)
modelValue  +  update:modelValue

# Vue 2.7
默认:     value   +  input
其它显式 model(如 visible / open): visible + update:visible、open + update:open

用户仍可用 createLayer(Dialog, { model: '…' }) / tier 覆盖改 prop 名;事件按上表由 model 推导,另开 event 配置项(除非将来修订)。

实现分工(与 D0.11 / D0.20 一致,避免前后打架)

  • compat 导出 DEFAULT_CONTAINER_MODELtoModelUpdateProp(model)(或等价名)。
  • bindContainerModel / bindLayer 只消费上述注入值,按传入的 model + flat 键写 props;isVue2
  • toPlatformVNodeData 只做 flat → Vue 2 { props, on, … }再把 onUpdate:value 特判成 input
text
# Vue 2.7 LayerView(目标;对外仍是「slots 对象」心智,compat 再转 scopedSlots)
h(container, /* 经 toPlatformVNodeData 后的 data */, {
  default: () => h(content, …),
})

D0.19 同包 dist 导入安全

同包一份 dist 可同时含 compat/vue2 + compat/vue3(D0.12),但若模块图里对 Vue 3-only API 做静态 named import,Vue 2.7 应用会在解析 / 打包阶段失败。

约束:

  • named import 两端交集 API(如 h / ref / watch / defineComponent / getCurrentInstance / provide / inject / computed / reactive / shallowRef / onUnmounted 等)。
  • Vue 3 专有能力(createApp / Teleport / …)仅在 compat/vue3 内通过 import * as Vue from 'vue'(或等价)再取属性;缺失则为 undefined,且 不得被 Vue 2.7 热路径执行到。
  • toValue vue named import:走库内 polyfill(D0.9);MaybeRefOrGetter 走本地类型 shim(D0.14)。
  • 否决:依赖「未执行的分支 bundler 不会解析」、或假定 Vue 2.7 会 re-export 这些符号。
  • CI(D0.17 / D0.22):在 tests-vue2 用 Vue 2.7 解析 / 打包库入口做冒烟,防止回归。

D0.20 默认 model / flat 更新键注入

澄清「D0.6 按大版本选事件」与「D0.11 bind 仍 flat、不分叉文件」:

导出(compat 聚合)Vue 3Vue 2.7
DEFAULT_CONTAINER_MODEL'modelValue''value'
toModelUpdateProp(model)'onUpdate:' + modelmodel === 'value''onInput';否则 'onUpdate:' + model
  • core 的 bindLayer / bindContainerModel 从聚合层取上述值;产出仍是 flat props。
  • 维护两套 bind-* 文件;在 bind 内散落 isVue2
  • toPlatformVNodeData承担 valueinput 语义(已在 flat 键上体现)。

D0.21 Host 窄接口

公开 API 仍只有 bindHost()。内部 Host 经 compat 统一窄接口(名称实现自定),例如:

  • getSetupInstance() / hasSetupContext() / onHostUnmounted(cb) / createLayerApp 所用挂载原语。

约束:

  • 禁止业务 / instance 编排层直接依赖 Vue 3 的 appContextComponentInternalInstance.provides,或把 2.7 raw 内部实例当 parent
  • Vue 3:现行 createApp + provide 桥接(可继续活在 compat/vue3)。
  • Vue 2.7:proxy + parent + $destroy(D0.8)。
  • 与 D3.5 一致:探测失败则 warn 并当无 Host(语义同现行)。

D0.22 Vue 2.7 测试 harness(独立包)

ADR 0009 同构:集成 / 兼容线只消费 dist,用独立 workspace 包隔离 Vue 大版本解析;覆盖率另开 src-alias 趟(与 tests-vue3 双跑对称)。

职责依赖要点
vue-layerx库 + unitsrc/**/__test__,测源码)vue@3(dev);不变
tests-vue3Vue 3 全量集成(ADR 0009)vue-layerx: workspace:* + vue@3 + @vue/test-utils@2
tests-vue2Vue 2.7 关键路径 + 入口解析冒烟(本篇)vue-layerx: workspace:* + vue@2.7 + @vue/test-utils@1

双跑:

配置作用
门禁tests-vue2/vitest.config.ts只消费 dist;CI / prepublishOnly
覆盖率vitest.vue2-coverage.config.tsvue-layerxsrcvue 钉到 tests-vue2 的 2.7;代替门禁

约束:

  • 前置:ADR 0009 已合并(tests-vue3 存在且集成只 import from 'vue-layerx')。本篇实现 负责把现行 tests/integration 迁出根包。
  • 否决根包同时安装 vue@2 + vue@3、或靠 npm alias / Vitest 全局改写 vue 解析跑双端(易串版)。
  • tests-vue2 追求与 tests-vue3 全量对等;覆盖 mount / Host / 换容器自然 remount / bind·model(默认 value)/ defineLayer 内容根 + D0.19 入口冒烟即可。
  • 流水线:pnpm build → 根 unit → tests-vue3tests-vue2(后两步均依赖 dist)。
  • Coverage:unit + vue3-alias + vue2-alias → merge;CI 设 100% threshold。
  • 用例按 @vue/test-utils@1 API 新写;不要指望把 Vue 3 集成原样改 alias 就能绿。

待决策问题清单(追溯用;决策已全部并入 D0)

填写约定:每项选一个选项(或写「自定义」);实现前把「决策」列填满。建议优先级:先 D1 交付形态D3 Host/挂载D4 事件/v-model → 其余。

现状:选项原文保留供追溯;决策列已填满并并入 D0.1–D0.18。实现评估后追加的工程约束为 D0.19–D0.22(导入安全、model/更新键注入、Host 窄接口、tests-vue2 独立包);Vue 3 集成迁包见 ADR 0009(本篇前置,不在此实现)。

D1. 交付与工程

D1.1 发布形态

选项内容
A同包条件导出(vue-layerx + exports / 自动按 vue 版本)用户装一个包构建/类型复杂;误用风险
Bvue-demi 同仓条件编译经典双端套路Teleport 分叉仍要两套 view;demi 维护成本
C独立包 vue-layerx-vue2(或 scope 子包)主线干净;版本可独立两份发布、文档分叉
D同仓 packages/vue2,npm 仍一个名字不同 tag(如 @vue2源码一仓tag/发布流程要定
  • 决策A + 同 API 运行时兼容(并入 D0.7)— 同包;公开函数一致;按 Vue 大版本选 runtime。否决独立包名(C)与 vue-demi 依赖(B)。

D1.2 版本号与 semver

选项内容
A与 Vue 3 同版本号,changelog 分区写 2.7 差异
BVue 2 包独立 major/minor(如 0.x2.x),不跟主线齐步
C主线升 major 才加 2.7 peer(^2.7 || ^3),此前独立预发
  • 决策A,首发 1.1.0(并入 D0.7)— 与 Vue 3 同版本号;changelog 分区写 2.7 差异。

D1.3 TypeScript 类型从哪来

选项内容
A依赖 vue@2.7 自带类型
B双端各自 types 入口(dist/index.d.ts vs dist/vue2.d.ts
C共享业务类型,Vue API 类型用条件类型 / 本地 shim(MaybeRefOrGetter 等)
  • 决策C(并入 D0.14)— 同包一份声明 + 本地 shim;否决双 types / postinstall 切 types。

D1.4 测试矩阵

选项内容
ACI 双矩阵:Vue 3(现行)+ Vue 2.7(@vue/test-utils@1
B仅 Vue 3 CI;2.7 本地/手工 + 少量冒烟
C2.7 单独 job,覆盖 runtime 关键路径(mount / Host / 换容器 remount / bind)
  • 决策C(并入 D0.17 / D0.22)— 保留 Vue 3 测试(unit 在根;集成在 tests-vue3,见 ADR 0009);另加 workspace 包 tests-vue2 跑 Vue 2.7 关键路径(mount / Host / 换容器自然 remount / bind·model / defineLayer 内容根)+ 库入口解析冒烟(D0.19)。在根包双挂 vue@2追求与 Vue 3 集成全量对等。实现本篇前须先完成 ADR 0009。

D1.5 文档与 demo

选项内容
A官网 VitePress 仍 Vue 3;另开「Vue 2.7」差异页 + 外链 playground
B同仓 playground-vue2(Vite + vue2 plugin / webpack)
C文档只写差异表,不提供可运行 2.7 demo
D一篇「Vue 2 兼容」指南即可(无 playground)
  • 问题:现有 DemoBlock / Element Plus 示例无法在 VitePress(Vue 3)里跑 2.7。
  • 决策D(并入 D0.17;与 D0.15 一致)— 提供一篇 Vue 2 兼容说明(差异、model 默认、换容器、Host);不做可运行 2.7 demo / playground。

D1.6 Vue 2 EOL 与支持窗口

选项内容
A明确「best-effort / 社区维护」,无 SLA
B定支持截止日期(如发后 N 个月或 Vue 2 安全通告后停更)
C与主线同生命周期,bugfix 都接
  • 决策A(并入 D0.17)— 最大努力(best-effort),无 SLA;不承诺与主线同寿。

D2. 视图树与 NoContainer(在已决 D0.2/D0.3 之上)

D2.1 LayerNoContainer 时 props 投影

Vue 3:NoContainer 仍走 Teleport 树,把 container.props(含 model)投影到 content。

2.7 扁平 h(content) 时:

选项内容
A同样把 merge 后的 container props(model / onUpdate / create 默认)merge 进 content props
B不投影;用户必须在 content 内自己接 visible / 关层
C仅投影 model 相关键,其它 container props 丢弃
  • 影响:A 最接近 Vue 3 用户体感;B 更简单但破坏「无容器仍命令式显隐」;单体 + NoContainer(ADR 0001)强依赖投影语义。
  • 决策A(并入 D0.5)— 全投影,与现行 Vue 3 一致:NoContainer 时把 merge 后的 container.props(含 model / 更新监听 / create 默认等)merge 进 content props。树形扁平,投影语义不变。

D2.2 换容器 remount 的触发面

选项哪些变化触发整树 remount
A container.component 引用变化
Bcontainer.component NoContainer ↔ 普通容器 切换
C任意导致「根 vnode type」变化都 remount(含误把异步组件每次新函数)
D与 Vue 3 配置/合并语义一致;不另定触发表。因无 Teleport、content 在 container 子树内,container.component(含↔ NoContainer)类型变化时 patch 自然 remount 整棵子树
  • 决策D(澄清 D0.3)— 不引入单独的「换容器 remount 策略」。行为差只来自树形:Vue 3 Teleport 可 park content;Vue 2.7 嵌套则 diff 连带刷 content。触发面 = 任何使容器 vnode type 变化的 container.component 更新(与现在改 component 的时机相同)。

D2.3 openId / key 策略

Vue 3:openIdvisible false→true 时递增,作 content 的 key;换容器靠 Teleport park,另涨 key。

选项内容
A2.7:key = openId 只在 visible false→true 递增;换容器另用 containerKey 迫使整树重建
B换容器与重新 open 共用同一 key 空间(任一变化都 ++)
C不做稳定 key,依赖 Vue 2 默认原地 patch(不推荐,换容器可能状态错乱)
D与 Vue 3 相同:仅 openId(false→true ++);为换容器加 containerKey——换容器重刷靠父组件 type 变化的自然 diff
  • 决策D(与现行一致)— watch(visible) 逻辑对齐 layer-view.ts;不为 2.7 单独发明 key 空间。

D2.4 容器 default 插槽被 UI 库吃掉 / 不渲染

部分 Dialog 用 append-to-body 把面板挪到 body,default 仍在组件内部——嵌套 content 一般仍 OK。若容器 不渲染 default(只渲染具名插槽):

选项内容
A文档约束:容器必须渲染 default;否则不支持
B检测无锚点则 fallback 把 content 挂到 LayerApp 根(行为分叉)
C2.7 要求容器关闭 append-to-body,避免双层传送门心智
  • 决策与 Vue 3 相同(并入 D0.16)— 内容放进容器 default(2.7 为直接嵌套);做无 default 时的 fallback,因本问题强制关 append-to-body。怪容器不渲染 default 时两端同样不可用,文档可顺带一句约束即可(非 2.7 专有决策)。

D2.5 Vue 2 无 Fragment:LayerApp / LayerView 包装节点

选项内容
ALayerView 单根即 container(或 NoContainer 时 content)——无额外 wrapper
B外层固定 <div class="vue-layerx-root">,便于挂 class / 测查询
C注释节点 / 空标签技巧(脆弱,不推荐)
  • 决策与 Vue 3 相同(并入 D0.18)— 无额外 wrapper。

D2.6 插槽函数形态(LayerTemplate / config.slots)

Vue 3:slots: { footer: () => VNode }。Vue 2 scoped slot 数据形态不同(slot-scope / v-sloth 的 children 规范不同)。

选项内容
A内部适配层:对外仍收「返回 VNode 的函数」,渲染时转成 2.7 children / scopedSlot
B2.7 公开 API 改为 Vue 2 风格 slot 对象,与 Vue 3 类型分叉
C首期 不支持 LayerTemplate / 具名 slots 配置,只支持 default content
  • 决策A(并入 D0.18)— 公开 API 同 Vue 3;仅 compat/h 侧转换。

D3. 挂载与 Host

D3.1 独立实例怎么造

选项内容
Anew Vue({ parent, render }).$mount(el) + $destroy()
BVue.extend 出构造器再 new
C挂到隐藏组件的 $root 下,不独立 Vue 实例
  • 决策B(并入 D0.8)— 宿主 Vue.extend(LayerApp)new$destroy() 卸载。否决挂到 $root 下不独立实例(C)。

D3.2 provide / inject 继承

选项内容
Aparent: hostProxy(经典,继承 _provided
Bparent + 手动拷贝 / 原型链 _provided(防 host 卸载后仍打开?)
C不继承;文档要求用户自备配置(无 Host 上下文)
  • 问题:无 Host 时 Vue 3 仍能挂到 body,只是没有 provide——2.7 是否同样允许?
  • 决策A(并入 D0.8)— parent: hostProxy;不手拷 _provided无 Host:允许挂 / open(无 provide,与 Vue 3 对齐)。

D3.3 全局能力继承($message、Element locale、prototype 插件)

parent 不会自动带上 Host 所在根的全部 Vue.prototype 扩展方式差异,且独立 new VueVue 构造器是模块里那份。

选项内容
A只保证 provide/inject;原型 / 全局组件 桥接(文档写清)
B从 host 根读取 constructor / options.components / prototype 做有限桥接
C要求用户 Vue.use 的插件在「创建 Layer 的同一 Vue 构造器」上,库用该构造器 new
  • 影响:Element UI 主题、$t$message 是高频坑。
  • 决策C(并入 D0.8)— 用 hostProxy.$root.constructor.extend(...)(与应用同一 Vue);不靠抄 prototype 列表(否决纯 A;B 的「有限桥接」不作主路径)。

D3.4 DevTools / 多根实例

Vue 3 曾特判 appContext.app 避免 DevTools 挂错树。

选项内容
A2.7 不处理 DevTools;接受多个 Vue 根
B设置 parent 后尽量让 DevTools 归到主应用下
CVue.config.devtools 相关专项测试后再定
  • 决策与 Vue 3 相同意图(并入 D0.18)— 不专项处理 DevTools;靠 parent 关联即可。

D3.5 bindHost 实例探测

Vue 3:getCurrentInstance() + isMounted。2.7 的 setup 实例是 proxy,字段名可能是 _isMounted,且 getCurrentInstance() 在 2.7 与 3 行为有历史差异。

选项内容
A适配层统一 getSetupInstance(),封装 isMounted / proxy / raw
B要求用户只在 setup / <script setup> 调;探测失败则 warn 并当无 Host
C暴露 bindHost(vm) 显式传入 Options API this
  • 决策A(并入 D0.18;窄接口见 D0.21)— 适配层对齐现 bindHost 语义(setup 同步、已挂载拒绝等);不新增公开 bindHost(vm) 除非日后需要。

D3.6 Host 卸载与晚绑定

现行:Host onUnmounted → dispose;打开时若 Host 变更则 bump key。

选项内容
A对齐 Vue 3:Host 卸则 unmount 弹层;晚 bindHost 下次 open 生效
BHost 卸只断 provide,弹层可继续(易成孤儿)
C无 Host 时禁止 open
  • 决策A(并入 D0.8)— Host 卸则 dispose 弹层;晚 bindHost 下次 open 生效。

D3.7 SSR

选项内容
A与 Vue 3 相同:无 document 不 mount;客户端再 open
B2.7 路径声明 不支持 SSR
C提供 onMounted 自动 defer 的包装(库内)
  • 决策A(并入 D0.18)— 与 Vue 3 相同。

D3.8 挂载点 DOM

选项内容
A继续 document.body.appendChild 空 div(现行)
B可配置 mountTo(对齐部分 UI 库)
C若容器自身 append-to-body,库改为挂到 Host 旁,避免双重 body
  • 决策A(并入 D0.18)— 与 Vue 3 相同,挂 document.body

D4. h、事件、v-model、attrs

D4.1 默认 container.model

选项内容
A2.7 默认 'visible'(Element UI Dialog)
B仍默认 'modelValue',文档强制 createLayer(Dialog, { model: 'visible' })
Cvue 大版本自动选默认(3→modelValue,2.7→value,与平台默认 v-model 对齐)
D无默认,缺 model 则 dev 抛错
  • 决策C(并入 D0.6;实现见 D0.20)— Vue 3 → modelValue;Vue 2.7 → value;Element UI 等用 visible 时显式 model: 'visible'。由 compat 注入 DEFAULT_CONTAINER_MODEL,bind 不读 isVue2

D4.2 更新事件命名(bind-container-model)

Vue 3:props['onUpdate:visible']。Vue 2 .syncon: { 'update:visible': fn },另有人用 input + value

选项内容
A内部按 model 分支:valueinput,其它→update:${model};Vue 3 始终 update:${model}
B只支持 update:${model}(文档要求 .sync / update:visible
Cmodel 同时配置 prop + event(公开扩展 config)
  • 决策A(并入 D0.6;实现见 D0.20)— Vue 3:flat onUpdate:${model};Vue 2.7:model === 'value' 时 flat onInput(对应 input),否则 onUpdate:${model}。不采用公开 event 字段(否决 C)。toPlatformVNodeData 不再二次特判。

D4.3 closeOn / 普通 listeners(bind-close-on)

Vue 3:onDone camelCase 进 props。Vue 2:on: { done: fn }nativeOn

选项内容
A适配层按版本输出正确 h data
B2.7 只支持组件自定义事件(非 .native
C另支持 nativeOn 表(配置扩展)
  • 决策A(并入 D0.11)— 适配发生在 h 前(createLayerViewVNode / createLayerApp),把 flat onXxx 转成 Vue 2 on: { … }。首期只保证组件自定义事件(非 .native);不另开 nativeOn 配置(否决 C 作首期范围)。model/valueinput 不在本层处理(D0.20)。

D4.4 ref 回调 / 字符串 ref

现行:只支持 function / Ref,字符串 ref warn 忽略。

选项内容
A2.7 保持相同策略
B2.7 额外支持字符串 ref 写到子实例 $refs(通常无 Host 控制权,意义不大)
  • 决策A(并入 D0.18)— 与 Vue 3 相同。

D4.5 inheritAttrs / 透传

Vue 2/3 对 fallthrough 略有差异;NoContainer inheritAttrs: false

选项内容
A保持组件自己的 inheritAttrs;库不强制
Bbind 时显式拆 props vs attrs,避免 class/style 进错地方
  • 决策A(并入 D0.18)— 与 Vue 3 相同。

D4.6 异步 / 函数式组件作 content

选项内容
A文档声明与 Vue 3 相同支持范围,测异步组件
B2.7 首期仅 SFC / 普通选项组件
C函数式组件单独测,不行就文档排除
  • 决策A(并入 D0.18)— 与 Vue 3 相同。

D5. Composition API / 类型 polyfill

D5.1 toValue / MaybeRefOrGetter

选项内容
A库内 polyfill(unref + 函数则调用)
Bpeer 依赖 @vueuse/coretoValue
C2.7 公开 API 接受 getter,只 plain / Ref(削弱 ADR 0003)
  • 决策A(并入 D0.9)— 库内 polyfill:toValue(source) = 函数则调用否则 unref;类型侧本地声明 MaybeRefOrGetter(可与 D1.3 共用)。否决 vueuse(B)与削弱 ADR 0003(C)。

D5.2 SlotsType / emits 选项

LayerTemplate 用了 SlotsTypeLayerView 用了 emits: { 'update:visible': … }

选项内容
A类型侧去掉 / 本地声明;runtime emits 在 2.7 忽略或改 model 选项
B2.7 用 model: { prop, event } 声明 LayerView 显隐
  • 决策A(并入 D0.18)— 不改变用户可见语义。

D5.3 InjectionKey + Symbol

选项内容
A继续 Symbol key(2.7 provide/inject 支持)
B改字符串 key 以免跨副本 Symbol 不一致
  • 问题:双 Vue 副本或重复打包时 Symbol 不共享会导致 defineLayerexists === false
  • 决策A(并入 D0.18)— 与 Vue 3 相同用 Symbol;双副本问题两端共有,文档提醒 dedupe。

D5.4 defineLayer 内容根判定

Vue 3:vnode.props 上挂 LAYER_CONTENT Symbol。2.7 VNode 是 data / componentOptions

选项内容
A标在占位 vnode 的 data(Symbol / 私有键);读取 $vnode.dataprops / attrs
B用 provide 层级「当前是否在 content 渲染中」替代 vnode 标记
C2.7 放宽:在 LayerView 子树内任意 defineLayer 都生效(可能误伤)
  • 决策A(并入 D0.10)— 与 Vue 3「标在 vnode 上」同构:
js
// 创建(Vue 2 h data 形态)
h(content.component, {
  key: openId,
  props: { ...contentProps },
  [LAYER_CONTENT]: true, // 落在 data,非 props
})

// 判定
instance.proxy?.$vnode?.data?.[LAYER_CONTENT] === true
// (勿用 _vnode;实现时核对 getCurrentInstance 适配层是否已映射到占位 vnode)

否决 provide 栈(B)与放宽子树任意生效(C)。

D5.5 <script setup> / defineOptions

选项内容
A文档要求 2.7.0+ 与对应编译器;setup 用法与 Vue 3 同文
B另附 Options API 示例(setup() 返回)
CADR 0007 静态 define 在 2.7 明确不做
  • 决策A + C(并入 D0.18)— 文档面向 2.7+;用法与 Vue 3 指南同文;ADR 0007 静态 define 两端都不做

D6.1 官方示例容器

选项内容
AElement UI el-dialog / el-drawer
B无 UI 库 headless 示例(自写假 Dialog)
CElement UI + 一篇「其它库」差异
  • 决策延后(并入 D0.15)— 官方可运行 Vue 2.7 / Element UI 示例暂时不做;发版以差异说明 + Vue 3 文档为准。日后补 playground 另开。

D6.2 与容器 append-to-body / modal 叠层

库已把 LayerApp 挂 body;Element Dialog 也可能再 append-to-body → 双层 body 节点,一般仍可用,但 z-index、焦点陷阱可能怪。

选项内容
A文档推荐保留 UI 库 append-to-body;接受双挂
B推荐关掉 append-to-body,由库统一挂 body
C检测并 warn
  • 决策A(并入 D0.18)— 与 Vue 3 相同:接受 UI 库 append-to-body 双挂;兼容篇可提一句。不强制关掉、不做检测 warn。

D6.3 是否承诺兼容 @vue/composition-api + Vue < 2.7

选项内容
A仅 2.7+(内置 composition)
B尝试兼容 2.6 + @vue/composition-api(工作量大)
  • 决策A(并入 D0.18)— 仅 Vue 2.7+

D7. API 表面与功能裁剪

D7.1 首期功能范围(可多选「砍」)

对每项标:做 / 砍 / 延后

能力Vue 32.7(首期)
createLayer / useLayer / open / close
confirm
clone
bindHost / 自动 Host
defineLayer
LayerTemplate
adapter
live MaybeRefOrGetter 配置
LayerNoContainer
打开后换容器park(允许;自然 remount,见 D0.3)
SSR(同:无 DOM 不 mount)
  • 决策全做(并入 D0.13)。不因无 Teleport 砍功能面。

D7.2 公开 API 是否同名同形

选项内容
A同名;行为差只写文档(推荐)
B2.7 专用 API 前缀 / 子路径(如 vue-layerx/vue2
C运行时 isVue2 时对不支持 API throw
  • 决策A(并入 D0.13)— 同名同形;行为差仅文档。日常路径对外几乎无感;文档重点写清换容器与默认 model(见 D0.13 表)。

D7.3 keep-alive Host

选项内容
A对齐 Vue 3 文档:deactivated 不自动关;卸才 dispose
B2.7 在 deactivated 时强制 close/unmount
C不测试、不承诺
  • 决策A(并入 D0.18)— 与 Vue 3 文档相同。

D8.1 源码分叉方式

选项内容
Alayer-view.vue3.ts / layer-view.vue2.ts + 构建别名
B运行时 if (isVue2) 分支(单文件交织)
C共享 core + 两套 runtime 入口(构建只打进一套)
Dcompat/vue2 + compat/vue3 分端实现 + compat/index 聚合用 isVue2 选导出
  • 决策D(并入 D0.12;导入安全 D0.19)— 两端代码分开写;统一聚合层做 isVue2 判断并导出同名 API。否决单文件交织(B);构建别名只打一套(A/C)可作为日后优化,非本期形态。同包双 compat 时必须遵守 D0.19,否则 Vue 2.7 消费方会在解析阶段失败。

D8.2 bind-* 是否抽「平台适配」层

选项内容
AbindContainerModel 产出「逻辑事件」,再 toVNodeProps(platform)
B两套 bind 实现文件
C仅在 h() 前一层转换,bind 仍产出 Vue 3 形态(2.7 入口翻译)
  • 决策C(并入 D0.11;澄清见 D0.20)— bind-* 仍产出 Vue 3 flat,且不维护两套文件;默认 model / flat 更新键由 compat 注入;仅 createLayerViewVNode + createLayerApph 前做平台翻译(toPlatformVNodeData)。否决两套 bind(B);不必先改成抽象「逻辑事件」再映射(A 可选优化,非必须)。

D8.3 运行时外部依赖

README 写「零外部依赖」。若 2.7 引入 vue-demi / portal-vue

选项内容
A2.7 包允许 peer/dep 额外依赖,README 分区说明
B坚持零依赖 → 禁止 demi/portal(与 D0.2 一致)
C主线仍零依赖;仅 vue2 包可有 dep
  • 决策B(并入 D0.12)— 坚持零运行时外部依赖;isVue2 自研探测;禁止 demi/portal。vue 仅为 peer。

与 Vue 3 行为对照(用户可见,已决部分)

场景Vue 3Vue 2.7
正常 open / closeTeleport → 容器 defaultcontainer 插槽内直接挂 content
打开后换 container.componentTeleport park,content 可保留同配置语义;无 Teleport → 嵌套 diff 自然整子树 remount(D0.3 / D2.2)
LayerNoContainer同构 Teleport + 全投影扁平 h(content) + 全投影(D0.5 / D2.1-A)
Host provideappContext 桥接extend + parent: proxy(D0.8 / D0.21)
全局插件 / $message独立 App + 桥接宿主 Vue 构造器上 extend(D0.8)
默认 model / 更新事件modelValue + update:modelValuevalue + input;其它→update:${model}(D0.6 / D0.20)
公开 API现行同名全做;差仅文档(D0.13)

风险与缓解

风险影响缓解(实现约束)
同包 dist 静态 named import Vue 3-only APIVue 2.7 应用解析 / 打包失败D0.19:交集 named import + vue3 侧 namespace 安全访问;tests-vue2 入口冒烟
DEFAULT_CONTAINER_MODEL / valueinput 与「bind 不分叉」张力事件绑错或两端行为不一致D0.20:默认 model 与 flat 更新键由 compat 注入;toPlatformVNodeData 不做二次特例
Host / getCurrentInstance 字段差异(isMounted vs _isMounted、proxy vs internal)provide 断裂、误绑、晚 bind 失效D0.21 + D3.5 适配层;关键路径测无 Host / 晚 bind / Host 卸 dispose
双份 vue / 根包双版本抢解析inject 丢失或测试串版文档 dedupe;库坚持 $root.constructor.extend;测试用 D0.22 独立包,否决根包双挂
LAYER_CONTENT 读路径($vnode.data)与 2.7 setup 实例映射不符defineLayerexists === falseD0.10;关键路径测钉死读路径;勿用 _vnode
slots / h 形态(scopedSlots vs slots 对象)具名插槽、LayerTemplate 不渲染D0.11 / D2.6:仅在 createLayerViewVNode / createLayerApp 转换;测具名 + default
打开后换 container.componentcontent state 丢失(相对 Vue 3 park)已接受(D0.3);兼容篇写明;不另加 containerKey
Element append-to-body 双挂 bodyz-index / 滚动锁怪异与 Vue 3 相同接受(D6.2);文档一句即可
getCurrentInstance 时序异步回调里 bindHost 失败(两端都有,2.7 更易踩 Options API)与现行相同:须 setup 同步调用;文档 / warn(D3.5)
同包一份 .d.ts 与运行时分叉IDE 提示与 2.7 默认 model 等不符D0.14 shim;兼容篇写清默认 model / 换容器;不双 types 入口
Vue 2 EOL / 矩阵拖累长期维护成本D0.17 best-effort;仅 tests-vue2 关键路径,不做全量对等覆盖
未先完成 ADR 0009 就开本篇集成仍测源码、Vue2 线模型不一致阻塞:先 0009,再本篇 runtime + tests-vue2

后果

  • 范围已定(Accepted);按 D0.1–D0.22 在已决降级下 可实现未实现前不改 peerDependencies / 版本号;实现合并后发 1.1.0
  • 实现前置ADR 0009 须先完成;本篇 搬迁 Vue 3 集成到独立包。
  • 实现须遵守 D0.1–D0.22:无 Teleport;compat 分端;h 前适配;同包 shim 类型;API 全做;与 Vue 3 同类问题行为对齐;导入安全(D0.19)、model/事件键注入(D0.20)、Host 窄接口(D0.21)、tests-vue2 独立包只消费 dist(D0.22)。
  • 对外文档:一篇 Vue 2 兼容说明(换容器、默认 model、双挂 body、dedupe 等);官方 2.7 playground 延后
  • 风险用 §风险与缓解 + D0.17 / D0.22 CI 锁定;不承诺与主线同寿(best-effort)。

决议录

text
D1.1 A(D0.7:同包同 API;构建/解析选 runtime;无 vue-demi / 独立包名)
D1.2 A(D0.7:首发预定 1.1.0;changelog 写 2.7 差异)
D1.3 C(D0.14:同包一份 d.ts + MaybeRefOrGetter 等本地 shim)
D1.4 C(D0.17 / D0.22:tests-vue2 关键路径 + 入口冒烟;前置 ADR 0009)
D1.5 D(D0.17:一篇 Vue 2 兼容说明;无 playground)
D1.6 A(D0.17:best-effort / 最大努力,无 SLA)
D2.1 A(D0.5:NoContainer 全投影,与 Vue 3 语义一致)
D2.2 D(与 Vue 3 同语义;无 Teleport → 换 container 自然 remount)
D2.3 D(openId 同 Vue 3:仅 false→true ++;无 containerKey)
D2.4 与 Vue 3 相同(D0.16:无 fallback;假定容器渲染 default)
D2.5 A(D0.18:无额外 wrapper)
D2.6 A(D0.18:对外 VNode 函数;compat 侧转 slots)
D3.1 B(D0.8:HostVue.extend(LayerApp) + new;一次 app)
D3.2 A(D0.8:parent: hostProxy;无 Host 允许挂)
D3.3 C(D0.8:宿主 $root.constructor.extend;不抄 prototype)
D3.4 与 Vue 3 意图相同(D0.18:不专项 DevTools)
D3.5 A(D0.18 / D0.21:适配层对齐 bindHost 语义;窄接口)
D3.6 A(随 D0.8:Host 卸则 dispose;晚 bind 下次 open)
D3.7 A(D0.18:SSR 同 Vue 3)
D3.8 A(D0.18:挂 document.body)
D4.1 C(D0.6 / D0.20:3→modelValue,2.7→value;由 compat 注入)
D4.2 A(D0.6 / D0.20:3 恒 onUpdate:model;2.7 value→onInput,其它→onUpdate:model)
D4.3 A(D0.11:h 前翻译 onXxx→on;首期无 nativeOn;不做 value/input 二次特例)
D4.4 A(D0.18:ref 同 Vue 3)
D4.5 A(D0.18:attrs 同 Vue 3)
D4.6 A(D0.18:异步 content 同 Vue 3)
D5.1 A(D0.9:库内 toValue / MaybeRefOrGetter polyfill)
D5.2 A(D0.18:类型本地处理)
D5.3 A(D0.18:Symbol InjectionKey)
D5.4 A(D0.10:标记 data[LAYER_CONTENT];读 $vnode.data;不进 props/attrs)
D5.5 A+C(D0.18:文档 2.7+;不做 ADR 0007 静态 define)
D6.1 延后(D0.15:官方 2.7 可运行示例暂不做)
D6.2 A(D0.18:接受 append-to-body 双挂,同 Vue 3)
D6.3 A(D0.18:仅 Vue 2.7+)
D7.1 全做(D0.13)
D7.2 A(D0.13:同名同形;行为差仅文档)
D7.3 A(D0.18:keep-alive 同 Vue 3 文档)
D8.1 D(D0.12:compat/vue2|vue3 分端 + index 聚合 isVue2;导入安全 D0.19)
D8.2 C(D0.11 + D0.20:bind 保持 flat;compat 注入 model/更新键;h 前翻译)
D8.3 B(D0.12:零运行时依赖;自研 isVue2;无 demi/portal)
D0.19 同包 dist:禁止对 Vue3-only API 静态 named import;vue3 侧 namespace/安全访问
D0.20 默认 model + flat 更新键由 compat 注入;bind 仍 flat;toPlatformVNodeData 不做 value/input 二次特例
D0.21 Host 经 compat 窄接口;业务不直触 appContext / 2.7 raw 内部实例
D0.22 tests-vue2 独立 workspace 包;只消费 dist;关键路径 + 冒烟;前置 ADR 0009;否决根包双挂 vue@2

MIT Licensed