主题
完整教程:做一个礼物滚动榜
本教程基于 快速开始 的最小闭环,从零做出一个完整可用、可发布的挂件——「礼物滚动榜」:
- 实时监听礼物事件,按时间倒序展示;
- 可配置标题、最多显示条数、是否显示礼物图标;
- 支持自定义强调色与背景色,挂件内用 CSS 变量直接消费;
- 累计金额持久化,关掉再开还在;
- 最后打包成
.zip供他人安装。
预计耗时 30 分钟。完成后的产物可直接放进 示例集 的同位置作为参考。
1. 建立项目骨架
gift-roll/
├── manifest.json
├── index.html
├── thumbnail.png ← 200×200 的预览图,挂件库列表展示
└── assets/
└── ding.mp3 ← (可选)礼物到达提示音完全可以不用任何构建工具,纯 HTML + CSS + JS 就够了。如果你喜欢用 Vite/TS,看 §7 用 Vite 做开发。
2. 写 manifest.json
json
{
"manifestVersion": 1,
"id": "gift-roll",
"name": "礼物滚动榜",
"version": "1.0.0",
"author": "你的名字",
"description": "实时显示最新礼物记录,可配置最多条数与样式",
"sdkVersion": 1,
"thumbnail": "thumbnail.png",
"defaultSize": { "width": 380, "height": 560 },
"minSize": { "width": 220, "height": 240 },
"category": "custom",
"permissions": ["gifts"],
"config": [
{ "key": "title", "type": "text", "label": "标题",
"default": "礼物榜", "group": "基础" },
{ "key": "max", "type": "slider", "label": "最多显示条数",
"default": 20, "min": 5, "max": 50, "step": 1, "group": "基础" },
{ "key": "showIcon", "type": "switch", "label": "显示礼物图标",
"default": true, "group": "基础" },
{ "key": "showTotal", "type": "switch", "label": "显示累计金额",
"default": true, "group": "基础" }
],
"theme": [
{ "key": "accent", "type": "color", "label": "强调色",
"default": "#63e2b7", "alpha": true },
{ "key": "bg", "type": "color", "label": "背景色",
"default": "rgba(0,0,0,0.35)", "alpha": true },
{ "key": "text", "type": "color", "label": "文字颜色",
"default": "#ffffff" }
]
}要点:
configvstheme:两者结构完全一样,区别只是宿主在配置面板上把它们放在「内容」与「样式」两个 Tab 下。不强求,但分开后用户更好找。permissions: ["gifts"]:声明本挂件会用到礼物事件 / 礼物 RPC。当前版本不强制校验,但建议从一开始就声明,未来切到强制模式时挂件无需改动。thumbnail:相对manifest.json所在目录的路径,挂件库列表里展示。建议正方形、≤ 50 KB。category: "custom":决定挂件归属在哪一类元素菜单下。本教程做的是「礼物滚动榜」,与内置「礼物菜单」(GiftMenu)侧重点不同(一个是滚动记录,一个是横排展示当前可送礼物),归custom即可。如果做的是真正能替代内置加班机的挂件,则选OvertimeMachine,用户在「素材 → 加班机」入口就能直接找到。取值直接复用ElementType字符串,完整列表见 manifest 规范 §2.1。
完整字段表见 manifest 规范。
3. 写 index.html:结构与样式
html
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<style>
html, body {
margin: 0; height: 100%;
background: transparent; overflow: hidden;
font: 14px/1.5 system-ui, "Microsoft YaHei", sans-serif;
color: var(--zbj-text);
}
.wrap {
height: 100%; box-sizing: border-box;
padding: 10px 14px;
background: var(--zbj-bg);
border-radius: 12px;
backdrop-filter: blur(8px);
display: flex; flex-direction: column;
}
.head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 6px;
}
.title { font-size: 18px; font-weight: 700; color: var(--zbj-accent); }
.total { font-size: 12px; opacity: .8; }
ul.list { list-style: none; margin: 0; padding: 0; flex: 1; overflow: hidden; }
li.item {
display: flex; align-items: center; gap: 8px;
padding: 6px 0;
animation: enter .35s ease;
}
li.item img { width: 28px; height: 28px; border-radius: 6px; object-fit: cover; }
li.item .nick { font-weight: 600; }
li.item .gift { opacity: .85; }
li.item .accent { color: var(--zbj-accent); margin: 0 2px; }
@keyframes enter { from { opacity: 0; transform: translateX(-12px); } }
</style>
</head>
<body>
<div class="wrap">
<div class="head">
<span class="title" id="title">礼物榜</span>
<span class="total" id="total" hidden>累计 ¥0</span>
</div>
<ul class="list" id="list"></ul>
</div>
<script src="http://127.0.0.1:9855/widget-sdk/zbj-widget-sdk.js"></script>
<script type="module" src="./app.js"></script>
</body>
</html>注意所有的颜色、文字色都直接用 var(--zbj-accent) / var(--zbj-bg) / var(--zbj-text)。这就是 CSS 变量桥:theme.accent 字段的值会被 SDK 写到 :root --zbj-accent,主题面板调色 → 挂件 UI 实时变化,不需要写一行 JS。
4. 写 app.js:拿配置、订事件
js
ZBJ.ready(() => {
const titleEl = document.getElementById("title");
const totalEl = document.getElementById("total");
const listEl = document.getElementById("list");
/* ── 1. 状态与渲染函数(必须先于 applyConfig 声明)─────────
* applyConfig 内部会调用 renderTotal / pruneList,
* 这两个函数读取 total 与 ZBJ.config。
* `let total` 必须在 applyConfig() 之前完成初始化,否则会触发
* "Cannot access 'total' before initialization" 的 TDZ 错误。 */
let total = ZBJ.storage.get("total") ?? 0;
function renderTotal() {
if (!ZBJ.config.showTotal) return;
totalEl.textContent = `累计 ¥${(total / 100).toFixed(0)}`;
}
function pruneList() {
const max = Number(ZBJ.config.max) || 20;
while (listEl.children.length > max) listEl.lastChild.remove();
}
/* ── 2. 配置同步 ─────────────────────────────────────── */
function applyConfig() {
titleEl.textContent = ZBJ.config.title;
totalEl.hidden = !ZBJ.config.showTotal;
pruneList();
renderTotal();
}
applyConfig();
ZBJ.onConfigChange(applyConfig);
/* ── 3. 礼物事件 ─────────────────────────────────────── */
ZBJ.on("gift", e => {
const li = document.createElement("li");
li.className = "item";
if (ZBJ.config.showIcon && e.giftIcon) {
const img = document.createElement("img");
img.src = e.giftIcon;
img.alt = "";
li.appendChild(img);
}
const nick = document.createElement("span");
nick.className = "nick";
nick.textContent = e.nickname;
const gift = document.createElement("span");
gift.className = "gift";
gift.innerHTML = `送出 <span class="accent">${e.giftName}</span> ×${e.giftCount}`;
li.append(nick, gift);
listEl.prepend(li);
pruneList();
// 累计(diamondCount 单位:分;可能为 0 表示免费礼物)
if (e.diamondCount) {
total += e.diamondCount * e.giftCount;
ZBJ.storage.set("total", total); // 仅在 edit 模式下落盘
renderTotal();
}
});
});四件事讲一下:
| 行为 | 解释 |
|---|---|
ZBJ.config.title 在初次渲染就能读到 | 因为 ZBJ.ready 一定在 init 之后才触发,此时配置已下发 |
applyConfig 在 ready 内调用一次、再注册 onConfigChange | 这是「初次 + 增量」的标准模式;如果只注册 onConfigChange 会漏掉首屏 |
storage.set 只在 edit 模式下真正落盘 | 在预览窗 / 虚拟摄像头窗里 set 仅在会话内有效。原因见 开发约定 §8 |
| 没有手动管理 WebSocket | SDK 内部已经连了 /ws,自动归一化平台事件并分发;挂件作者只需要 ZBJ.on("gift", …) |
5. 在主播酱里挂上
第一次:
- 在主播酱「创意工坊」→「开发者」Tab 注册:
- manifest 路径:
.../gift-roll/manifest.json; - 挂件源 URL:留空(主播酱内嵌服务会从
dev-widgets/<id>/直接托管同目录文件)。
- manifest 路径:
- 「我的挂件」中拖到画布。
- 选中该挂件,右侧出现「内容」与「样式」两个 Tab,对应
config/theme数组。
后续:改 index.html / app.js 保存即可,开发者模式下 iframe 会自动刷新。
改 manifest.json 时客户端会自动重新读取并刷新配置面板,不需要重新注册。
6. 用真实事件验证
两种方式:
| 方式 | 怎么做 |
|---|---|
| 连真实直播间 | 在主播酱主界面绑定一个开播中的直播账号(任意支持平台),礼物到达即可看到挂件更新 |
| 注入测试事件 | 在配置面板点「发送测试事件」按钮,选事件类型即可(详见 调试与排错) |
更丰富的事件录制 / 回放工具在规划中(见 版本策略 §3)。
7. 用 Vite 做开发体验
如果你想要 HMR、TypeScript 或者 npm 包,可以让挂件在自己的 dev server 上跑:
gift-roll/
├── manifest.json
├── public/
│ ├── thumbnail.png
│ └── assets/ding.mp3
├── src/
│ ├── index.html
│ ├── app.ts
│ └── style.css
├── vite.config.ts
└── package.jsonvite.config.ts 关键配置:
ts
export default {
root: "src",
base: "./",
server: { port: 5173 },
build: { outDir: "../dist", emptyOutDir: true }
};在主播酱「开发者」Tab:
- manifest 路径:选项目根的
manifest.json; - 挂件源 URL:填
http://localhost:5173。
主播酱会优先用此 URL 装载 iframe,HMR、TS、PostCSS 全部按 Vite 默认行为工作。SDK 仍然从 http://127.0.0.1:9855/widget-sdk/zbj-widget-sdk.js 引入。
构建并打包时执行 pnpm build,将 dist/ 改名为 gift-roll/ 后按 §8 打包 走流程。
8. 打包发布
完整流程见 打包与发布。这里给出 90 秒版本:
- 确保目录结构(
manifest.json必须在根):gift-roll/ ├── manifest.json ├── index.html ├── app.js ├── thumbnail.png └── assets/ - 打成 zip(zip 内不要多一层文件夹,根直接是
manifest.json):pwshCompress-Archive -Path gift-roll\* -DestinationPath gift-roll-1.0.0.zip - 把
gift-roll-1.0.0.zip发给用户,对方在主播酱「创意工坊」→「我的挂件」→「安装」选择该 zip,校验通过即解压到userData/widgets/gift-roll/。 - 改了代码要发新版?升
manifest.version,重新打 zip 即可。
9. 下一步
| 想做的事 | 看这里 |
|---|---|
| 给挂件加一个「输入礼物 ID 显示发出该礼物的最新用户」类的下拉,让用户从平台礼物里选 | 示例集 §4 RPC |
| 在没有动画/JS、纯 CSS 的场景做更轻量的挂件 | 示例集 §2 弹幕墙 |
| 排查 iframe 白屏 / SDK 没加载 / 事件没收到 | 调试与排错 |
| 了解所有可用事件字段(不同平台差异) | 平台事件 |
| 设计多挂件协同的玩法 | 暂在版本策略的规划中,见 版本策略 §3 |