@yaoxiu/marketing-dsl-vue2
v2.0.0
Published
营销弹窗 DSL 的 Vue 2 渲染壳,只负责把渲染树画成 DOM
Readme
@yaoxiu/marketing-dsl-vue2
营销弹窗 DSL 的 Vue 2 渲染壳。
整个包只做一件事:把 @yaoxiu/marketing-dsl 算出来的渲染树画成 DOM。不含任何解释逻辑——插值、条件、循环、样式白名单、层级规则全在 core 里。
源码 200 行出头,其中一半是注释和类型。往里加 if 判断之前先想想是不是该加在 core。
安装
pnpm add @yaoxiu/marketing-dsl @yaoxiu/marketing-dsl-vue2vue 和 @yaoxiu/marketing-dsl 都是 peer 依赖,必须由宿主自己装。vue 要求 ^2.6.0(2.6 和 2.7 都能用,没有用到 2.7 才有的 API)。
为什么 core 是 peer 而不是普通依赖
壳和宿主必须共用同一个 core 实例。宿主(后台配置页)自己也会直接 import { validate, createRuntime } from '@yaoxiu/marketing-dsl' —— 编辑器保存前校验走的就是它。
core 如果放在 dependencies 里,只发 core 不发壳(core 改了、壳没改,这是常态)时,包管理器会给壳装一份它自己锁定的旧 core:宿主用新版校验通过的配置,壳内部却用旧版解释,两套逻辑并存,而且这种不一致不会报错,只会表现成「后台预览正常、线上渲染不对」。放 peer 就只有一份,版本对不上时安装阶段直接告警。
跟 vue 是 peer 完全同一个道理:宿主已经有的东西,库不该再夹带一份。
关闭由宿主控制
组件不会自己消失。配置里的 close 动作、点 X、点遮罩、倒计时归零,最终都只是抛一个 close 事件出来,关不关、关多久、要不要记住「不再提醒」,全是宿主说了算。
这是刻意的:一段运营配置不该有权限决定宿主页面的 DOM 生死。
<dsl-renderer v-if="visible" :dsl="dsl" @close="onClose" />onClose({ reason }) {
this.visible = false // 关掉它 —— 这一步必须宿主自己做
if (reason === 'user-dismiss') {
localStorage.setItem('mbl_never_remind', '1') // 记住别再出
}
}reason 能区分用户是怎么走的:close-button(点 X)、mask(点遮罩)、配置里 close 自带的值(如 user-dismiss、countdown-end)、action(没写 reason 的默认值)。
多视图叠了好几层时,关内层不会抛 close —— 那只是内部弹栈,用 @view-change 观察。只有最后一层关掉、或者配置里用了 closeAll,宿主才会收到 close。
倒计时到点自动关闭也走这条路:
{
"type": "countdown",
"to": "2026-09-01 18:00:00",
"onEnd": { "type": "close", "reason": "countdown-end" },
}用法
<template>
<dsl-renderer
v-if="visible"
:dsl="dsl"
:user="user"
:sources="sources"
:handlers="handlers"
@ready="onReady"
@close="onClose"
@interaction="onInteraction"
@track="onTrack"
@navigate="onNavigate"
/>
</template>
<script>
import { DslRenderer } from '@yaoxiu/marketing-dsl-vue2';
export default {
components: { DslRenderer },
data() {
return {
dsl: {}, // 后端下发的配置
user: { shopName: '麦爆了旗舰店', levelName: '专业版', remainDays: 3 },
// 数据源白名单:配置里写 { "$source": "listRenewTiers" } 才能调到
sources: {
listRenewTiers: params => this.$api.getTiers(params),
},
// call 动作的方法白名单。只放「让宿主去做一件事」的动作
// (唤起充值、打开客服、复制到剪贴板…),
// 不要放写本地存储这类「记录用户意图」的事——那是上报该干的,
// 比如「不再提醒」应配 { "type": "close", "reason": "user-dismiss" }
handlers: {
openRecharge: () => this.$modal.recharge(),
},
};
},
methods: {
onReady() {
// 数据备齐、这一屏画得出来了。曝光要等这个信号再上报 ——
// 数据源没注册 / 外部数据没回来时组件什么都不画,挂载即报会虚报曝光
},
onClose({ reason }) {
// reason: close-button / mask / replace / 配置自定义的值 / action
this.visible = false;
},
onInteraction(e) {
// 一次手势 / 一个时机只有一条,**上报请只订这个事件**
// e.trigger: 'show' | 'close' | 'action'
// e.track / e.navigate: 这次交互里的埋点与跳转(sequence 里多个时各取第一个)
// 按动作粒度的 track / navigate 上报会重复计数,详见下方「为什么有 interaction」
},
onTrack({ event, params }) {
// 动作粒度,给宿主接自有埋点系统用;别拿它做营销上报
this.$track(event, params);
},
onNavigate({ url, target }) {
// 组件已经跳过了,这里只是让宿主有机会记一笔
},
},
};
</script>Props
| 名称 | 类型 | 说明 |
| ---------- | ------- | -------------------------------------------------- |
| dsl | Object | 必填。配置 JSON |
| user | Object | 宿主注入的用户上下文,配置里用 {{ user.xxx }} 读 |
| sources | Object | 数据源白名单 { 名称: (params, user) => Promise } |
| handlers | Object | call 动作的方法白名单 |
| editMode | Boolean | 编辑态:动作只上报不真正执行,调试台用 |
user / handlers / editMode 变化时会就地更新,不会重建——所以用户信息异步到达时文案会自动补上,也不会重复上报曝光埋点、不会把用户切好的 tab 打回默认值。
dsl 变化则整个重建(重新拉数据源、重放曝光)。
事件
ready close interaction track navigate call state-change view-change open error
载荷结构见 core 的 RuntimeEvents 类型。
为什么有 interaction
interaction 是交互粒度的汇总事件:一次用户手势 / 一个时机只抛一条,sequence 里有 N 个动作也只汇总成一条。营销上报请只订它。
按 track / navigate 这些动作粒度的事件上报会重复计数,实测中一个弹窗的完整生命周期(1 曝光 + 1 点击 + 1 关闭)报出了 6 条、click 放大 4 倍:
sequence: [track, navigate](先埋点再跳转)是最常见的写法,两个动作各报一条 → 一次点击变两条track还能挂在stage.onShow/onClose上,动作粒度看不出时机 → 曝光和关闭也被算成点击
interaction 的 trigger 区分了这三种时机:action 才是真正的用户点击;show / close 只带上运营配的埋点名,由宿主并进曝光 / 关闭上报的 extra,不另算点击。
另有 closes 标记:手势里执行了 close / closeAll 动作时为真。运营常把埋点配进关闭按钮的 sequence: [track, close],这一下的本质是「关掉它」而不是「点了它」,上报侧据此不产出 click,埋点名并进 close 的 extra。
interaction 是新增事件,track / navigate / close / call / error 的语义一个都没变,照常派发 —— 宿主仍要靠它们做实际业务(接自有埋点系统、记跳转)。
定位说明
组件本身恒为 position: relative,老实待在宿主给它的位置上——流式坑位(banner / 公告条)直接放进普通 div 即可,不需要外层容器有确定尺寸。
弹窗铺满视口由弹窗层自己 position: fixed; inset: 0; z-index: 1000 完成,与根容器无关。多视图配置(如「公告条点击弹出弹窗」)因此两种形态各自正确:公告态是坑位里的一条流式内容,弹出后弹窗覆盖整个视口,公告条留在原位不动。
早期版本是「配置里只要有 popup 视图,根容器就变
absolute; inset: 0」。那样多视图物料一进流式坑位就脱流:坑位高度塌成 0,公告条按最近的定位祖先(通常是视口)飘走。已改。
想把弹窗限制在某个容器内(比如后台的预览框),在该容器上加 transform: translateZ(0)(或 filter / will-change: transform / contain: paint)即可——这会截胡 fixed 的包含块,让弹窗以该容器为准铺满。
关于 CSS
不需要引任何样式文件。所有样式都由 core 算成行内样式,包括遮罩、滚动层、圆角裁切。DOM 上的 .dsl-layer / .dsl-mask / .dsl-stage 等类名只是给调试和外部微调留的钩子。
关于 SVG
渲染树里可能出现 tag: 'svg' / 'line' 的节点(比如关闭按钮的默认 × 图标),壳只提供两项通用能力:按 el.tag 建元素、把 el.attrs 用 setAttribute 语义写进去(viewBox / stroke-width 这类带连字符的属性没有对应的 JS 属性,走 domProps 写不进去)。命名空间不用管,Vue 2 认得这些标签,会自己用 createElementNS。
图标画什么形状、多大、线多粗,全在 core 算——壳里不该出现任何跟关闭按钮有关的判断。
