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-dom

v1.0.0

Published

营销弹窗 DSL 的原生 DOM 渲染层,只负责把渲染树画成 DOM,零框架依赖

Readme

@yaoxiu/marketing-dsl-dom

营销弹窗 DSL 的原生 DOM 渲染层,零框架依赖。

整个包只做一件事:把 @yaoxiu/marketing-dsl(下称 core)算出来的渲染树画成 DOM。 它是「渲染树 → DOM」的唯一实现:

  • Chrome 插件(第三方平台后台,原生 DOM + Shadow DOM)直接用它;
  • Vue 2 壳 @yaoxiu/marketing-dsl-vue2 只是把它包成组件。

DSL 加新能力时只改 core(解释)和本包(映射,如果渲染树结构变了)两处,各框架壳不用动。

与 core 的分工

| 谁 | 做什么 | | ---- | -------------------------------------------------------------------------------------------------------- | | core | 全部解释逻辑:插值、条件、循环、样式白名单、层级与方位、动作派发、倒计时 / 轮播定时器、CSS 文本生成 | | 本包 | 照着渲染树建元素、改 class / style / attrs / 文本、挂点击、按 key 复用元素、把 tree.css 落成 <style> |

本包不含任何解释逻辑。往里加 if 判断之前先想想是不是该加在 core; 若发现 core 缺能力、本包不得不自己「解释」配置,应先补 core。

安装

pnpm add @yaoxiu/marketing-dsl @yaoxiu/marketing-dsl-dom

core 是 peer 依赖,必须由宿主自己装(理由同 marketing-dsl-vue2:宿主自己也会直接用 core 做校验, 必须共用同一个 core 实例,否则「后台预览正常、线上渲染不对」)。

API

function mountDsl(
  container: Element | ShadowRoot,
  options: {
    dsl: Dsl; // 必填。配置 JSON
    user?: Record<string, unknown>; // 宿主注入的用户上下文,配置里用 {{ user.xxx }} 读
    sources?: Record<string, DslSource>; // 数据源白名单
    handlers?: Record<string, DslHandler>; // call 动作的方法白名单
    editMode?: boolean; // 编辑态:动作只上报不真正执行
    navigate?: (url: string, target: '_self' | '_blank') => void; // 宿主接管跳转
    onEvent?: (name: RuntimeEventName, payload: RuntimeEvents[name]) => void; // 全部运行时事件
  },
): {
  runtime: DslRuntime; // 底层 runtime,调试台读状态用
  update(patch: { user?; handlers?; editMode? }): void; // 就地更新,不重建、不重放曝光
  destroy(): void; // 卸载,可重复调用
};

除 dsl / onEvent 外的选项原样交给 core 的 createRuntime,语义见 core README (navigate 见「宿主接管跳转」,事件见「运行时事件」)。

  • 挂载点里只多出一个根元素(div.dsl-renderer dsl-rN),挂载点原有内容不动。
  • 数据源没就绪 / 加载失败时什么都不画(挂载点里没有根元素),曝光要等 ready 事件再报。
  • 关闭由宿主决定:close 事件只是通知,要不要移除、记不记「不再提醒」都是宿主的事,移除就调 destroy()。
  • 换配置要 destroy() 后重新 mountDsl(重新拉数据源、重放曝光);user / handlers / editMode 用 update。
  • sources / navigate 只在挂载时生效。

在 Shadow DOM 里使用(插件)

import { mountDsl } from '@yaoxiu/marketing-dsl-dom';

const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });

const handle = mountDsl(shadow, {
  dsl: material.dsl,
  user,
  sources,
  // 插件:一律开新标签页,不让用户被跳离平台后台
  navigate: url => window.open(url, '_blank', 'noopener,noreferrer'),
  onEvent: (name, payload) => {
    if (name === 'ready') reporter.reportExposure(material.id);
    if (name === 'interaction') reporter.onInteraction(material.id, payload);
    if (name === 'close') {
      reporter.reportClose(material.id, (payload as { reason: string }).reason);
      handle.destroy();
      host.remove();
    }
  },
});

// 用户信息后到
handle.update({ user: nextUser });

挂载点传 ShadowRoot 本身或 shadow 里的任意元素都行。

CSS 写到哪

tree.css(keyframes + hover 规则 + reduced-motion 兜底)非空时,本包建一个 <style data-dsl-renderer>,写到挂载点所在的根:

  • 挂载点在 ShadowRoot 里 → 写进那个 ShadowRoot(宿主页面看不到、也影响不到);
  • 否则 → 写进 document.head。

每个实例一个 <style>,规则都挂在各自的作用域类名下,同页多份物料互不覆盖; tree.css 变空或 destroy() 时移除。判断 ShadowRoot 不用 instanceof——插件内容脚本与页面主环境是两套全局对象。

<style> 在第一次有 CSS 时按挂载点当时所在的根决定位置,挂载后再把挂载点挪进 / 挪出 Shadow DOM 不会跟着搬。

其余样式全部行内,不需要引任何样式文件。

渲染契约(与 core 类型注释一致)

结构照 RenderLayer 的注释套四层:

div.dsl-renderer(rootClassName / rootStyle,有 onRootEnter/Leave 才绑 mouseenter/mouseleave)
  div.dsl-layer(每个视图一层,key = 视图名)
    div.dsl-mask        ← layer.mask 为 true 时才有,点击调 onMaskClick
    div.dsl-scroll      ← 点它自身的空白处等同点遮罩,弹窗内部冒泡上来的不算
      div.dsl-stage
        div.dsl-stage-clip > 节点…
        关闭按钮(有配才有)
  • 节点有 onClick 时挂 click 并 stopPropagation;没有的不绑任何事件,正常冒泡。
  • attrs 一律 setAttribute;svg / line 及其子孙用 createElementNS(SVG 命名空间)。
  • img 写 src 和空 alt。
  • 角落方位(stage.placement)只是样式不同,本包没有任何方位分支。

更新策略

core 每次通知(状态变化、倒计时每秒、轮播到点、update)后,在同一个微任务里合并成一次渲染:

  • 按 key(同级唯一)+ 标签复用元素,class / style / attrs / 文本逐项比对,只改变了的;
  • 监听器每个事件只挂一个,回调每轮换成最新的;
  • 不会 innerHTML 整树重建——那样会让入场动画重播、hover 态丢失,倒计时每秒闪一次。

弹出二次挽留层时下层的遮罩 / 节点是同一个 DOM 对象(只是遮罩变透明),关掉后也还是它,入场动画不会重播。

测试

pnpm --filter @yaoxiu/marketing-dsl-dom test run

jsdom 环境。patch.test.ts 直接测打补丁器,其余三个文件经 core 端到端测映射、交互和生命周期。