@lark-apaas/coding-cd-elements
v0.1.4
Published
Custom elements SDK for the buildless runtime (design-html / 创意模式) — self-contained IIFE bundles served from feishucdn, consumed via <script src>
Downloads
1,262
Readme
@lark-apaas/coding-cd-elements
buildless 运行时(design-html / 创意模式,AppType=8)的自定义元素 SDK。TS 源码经 tsup 编译成自包含 IIFE,由 feishucdn 分发,产物用
<script src>引入。
⚠️ 与同目录的 buildless/(registry 包)分发模型相反
两个包都在 packages/registries/ 下、都服务 buildless 运行时,但交付方式完全不同,别混:
| | buildless/(registry) | cd-elements/(本包) |
| ------ | ---------------------------------------------------- | ------------------------------------------------------------------ |
| 分发 | miaoda registry add 抓 tarball,源码拷进项目根 | CDN,产物 <script src> 引远端钉版 URL |
| 可改性 | AI 与用户都能改(改坏只影响该产物) | 产物里改不到,行为在所有引用它的产物间一致 |
| CLI | 由 miaoda-cli 消费 | 不由 CLI 消费(无 miaodaRegistry 块,registry add 抓不到) |
| 适合 | shadcn 式 UI 素材,用户要改样式和结构 | 基建型元素,行为必须全局统一、且不该被产物作者改动 |
<cd-image> 属于后者——它封装 TOS 图片参数拼接、webp 探测、DEFAULT_RESOLUTIONS 阶梯、SRC_ALLOWLIST 白名单。这些用户不该改:改坏 SRC_ALLOWLIST 会让优化静默失效,改坏阶梯会造出踩服务端冷处理的一次性尺寸变体(实测冷启 ~23s)。copy-in 把源码摊在项目根,AI 完全可能"顺手优化"掉它们。
选型判据见 docs/design-html-sdk-spec.md §1.1:用户该改的放 registry,用户不该改的放本包。
⚠️ 不要把 CDN 理解成「升级能自动覆盖存量产物」:产物里写的是钉版 URL(
design-html-sdk-spec.md§8 的固定版本契约要求如此),发新版本只作用于新产物——这点和 copy-in 一样。真实优势是代码不落进产物源码、跨产物共享浏览器缓存,以及为将来的平台注入留了口子(只有注入方统一控制版本时"覆盖存量"才成立)。
目录结构
src/
core/ # 元素间共享的内核(一份,避免各元素复制导致漂移)
tos.ts # TOS 图片处理:applyParamsToUrl / buildSrcSet / supportWebp / 白名单与阶梯常量
dom.ts # DOM 工具:defineOnce(防重复注册)/ injectStyleOnce(防重复注样式)
elements/ # 元素实现,一个元素一个文件
image.ts # <cd-image>
index.ts # 全量入口:引入即注册所有元素
dist/ # tsup 产物(gitignore,发布物)
__tests__/ # vitest(happy-dom 环境)
__gallery/ # 浏览器手工冒烟页元素一览
| 元素 | 单文件产物 | 说明 |
| ------------ | ------------------- | ---------------------------------------------------------- |
| <cd-image> | dist/image.min.js | 图片性能优化:TOS resize / webp / 响应式 srcSet / 异步解码 |
每个元素源码顶部有 /* BEGIN USAGE */ … /* END USAGE */ 块,声明注册的标签名、属性与约束。
用法
引全部元素(推荐,元素总量小):
<script src="<CDN_BASE>/@lark-apaas/coding-cd-elements@<ver>/dist/cd-elements.min.js"></script>只引一个元素(各单文件自包含,内核已 inline,无加载顺序要求):
<script src="<CDN_BASE>/@lark-apaas/coding-cd-elements@<ver>/dist/image.min.js"></script>
<cd-image src="/aily/api/v1/files/static/x.png" width="400" height="260" alt="…"></cd-image>版本精确 pin,不用浮动版本——与 design-html-sdk-spec.md §8 的 CDN 固定版本契约一致。调试时可把 .min.js 换成 .js(未压缩同源产物)。
CDN 路径从哪来
本包不自己上传 CDN。产物由 apaas/fullstack-plugin 的 coding-unpkg-sdk 镜像产线从 npm 搬到 feishucdn:
<CDN_BASE> = https://sf3-scmcdn-cn.feishucdn.com/obj/feishu-static/miaoda/coding-unpkg-sdk路径与 unpkg 原生对齐(<pkg>@<version>/<包内原始路径>),scoped 包自然嵌套。
为什么必须走 CDN:创意模式产物的 CSP 把 script-src 收口到 feishucdn,引 unpkg.com 会被浏览器拦截。
<cd-image> 与原生 <img> 的等价性
模型是把 cd-image 当 img 用的,所以它的承诺是「常见写法下等同原生 img」——不是完全等同。差异的根源只有一条:DOM 里多了一层普通元素,而 CSS 里替换元素与普通元素的语义不同。
三条机制守住等价性:
| 机制 | 解决什么 |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 宿主 display:contents | 宿主不生成盒子,参与布局的始终是那个原生 img,替换元素语义全部为真 |
| :where(cd-image>img){…:inherit} | display:contents 只从盒子树摘掉宿主,DOM 树没变,section > .bg 这类结构选择器仍命中宿主而其值会被丢弃;这条把已算好的 computed value 搬给内层 img |
| width/height → 宿主 --cd-w/--cd-h/--cd-ar | 内层 img 上属性是表现层提示,层叠位置低于一切作者规则,会被上面的 width:inherit 抹掉;改由宿主变量承载后尺寸只剩一条路径 |
后两条一律包 :where() 归零权重——它们是兜底,任何直接命中 img 的作者规则都该赢。
已知偏差(改动前先读,别重复论证)
- 结构选择器 +
display:section > .bg{display:block}传不过去。display不能进搬运名单——替换元素上的display:contents计算为none,搬过去图片会整个消失。作者写.bg{display:block}(非结构选择器)正常,class 已拷到内层 img。 - 搬运名单漏项:名单外的非继承属性,只有当作者用结构选择器设置时才会丢。加条目是安全的(见下),所以发现一条补一条即可。
- 宿主的伪元素会渲染:
.bg::after{content:"x"}在宿主上会生成盒子,原生 img 是替换元素不渲染伪元素。 - 带 tag 名的结构选择器:
section > img.bg两头都不命中(宿主标签是cd-image,内层 img 又多一级)。
为什么是白名单,什么时候可以删掉它
这套机制是约束组合下的唯一解,不是随手挑的方案。三条约束同时成立才需要它:
- buildless 静态 HTML —— 没有编译期,行为只能靠运行时自定义元素承载,wrapper 必然出现在 DOM 里;
- 作者是模型,把
cd-image当img用,不能靠文档约定它的契约; - 承诺等同原生
img—— 于是布局主体必须是那个原生 img,宿主只能display:contents,命中宿主的样式就必须搬。
去掉任何一条,白名单都可以整个删掉:
- 去掉 ③ → 改用生态标准姿势「宿主即盒子」(
:host{display:inline-block}+ 内层width:100%,Shoelace / Cloudinary / lazy-img 都这么做),代价是模型得知道 cd-image 有自己的尺寸契约(Shopify 给 Polaris web components 就是配 agent skill 解决的); - 去掉 ①(产物下发时由服务端把
<cd-image>重写成<img srcset>)→ DOM 里没有 wrapper,整类问题一次性消失,这是 Next/unpic 那条路。
平台层没有别的出路:csswg-drafts#11647(display:contents 与 nth-child)已被 CSSWG Closed as Question Answered——「只有盒子树受影响,基于文档树的选择器匹配不受影响」是设计如此;补救提案(#7012 只统计已渲染子元素的伪类、::wrap/::contents)都还停在提案阶段。
加名单条目是安全的:475 属性全枚举比对过宿主与原生 img 的默认计算值,只有 display 与 overflow 系列不同。所以名单只要不碰那几个,加多少条都不引入新偏差;漏一条也只是该属性在「仅命中宿主的结构选择器」这条支路上不生效。
验收
__tests__/*.test.ts 跑在 happy-dom 上,没有布局引擎——只能断言注入的 CSS 文本与 DOM 属性。真正的几何等价性靠浏览器手工验收页:
pnpm --filter @lark-apaas/coding-cd-elements build
open __tests__/manual/native-parity.html # 31 组 CSS 场景,逐格比对 cd-image 与原生 img页面顶部会给出「N/N 全部与原生一致」的结论,不一致的行高亮。改搬运名单或尺寸逻辑后必须重跑,并且尽量在目标 WKWebView 上也跑一次——搬运名单选的是白名单而非 all:inherit 黑名单,正是因为黑名单的边界由各引擎的 UA 样式表决定,而我们只能在 Chrome 上验证。
开发
pnpm --filter @lark-apaas/coding-cd-elements build # tsup → dist/
pnpm --filter @lark-apaas/coding-cd-elements dev # watch 模式
pnpm --filter @lark-apaas/coding-cd-elements typecheck
pnpm --filter @lark-apaas/coding-cd-elements lint
pnpm test # 全仓 vitest(含本包)
open __gallery/image.html # 浏览器冒烟(先 build)__tests__ 直接 import src/(不测产物),所以改源码即可跑测试,不必先 build。
新增一个元素
src/elements/<name>.ts写实现,export class+ 文件末尾defineOnce('cd-<name>', Class)- 顶部补
/* BEGIN USAGE */块(标签名 / 属性 / 约束) tsup.config.ts的ENTRY加一项<name>: 'src/elements/<name>.ts'src/index.ts加一行import './elements/<name>'__tests__/<name>.test.ts加单测(首行// @vitest-environment happy-dom)__gallery/<name>.html加冒烟页- 更新本文「元素一览」
- 让 Agent 知道它存在——在
packages/skills/coding-steering/steering/design-html/补 skill 说明(含 CDN URL 与用法)
共享逻辑放 src/core/,不要在元素间互相 import——每个元素产物必须自包含,能被单独引入。
发版流程(两步,跨仓)
1. 本仓:改元素 → chiya 发 npm(registry.npmjs.org)
2. fullstack-plugin:coding-unpkg-sdk/manifest.json 追加新版本号 → MR → SCM build 上传 feishucdnmanifest 条目形如:
{
"@lark-apaas/coding-cd-elements": {
"versions": ["0.1.0"],
"files": ["dist/*.js"],
},
}versions 是数组、多版本并存——升级时追加而非替换,老产物 pin 的旧 URL 继续可用(保留降级能力)。
files 是「目录 + 扩展名」级 glob:在 dist/ 下新增或重命名产物文件不用动 fullstack-plugin(下次发版自动被镜像搬走)。只有改 tsup 的 outDir 目录名才必须同步改这条 glob,而且得写成两条并存(["dist/*.js", "umd/*.js"] 之类)——glob 对 versions 里所有版本一起生效,老版本 tarball 里只有老目录。这是产物目录不轻易改名的主要原因。
⚠️
registry.npmmirror.com(mirror 脚本默认源)同步公网 npm 有延迟。发布脚本末尾虽有npx cnpm sync主动推同步,但那只是触发;SCM 构建环境建议把NPM_MIRROR指向https://registry.npmjs.org(源站无延迟)。
对 docs/conventions.md §9 的两处偏离
§9 的 SDK 模板是给被 import 消费的 npm 包写的,本包消费方是浏览器 <script src>,故:
format: ['iife']而非['esm','cjs']——design-html-sdk-spec.md§2 明令避免type="module"(产物内 Babel 编译基于 classic scripts,window是唯一跨脚本共享机制)- 不出
dts—— 没有 import 型消费者,类型声明无处可用
其余均按 §9:target: 'es2022' 对齐 root tsconfig、src/ 布局、构建前 clean。
License
MIT — see LICENSE.
