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)声明了属性到属性的映射,包括 autoplaycontrolsloopmuted(属性 muted)、preloadsrcstreamType(属性 stream-type)等。每个条目指定 { type: Boolean|String, attribute?, empty? }

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

两层为什么要分开

我读的时候琢磨了一下。第一层(原型遍历)是「自动发现」------host 有啥就代理啥,省得手写。但有些属性的 HTML 名和 JS 名不一致(defaultMuted 对应属性 mutedplaysInline 对应 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> 一样用。下一篇讲这些引擎适配器的全景。

相关推荐
右耳朵猫AI1 小时前
Web前端周刊2026W38 | React 19.3 发布、StyleX 深潜、jsdom 30.1 提速
前端·javascript·react.js·typescript·node.js
落魄大学生之流水线上谋生计1 小时前
GreenLife Carbon · OpenHarmony 智慧低碳生态平台
javascript
人工智能培训2 小时前
大语言模型:从语言理解到通用智能的跃迁
linux·服务器·前端·人工智能
一拳不是超人2 小时前
做独立开发三个月,我的第一个产品终于有了 1000 个用户
前端·程序员
Amos_Web2 小时前
Rspack 源码解析(十):Hash 与 Asset 生成
前端·rust·源码阅读
福兮说2 小时前
用 IndexedDB 存用户的文件,我踩过的五个坑
前端·javascript
闪耀之光M782 小时前
前端依赖自动导入:unplugin-auto-import
前端
Ricon组态薄荷糖2 小时前
Ricon组态系统:工业组件开发指南与实践
前端·后端·物联网
flash俊杰2 小时前
Electron 桌面应用的进程模型:为什么 fork Next.js standalone
前端