跳到主内容
简牍jiandu
阅读模式

写一个自己的 Widget

官方教程组件masonv1
查看历史版本
目录

官方组件不够用时,自己做一个,推到站点,然后在 Markdown 里按名字调用。构建发生在你自己的机器上;站点只保存产物,不执行你的源码。

最快的路:jdu widget create

jdu widget create StarRating --template vanilla   # vanilla | react | vue;react 默认走平台 runtime(产物几 KB)
cd star-rating && npm install
npm run dev        # 本地预览:改代码自动刷新、暗色切换、sampleData 即时生效
npm run push       # 构建并发布

生成的目录里契约类型、框架基类、构建脚本全部内联,改 src/types.ts 里的 Data 接口,npm run build 会自动生成 widget.json 的 dataSchema(字段 JSDoc 变成 description)。下面是这条路背后的东西,想手工搭也行。

一份最小实现

一个 Widget 就是一个目录,里面至少有:

  • widget.json:名字、scope、版本、入参的 JSON Schema
  • index.js:自包含 ESM,默认导出一个无参可构造的 class
  • index.css:可选

widget.json

{
  "name": "StarRating",
  "scope": "personal",
  "version": "1.0.0",
  "description": "星级评分:0–max 颗星,适合评审打分、满意度",
  "dataSchema": {
    "type": "object",
    "required": ["value"],
    "properties": {
      "value": { "type": "number" },
      "max": { "type": "number", "default": 5 }
    }
  },
  "sampleData": { "value": 3, "max": 5 }
}

description 可选,一句话说什么场景用:/api/syntax 会带上它,agent 的 get_syntax 也靠它在几个组件之间做选择。

index.js(vanilla,零框架;源码即产物):

export default class StarRating {
  mount(el, ctx) {
    const { value, max = 5 } = ctx.data;
    el.textContent = "★".repeat(Math.min(value, max));
    el.style.color = "#f59e0b";
  }
}

契约:

  • 默认导出 class,new 时不做事
  • 必须有 mount(el, ctx);副作用全放这里
  • 可选 update(ctx)(主题切换、流式补数据)、destroy()(清定时器)
  • ctx.data 就是 fence 里的 JSON。未声明流式时,mount 时数据完整且已过 dataSchema 校验,不用再写守卫
  • 只有处理半截 JSON 时才加 static __widgetConfig = { streaming: true }

scope 填 personal(自己用);official 只有站点管理员能推。

有依赖时再打包

用到 npm 包或 TypeScript 时,打成浏览器 ESM,不要 external 任何东西,产物里也不能出现 node: 内置模块:

npx esbuild index.ts --bundle --format=esm --target=es2022 \
  --minify --platform=browser \
  --define:process.env.NODE_ENV='"production"' \
  --outfile=dist/index.js
cp widget.json dist/

有 CSS 就一并放进 dist/index.css。然后:

jdu widget push dist
jdu widget list

<dir> 必须直接含 widget.jsonindex.js。推的是构建产物目录,不是源码根目录。

目录里如果有 src/,会随产物一起上传,方便以后回头看;不会经 HTTP 提供给读者。

在文档里调用

```widget:StarRating
{ "value": 4 }
```

裸名优先用你自己的 personal 实现。别人要钉死官方的同名组件,需要写成 widget:official/StarRating(你这个例子没有官方版,写了会降级)。

上线、下线、审批

引用格式照 jdu widget list 抄:personal/StarRating@1.0.0

jdu widget disable personal/StarRating@1.0.0
jdu widget enable  personal/StarRating@1.0.0

下线不删产物。文档不必重渲:读者下次打开时,这一块变成代码块。再上线同样即时生效。

status含义
active可解析
pending站点开了审批时,team / personal 新推送的落地状态
disabled主动下线
rejected审批驳回

active 一律按未注册处理。管理员用 jdu widget approve / reject。重推不会改变 status:已经下线的,再 push 同一版本不会悄悄上线。

React 作者

不想手写 class 时,用 @monoedge/jdu-render/react 的工厂,一行接上契约:

import { reactWidget } from "@monoedge/jdu-render/react";

function StarRating({ props }) {
  const value = Number(props?.value ?? 0);
  return `★`.repeat(value);
}

export default reactWidget(StarRating);

update 走 React 的重新渲染,不要在 update 里自己 unmountmount。官方组件就是这么接的。

构建时把 React 打进产物,或走站点提供的平台档 runtime(体积更小,同页共享一份)。第一份自己的组件用全内联最省事。

站点会拒绝的产物

push 时服务端做静态检查,不执行代码:

  • 产物里 import 了 Node 内置模块
  • index.js / index.css / dataSchema 超过体积上限
  • dataSchema 不是能编译的 JSON Schema

浏览器里执行失败(ESM 拉取失败、mount 抛错)只影响这一块,降级成代码块。

调 JSON 不必反复 push 文档:jdu widget dev 本地预览,调好了再推。


教程目录:使用简牍 · 下一篇:怎么读简牍站点