16 · `custom-media-element`:属性拦截与转发

上一篇讲了 HTMLMediaElementHost 的主机-组件模型。但 host 是个普通 JS 对象,不是 Web Component。这篇讲 CustomMediaElement------一个类工厂,把 host 包装成自定义元素,让它能像 <video> 一样用 HTML 属性声明式控制。核心是两层属性转发:HTML 属性 → host 属性 → 真实媒体元素。

它是什么:一个混入工厂

packages/media/src/dom/custom-media-element/custom-media-element.ts:105-108:

ts 复制代码
export function CustomMediaElement(tag, MediaHost) {
  // ...
}

它接收一个 HTML 标签名(如 'video')和一个 MediaHost 构造器(如 HTMLVideoElementHost),返回一个 CustomMedia 自定义元素类。这个类桥接自定义元素 API 和 media host。

比如 <mux-video> 元素就是 CustomMediaElement('video', HlsJsMedia) 的产物------用户写 <mux-video src="...">,它内部转发到 host 和真实 <video>。

内部元素在开放的 Shadow DOM

构造函数(L234-244)会 attachShadow({ mode: 'open' }),模板(getVideoTemplateHTML L26-56)渲染一个 <slot name="media"><video></video></slot>。

get target()(L270-277)解析内部元素,优先级:slotted 子元素(:scope > [slot=media])> 宿主中的 <tag> > shadow root 中的 <tag>。

为什么用开放 Shadow DOM?因为 slotted 模式让外部 CSS 和 light DOM 子元素(<track>/<source>)仍然能正常工作,同时把内部媒体元素隔离。

两层转发策略

属性转发是这篇的重点,有两套机制。

第一层:Host 原型遍历(L152-203)

static #define 内部遍历 MediaHost.prototype 的原型链,为每个属性在 CustomMedia.prototype 上装代理:

方法是(L172-175):

ts 复制代码
config.value = function(...args) { return this.#mediaHost[prop](...args); };

getter(L176-179):

ts 复制代码
config.get = function() { return this.#mediaHost[prop]; };

setter 分两种:

  • 如果属性的 kebab-case 名在 observedAttributes 里(L182-184),setter 通过 toggleAttribute/setAttribute(L186-192)------设置 JS 属性会触及 HTML 属性,走 attributeChangedCallback 路由。
  • 否则直接写 this.#mediaHost[prop] = val(L194-196)。

这个分支很关键:有些属性要走 HTML 属性(因为属性变化会触发浏览器行为),有些直接写 host 。比如 src 走属性(浏览器会触发 loadstart),volume 可以直接写。

第二层:显式属性循环(L205-223)

static properties(L119-135)声明了属性到属性的映射,包括 autoplay、controls、loop、muted(属性 muted)、preload、src、streamType(属性 stream-type)等。每个条目指定 { type: Boolean|String, attribute?, empty? }。

对那些没被第一层遍历连接的属性,第二层定义直接读/写元素属性的 getter/setter:布尔用 hasAttribute,字符串用 getAttribute。

两层为什么要分开

我读的时候琢磨了一下。第一层(原型遍历)是「自动发现」------host 有啥就代理啥,省得手写。但有些属性的 HTML 名和 JS 名不一致(defaultMuted 对应属性 muted、playsInline 对应 playsinline),自动遍历搞不定,需要第二层显式声明。

两层配合:自动的尽量自动,特殊的显式处理。这是务实的工程取舍。

attributeChangedCallback:属性 → 属性

L322-353 处理 HTML 属性变化:

  1. 如果 mediaHostAttrToProp 映射到 host 属性(L323-337),按 typeof this.#mediaHost[prop] 做类型转换(布尔/数字/字符串),写入 host------最终流向附加元素。
  2. 如果属性没被 CustomMedia 观察到但被子类观察(L339-344),提前 return 让子类处理。
  3. 否则对同步目标(syncTargetAttributes = tag !== 'iframe',L346-352),镜像到内部元素:null → removeAttribute,否则 setAttribute。

注意 syncTargetAttributes------iframe/embed(Vimeo 那种)驱动外部播放器,属性不镜像到内部。

子元素同步:<track> 和 <source>

构造函数设了个 MutationObserver(L244 附近)监听 <track>/<source> 子元素,slotchange 时重新附加 + 重新同步。

#syncMediaChildren(L355-385)把 slotted 的 <track>/<source> 克隆到内部原生元素并保持同步。iframe 排除在外。

#enableDefaultTrack(L403-415)为 chapters/metadata 轨道启用 default------因为浏览器不会对 JS 添加的轨道自动启用。这是个浏览器行为的补丁。

事件桥

addEventListener(L296-306)把监听器类型转发到 #mediaHost。#bridgeEvent(L316-320)重建并重新分发事件,除非它已经组合(composed 事件会自动冒泡穿过 shadow 边界,不用桥接)。

这和 06 篇讲的 resolveEventTarget(composedPath 穿透 shadow)是互补的------这里是从内向外转发。

销毁保护:queueMicrotask 检查 isConnected

disconnectedCallback(L287-294)和 element 包的 DestroyMixin(10 篇)思路类似:遵 keep-alive 属性,排 queueMicrotask 检查 isConnected------同步重新挂载(移除后立即插回)不会销毁 host。

不过这里用的是单次 queueMicrotask(不是 DestroyMixin 的双重 rAF)。因为这里保护的是「同步重新挂载」,微任务足够;DestroyMixin 的双重 rAF 保护的是「跨帧 DOM 重组」,更激进。

小结

CustomMediaElement 把 host 包装成可声明式使用的 Web Component。核心是两层属性转发:

  1. 原型遍历自动代理------host 有啥就代理啥。
  2. 显式属性声明------处理名字不一致的特殊属性。
  3. attributeChangedCallback 类型转换------字符串属性转成正确类型写 host。
  4. 子元素克隆同步 ------<track>/<source> 进 shadow。
  5. 事件桥------非 composed 事件手动转发。

这层让 <mux-video>、<hlsjs-video> 这些自定义元素能像原生 <video> 一样用。下一篇讲这些引擎适配器的全景。

相关推荐
用户69371750013844 分钟前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员7 分钟前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端
小兔子22 分钟前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端
IT_陈寒28 分钟前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
guslegend2 小时前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
小宋10212 小时前
A2UI从零实战:Agent安全生成交互界面与事件回传
javascript·人工智能·安全·交互
大包子2 小时前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
海码事务所2 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
乐橙开放平台2 小时前
智慧连锁客流检测和离岗检测怎么对接
大数据·人工智能·笔记·物联网·自动化·音视频·智能家居
田威AI2 小时前
图片内文字翻译的规格:输入输出、保真、自动化、时间与费用
前端·计算机视觉