进入卷四·播放器核心。这个卷讲
@videojs/core——把 media 契约和 store 状态组合成播放器逻辑的中枢。开篇先讲 v10 最核心的组装机制:「Feature」。看完这篇你会明白,v8 的「插件」在 v10 里变成了什么。
一行代码揭开的真相
先看 packages/core/src/dom/feature.ts 的核心(L15-17):
const definePlayerSlice = defineSlice<PlayerTarget>();
// 重载 1:单参数 = plain slice 形式
export function definePlayerFeature<State>(config: SliceConfig<PlayerTarget, State>) { ... }
defineSlice<PlayerTarget>() 是什么?回忆 12 篇——它是个恒等函数,运行时零成本,纯为类型推断辅助。所以:
Feature 就是 Slice<PlayerTarget, State>,一字不差。
这不是巧合,是 v10 的核心设计决策。「播放器的一个能力」(播放、音量、字幕、全屏……)不是挂在 Player 实例上的插件对象,而是一个标准的 store slice——有 state()(返回状态+动作)和 attach()(绑定副作用)。
这意味着播放器本身就是 combine(...features) 组合出来的一个 store。04 篇追过的生命周期(createPlayer → combine → createStore → attach)里,feature 是最小单元。
PlayerTarget:feature 操作的世界
packages/core/src/dom/player.ts:23-26:
export interface PlayerTarget {
media: Media;
container: MediaContainer | null;
}
每个 feature 的 state() 和 attach() 都拿到这个世界:media 是 13 篇讲的 Media 能力契约对象,container 是承载播放器的 HTML 容器(可为 null——音频播放器可能没有容器)。
后面的类型都是围绕它展开的(player.ts:28-34):
export type PlayerFeature<State> = Slice<PlayerTarget, State>;
export type AnyPlayerFeature = AnySlice<PlayerTarget>;
export type PlayerStore<Features extends AnyPlayerFeature[] = []> =
Store<PlayerTarget, UnionSliceState<Features>>;
UnionSliceState<Features> 把元组里所有 feature 的状态类型做交叉合并——所以 PlayerStore<[typeof playbackFeature, typeof volumeFeature]> 的 state 类型精确包含两者的字段,全程类型推导,不用手写。
两种 Feature 形态:plain 和 configurable
feature.ts 的精髓在它支持两种形态。
plain 形态(所有内置 feature 都用这个)
单参数调用(feature.ts:26-28):
if (arguments.length === 1) {
return definePlayerSlice(config); // 直接就是 slice
}
内置的 playback/volume/time 等 16 个 feature 全是这个形态——一个配置对象,完事。
configurable 形态(为外部扩展预留)
双参数调用(feature.ts:18-21):
definePlayerFeature(config, defaultConfig) // config 的 state/attach 多收一个 config 参数
看类型(feature.ts:9-13):
export interface ConfigurablePlayerFeatureConfig<Config, State>
extends Omit<SliceConfig<PlayerTarget, State>, 'attach' | 'state'> {
state: (ctx: StateContext<PlayerTarget>, config: Config) => State;
attach?: (ctx: AttachContext<PlayerTarget, State>, config: Config) => void;
}
state() 和 attach() 多收一个 config 参数。实现(L30-47)通过 forConfig 闭包把 config 柯里化进去:
const forConfig = (featureConfig) => ({
state: (ctx) => state(ctx, featureConfig), // config 绑进 state
attach: (ctx) => attach?.(ctx, featureConfig),
});
const defaultFeature = forConfig(defaultConfig);
// 返回的 feature 是个函数:
// 不传参 → defaultFeature
// 传参 → forConfig(featureConfig)
返回的 feature 既是个函数(可调用定制),又把 defaultFeature 的 state/attach 拷到自己身上(L43-45)——所以它既能直接当 slice 用,又能 feature({...}) 生成定制版。
最典型的使用者是 orientationLockFeature(26 篇会讲):
orientationLockFeature // 直接用,默认 landscape
orientationLockFeature({ type: 'portrait' }) // 定制成竖屏锁
一个是「拿来就用」,一个是「带配置的工厂」。这种「既是值又是函数」的设计在 JS 里很自然(函数是一等公民),v10 把它用在了 feature 扩展点上。
一个 Feature 长什么样:playback 回顾
拿最简单的 playback 完整看一遍(features/playback.ts:6-54):
export const playbackFeature = definePlayerFeature({
name: 'playback',
state: ({ target }) => ({
paused: true, ended: false, started: false, waiting: false,
play: () => target().media.play(),
pause: () => {
const media = target().media;
if (isMediaPauseCapable(media)) media.pause(); // 谓词守卫
},
togglePaused: () => { /* 翻转 */ },
}),
attach: ({ target, signal, set }) => {
const { media } = target;
if (!isMediaPauseCapable(media) || !isMediaSeekCapable(media) || !isMediaSourceCapable(media)) {
return; // 能力守卫:不支持就静默跳过
}
const sync = () => set({ paused: media.paused, ended: media.ended, /* ... */ });
sync();
listen(media, 'play', sync, { signal });
// ... 7 个事件
},
});
三个组成部分清晰可见:
state()——返回状态字段 + 动作方法。动作闭包通过target()(惰性校验的 getter)访问媒体。注意pause()里也有谓词守卫。attach()——能力守卫 → 定义sync()→ 初始 sync →listen(事件, { signal })。signal——12 篇讲的 base 信号,detach/重 attach 自动清理。
这个「能力守卫 → sync → listen」的骨架贯穿所有 16 个 feature(21 篇逐个拆解)。
为什么这个设计比 v8 的插件好
对比 v8 的 plugin 机制,我能列出几个实质优势:
-
类型推导贯穿。feature 元组 →
UnionSliceState→ store 类型 → selector 返回类型 → UI 组件 props。全链路类型安全,拼错字段名编译期报错。 -
生命周期免费获得。slice 的
attach挂在 store 的 attach 上(04 篇),signal自动清理。v8 插件得自己管dispose()、自己解事件。 -
能力降级内置。谓词守卫 + combine 的错误隔离(12 篇),feature 在不支持的媒体上自动失效,不影响其他 feature。
-
组合即播放器。
combine(playback, volume, time)就是一个「有播放/音量/时间的播放器」。不需要 Player 类——播放器就是 feature 的集合。这是「组合优于继承」的极致体现。 -
状态天然响应式。feature 的 state 就是 store state,
set()触发微任务批处理(11 篇),UI 订阅自动更新。v8 插件改状态得手动触发 UI 刷新。
小结
- Feature =
Slice<PlayerTarget, State>,恒等函数定义,零运行时成本。 - PlayerTarget = { media, container },feature 操作的全部世界。
- 两种形态:plain(内置 feature 全用)和 configurable(外部扩展用,可调用定制)。
- 统一骨架:能力守卫 → sync → listen({ signal })。
下一篇看预设——v10 怎么把 16 个 feature 组装成 video/audio/live 等「开箱即用」的播放器。
483



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



