上一篇建立了全局观,这篇我们钻进每个包的
package.json,看清楚三件事:这个包导出什么、它依赖谁、为什么这么分层。我把 12 个包全翻了一遍,这篇文章就是一份依赖地图——读完它,后面读任何代码都不会迷路。
先看分层全貌
12 个包构成一个干净的分层 DAG(有向无环图)。我画了个框图,从下到上四层:
┌─────────────────────────────────────────────────────────────┐
│ 顶层消费者(平台层) │
│ html react (react-native 不存在) │
├─────────────────────────────────────────────────────────────┤
│ 第三层(领域核心) │
│ core media │
├─────────────────────────────────────────────────────────────┤
│ 第二层(基础设施) │
│ store spf │
├─────────────────────────────────────────────────────────────┤
│ 叶节点(无工作区依赖) │
│ utils element │
├─────────────────────────────────────────────────────────────┤
│ 资源与工具(游离) │
│ icons(私有) skins(私有) cli │
└─────────────────────────────────────────────────────────────┘
下面从最底层开始往上拆。这样读有个好处:看上面一层时,它依赖的东西你都已经认识了。
叶节点层:地基
@videojs/utils — 通用工具根
一句话:纯函数工具集,没有一丁点状态。
依赖:谁都不依赖(连第三方运行时依赖都没有)。
导出方式很有意思:它有 13 个子路径导出,但没有根入口:
./array ./dom ./events ./function ./jwt ./object
./predicate ./percent ./string ./style ./time ./number ./types
意思是消费者必须写成 import { listen } from '@videojs/utils/dom',不能偷懒 from '@videojs/utils'。这是刻意的——为了 tree-shaking。每个主题是独立入口,你只用 DOM 工具,就不会把 JWT 解析那些打进去。./dom 是其中最大的模块(事件监听、聚焦、Shadow DOM、popover 定位、WebKit 兼容)。
为什么是地基:整个 monorepo 几乎所有包运行时都依赖它。它的构建配置用的是 neutralLibraryConfig(单一产物,不像别的包分 dev/default 两份)。后面 05、06 篇会专门拆它。
@videojs/element — 响应式自定义元素基类
一句话:一个故意做小的 Lit 子集。
依赖:不依赖任何工作区包;唯一第三方依赖是 @lit/context。
导出就两个:
. → ReactiveElement, DestroyMixin, 及类型
./context → @lit/context 的透传再导出
为什么不用完整 Lit:核心文件 reactive-element.ts(约 13KB)只保留了 Lit 的「响应式属性 + 更新调度」内核,把模板系统、Shadow DOM、装饰器全砍了。因为 v10 的 UI 元素自己控制渲染(不靠 Lit 模板),只需要那个调度模型。砍完之后体积小很多、心智负担也低。
有个细节值得记:store 把 element 声明成可选 peer——意思是核心状态层不强制依赖元素库。这是个很关键的解耦,后面会反复看到这种「可选依赖让框架中立包真正中立」的手法。08-10 篇会深入它。
第二层:基础设施
@videojs/store — 响应式状态
一句话:目标附加式(target-attached)的响应状态管理。
依赖关系:
| 类型 | 工作区依赖 |
|---|---|
dependencies | @videojs/utils |
peerDependencies(可选) | @videojs/element、react |
devDependencies | @videojs/element |
导出三个子路径:
. → 核心:createState, createStore, defineSlice, combine, createSelector, AbortControllerRegistry
./html → 自定义元素绑定:StoreController, SnapshotController, StoreAccessor
./react → React 绑定:useStore, useSelector, useSnapshot
这个包最特别的地方是「目标附加式」:state 在 target(media/container)存在之前就可以创建,attach(target) 才触发 setup,detach 把状态重置回初始值。状态用 Slice 组合(combine(...slices) 扁平合并),不是 Redux 那种 reducer 树。清理机制是 AbortControllerRegistry(base/supersede/clear 三招),这是它的一等公民。
element/react 为什么是可选 peer:核心 core/ 目录零框架依赖;html 和 react 绑定只是薄适配器。理论上你可以把 store 用在非 Web、非 React 的环境。11、12 篇深入。
@videojs/spf — 流处理框架
一句话:自研的 HLS over MSE 流处理框架,全仓库最大的包。
依赖:@videojs/utils;第三方依赖 signal-polyfill(TC39 Signals 提案的 polyfill)。
导出:
. → 框架原语(信号、Task、Actor、Reactor、createComposition, defineBehavior)
./dom → DOM 便利聚合
./hls → createSimpleHlsEngine, SimpleHlsMediaElement
./media-tracks → 媒体轨道绑定
./background-video → 背景视频引擎变体
代码量:37,746 行,全仓库最大。四层模型:信号 → Reactor → Actor → 平台。Behavior 是组合的通用单元,引擎就是一串 Behavior。
为什么自研:我去翻 internal/design/spf/ 的设计文档,里面的说法是——目标是把「天然不属于一起」的流媒体问题(清单解析、质量选择、分段缓冲、流结束协调)拆成单一职责的层,而不是一个单体播放器。注意 media 包仍然提供 hls.js 适配器作为可选——SPF 是新增的自研路径,不是强制替换。卷五 15 篇全是它。
第三层:领域核心
@videojs/media — 媒体契约
一句话:定义「媒体能干什么」的契约层。
依赖:@videojs/spf、@videojs/utils;第三方依赖 @vimeo/player、dashjs、hls.js、mux-embed(都是可选的引擎适配器)。
导出:
. → 核心:Media 能力契约类型、谓词、MediaError、track 列表实现
./dom → DOM 绑定:HTMLMediaElementHost, video-host, audio-host, custom-media-element
./dom/* → 各引擎适配器(./dom/hls-js, ./dom/dash, ./dom/vimeo, ./dom/mux, ./dom/google-cast)
核心内容:
- 能力型契约:
Media类型由许多小*Capability+*Events接口组合而成。非 HTML 目标(Cast、Vimeo)可以只实现部分接口。 - 谓词守卫:
isMediaPauseCapable(media)等类型守卫。 - Host-Component 模型:
HTMLMediaElementHost委托 getter/setter,通过组件注册表编排引擎。 - 一堆引擎适配器:hls.js、dash.js、native-hls(Safari)、Vimeo、Mux(监控)、Google Cast。
依赖方向:是 core → media,不是反过来。media 不依赖 core。13-18 篇深入。
@videojs/core — 播放器核心
一句话:播放器逻辑的中枢,组合 media 契约 + store 状态 + utils 工具。
依赖:@videojs/media、@videojs/store、@videojs/utils。
导出(代码量第二大,36,576 行):
. → 框架中立的 UI Core 类(PlayButtonCore 等约 30 个)
./dom → DOM 播放器层:PlayerFeature, definePlayerFeature, features 预设, selectors, gesture, hotkey
./i18n → 翻译 API
./i18n/locales/* → 每个语言环境的生成包
./i18n/text/* → 每个文本域的生成包
几个重点:
- Feature 即 Slice:
definePlayerFeature其实就是defineSlice<PlayerTarget>()(恒等函数)。播放器是combine(...features)组成的 Store。 - 无头 UI Core:
src/core/ui/<component>/每个组件有纯逻辑XCore类,不碰 DOM。 - i18n 系统:以英文短语为键,BCP47 回退链,代码生成类型化参数。
- 输入子系统:gesture、hotkey、media-actions。
关键定位:它是中枢,但不含 DOM 渲染——纯逻辑在根 . 子路径,DOM 在 ./dom 子路径。19-26 篇深入。
顶层消费者:平台层
@videojs/html — Web Components 平台
依赖最完整:core + element + media + spf + store + utils;devDep icons、skins。
核心内容:
createPlayer({ features })工厂:返回{ context, create, PlayerController, ProviderMixin, ContainerMixin }。- Store Mixin 三件套:
ProviderMixin(持有 store)、ContainerMixin(注册容器)、MediaAttachMixin(注册媒体),全靠 Lit 风格 Context 连接。 - 约 40 个 UI 元素:每个包装
core里的XCore类。 - SkinElement:拉取共享 CSS + 每个预设的皮肤模块。
为什么 html 依赖 element:它是唯一运行时依赖 @videojs/element 的顶层包——Web Components 需要 ReactiveElement 基类。42-47 篇深入。
@videojs/react — React 平台
依赖:core + media + spf + store + utils;不依赖 element;peerDep react/react-dom(^18 || ^19)。
核心内容:
createPlayer({ features }):返回{ Provider, Container, usePlayer, useMedia }(和 html 的返回对偶)。- Store 集成:用
@videojs/store/react的useStore/useSelector(基于useSyncExternalStore),不是 html 那套StoreController。 createMediaButton工厂:React 版的无头 Core 复用模式。
为什么对等:React 自己管组件生命周期,不需要 element 的 ReactiveElement。但它复用 core 的所有无头 Core 类——这就是「同一逻辑两种渲染」。48-51 篇深入。
资源与工具包:游离层
这三个包不参与主依赖链,单独说一下。
@videojs/icons(私有,不发布)
源是 src/assets/{default,minimal}/ 各 25 个 SVG。构建脚本 scripts/build.ts 用 svgo + @svgr/core 生成多格式模块(react/html/render/element 四套)。被 html 和 react 的 src/icons/ 再导出。58 篇会拆它的代码生成流程。
@videojs/skins(私有,不发布)
纯样式包,无运行时逻辑。src/{default,minimal,shared}/,每个含平行 css/ 和 tailwind/ 树。59、60 篇深入。
@videojs/cli — 文档 CLI
唯一和播放器库运行时完全解耦的包。两个命令:docs <slug>(读打包的 markdown 文档页)和 config(管理配置)。有意思的是它在构建时和文档站耦合——通过 ambient 模块声明把站点的安装代码生成器打包进 CLI。它是文档的「交付通道」,不是构建工具。64 篇深入。
这么分层,到底图什么
把 12 个包放一起看,能提炼出几条很清晰的设计原则。这些原则后面会反复验证:
-
框架中立优先。
utils、element、store、spf、media、core这六个包不含 React 依赖,前三个甚至不含 DOM 依赖。平台行为全推到 html 和 react。这让核心逻辑理论上可以跨平台(含未来的 RN)。 -
依赖单向向下。依赖图是 DAG,没有循环。
core → media → spf → utils是主干,html/react 在顶端消费。 -
可选依赖解耦框架。
store把element和react都声明成可选 peer——这是「让框架中立包真正中立」的关键。 -
私有资源包隔离。icons 和 skins 标记私有,只当 devDep,不进运行时依赖图。
-
CLI 独立。和播放器库零运行时耦合,独立演进。
-
平行 API 设计。html 和 react 的 exports 结构(预设、media、icons、i18n)几乎 1:1 对应,刻意维持对等,降低跨平台心智负担。
理解了这张依赖地图,v10 的骨架就清楚了。下一篇我们看撑起这套架构的三条设计哲学,用真实代码说话。
1696



被折叠的 条评论
为什么被折叠?



