npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-vue2

vue@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-dismisscountdown-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 上,动作粒度看不出时机 → 曝光和关闭也被算成点击

interactiontrigger 区分了这三种时机: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 算——壳里不该出现任何跟关闭按钮有关的判断。