Bilibili-Evolved 三连触摸支持组件解析:为视频页面长按点赞启用触摸交互
【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved
导读
本文聚焦 Bilibili-Evolved 中名为touchComboLike(三连触摸支持)的触摸组件,讲解它如何在触屏设备上为视频页面的“三连操作”(长按点赞)补齐触摸交互能力。读完本文,你将理解该组件如何借助轮询选择器定位点赞按钮、如何用触摸事件模拟鼠标事件序列,以及它的默认开启策略与生效页面范围,并掌握组件元数据(index.md+index.ts)在仓库中的协作方式。
一、组件定位:为什么需要“三连触摸支持”
B 站视频页面的“三连”操作依赖长按点赞按钮触发:按住点赞图标不放,会依次点亮“点赞—投币—收藏”,松开即完成三连。这一交互在桌面端由鼠标mousedown/mouseup事件驱动,但在触屏设备上,浏览器只会产生touchstart/touchend等触摸事件,原生页面若没有对触摸事件的适配,长按点赞将无法正常工作。
Bilibili-Evolved 在registry/lib/components/touch/combo-like/目录下提供了专门解决该问题的组件。其官方描述(即该组件目录下的 index.md)仅一句话,却精准概括了全部功能:
为视频页面中的三连操作(长按点赞)启用触摸支持。
这一描述并不是手写的,而是由仓库的构建工具链在编译期自动注入的:webpack/inject-metadata/description.ts会在组件入口index.ts旁边存在index.md时,将其内容作为该组件的description注入元数据(index.{language}.md则对应注入多语言描述)。也就是说,index.md 既是本文的主题文档,也是最终呈现在设置面板中的组件说明文字。
二、源码总览:入口函数与组件元数据
组件的完整实现位于 registry/lib/components/touch/combo-like/index.ts,结构分为两部分:
entry异步函数:组件被启用后执行的逻辑主体;defineComponentMetadata声明的组件元数据:通过 src/components/define.ts 中的defineComponentMetadata泛型函数定义,用于声明名称、显示名、标签、默认开启状态与生效 URL 范围。
export const component = defineComponentMetadata({ name: 'touchComboLike', displayName: '三连触摸支持', tags: [componentsTags.touch], enabledByDefault: navigator.maxTouchPoints > 0, entry, urlInclude: videoUrls, })元数据各字段的含义如下:
| 字段 | 值 | 说明 |
|---|---|---|
name | touchComboLike | 组件的唯一标识,用于设置存储与运行时检索 |
displayName | 三连触摸支持 | 设置面板中展示的名称 |
tags | [componentsTags.touch] | 归属“触摸”标签,在设置面板中按标签分类展示 |
enabledByDefault | navigator.maxTouchPoints > 0 | 仅当设备支持触摸时才默认开启 |
entry | entry函数 | 组件启用后运行的入口逻辑 |
urlInclude | videoUrls | 仅在匹配的视频类页面生效 |
其中componentsTags.touch定义于 src/components/types.ts:显示名“触摸”、颜色#78909C、图标mdi-gesture-tap-button、排序order: 6。
默认开启策略:设备能力检测
enabledByDefault: navigator.maxTouchPoints > 0是一个关键设计:只有当前设备具备触摸能力时才默认开启。navigator.maxTouchPoints返回设备支持的最大同时触摸点数,桌面鼠标用户在绝大多数情况下该值为 0,因此组件不会默认启用,避免多余的事件监听开销;而平板、手机等触屏设备则会默认开启,开箱即用。该值仅决定默认状态,用户仍可在设置面板中手动调整(参考 ComponentMetadata 中enabledByDefault与configurable的定义)。
生效范围:videoUrls 的覆盖分析
urlInclude: videoUrls决定了组件只会在包含视频的页面上运行。videoUrls定义于 src/core/utils/urls.ts:
export const videoUrls = [ '//www.bilibili.com/video/', ...festivalUrls, // /\/\/www\.bilibili\.com\/festival\// ...mediaListUrls, // 稍后再看、收藏夹连播、UP 主连播等 ]展开后覆盖以下几类页面:
- 普通视频页:
//www.bilibili.com/video/ - 拜年纪等节日活动页:
/\/\/www\.bilibili\.com\/festival\// - 稍后再看页:
//www.bilibili.com/medialist/play/watchlater与//www.bilibili.com/list/watchlater - 收藏夹连播页:
//www.bilibili.com/medialist/play/ml与//www.bilibili.com/list/ml - UP 主视频连播页:
/\/\/www\.bilibili\.com\/medialist\/play\/\d+/与/\/\/www\.bilibili\.com\/list\/\d+/ - 其他合集类页面:
/\/\/www\.bilibili\.com\/list\//
在组件加载阶段,loadComponent(src/components/component.ts)会先校验urlInclude与当前页面 URL 的匹配结果(内部使用matchUrlPattern),不匹配则跳过执行,从而保证该组件不会在直播间、动态等无关页面空转。
三、核心实现原理:用触摸事件驱动鼠标事件序列
entry函数的完整逻辑如下(registry/lib/components/touch/combo-like/index.ts):
const entry = async () => { const { select } = await import('@/core/spin-query') const likeButton = (await select(':is(.ops, .video-toolbar-v1) span.like')) as HTMLElement if (!likeButton) { return } likeButton.style.userSelect = 'none' const mountEvent = (name: string, args: EventInit) => { const event = new CustomEvent(name, args) likeButton.dispatchEvent(event) } const clickInterval = 200 let click = true likeButton.addEventListener('touchstart', e => { e.preventDefault() click = true setTimeout(() => (click = false), clickInterval) mountEvent('mousedown', e) }) likeButton.addEventListener('touchend', e => { e.preventDefault() mountEvent('mouseup', e) if (click) { mountEvent('click', e) } }) }3.1 动态查找点赞按钮:spin-query 轮询
视频页的点赞按钮是页面异步渲染出来的,直接查询可能拿不到元素。因此组件先动态导入select(来自 src/core/spin-query.ts),用选择器':is(.ops, .video-toolbar-v1) span.like'轮询等待目标元素出现——该选择器同时兼容新旧两版视频工具栏(.ops为旧版操作栏,.video-toolbar-v1为新版工具栏),再定位其中的span.like点赞按钮。
select底层是基于SpinQueryConfig的轮询器:默认每1000ms查询一次、最多重试15次(见 src/core/spin-query.ts),超时返回null。因此源码中if (!likeButton) return用于处理“页面结构变化导致找不到按钮”的兜底场景,保证组件静默退出、不抛错。
3.2 禁用文本选择
likeButton.style.userSelect = 'none'用于阻止触摸长按时触发浏览器原生的文本选择行为,避免长按过程中出现选中高亮干扰交互,这是移动端“长按=自定义操作”场景下的常见预处理。
3.3 事件桥接:touch 事件 → mouse 事件
mountEvent是一个轻量的事件桥接器:它基于触摸事件对象构造同名CustomEvent,并重新派发到点赞按钮上:
touchstart触发时派发mousedown;touchend触发时先派发mouseup,若判定为“短按”则再补发一次click。
这样,页面上原本只监听mousedown/mouseup/click的三连逻辑(以及点赞、投币等常规点击逻辑)无需任何改动,就能被触摸操作驱动。
3.4 长按判定:200ms 时间窗
组件通过clickInterval = 200(毫秒)区分“短按点赞”与“长按三连”:
touchstart时先把click置为true,并启动一个 200ms 的定时器,超时后把click置为false;- 若
touchend在 200ms 内到达,视为短按,补发click(对应普通点赞); - 若按住超过 200ms 才松开,
click已为false,则只派发mouseup——此时页面原生的长按三连逻辑已经在mousedown持续期间被触发,无需再补发click,从而避免三连后又被误判为一次普通点击。
同时,touchstart/touchend处理器都调用了e.preventDefault(),阻止浏览器在触摸时合成“模拟鼠标事件”与长按菜单,确保整个事件流完全由组件接管,行为可控、可预测。
四、组件如何被加载与启用
Bilibili-Evolved 的组件统一由 src/components/component.ts 中的loadComponent加载:脚本启动后会遍历全部组件,先校验开关状态(isComponentEnabled)与 URL 匹配,再执行component.entry({ settings, metadata, coreApis })。对本组件而言:
- 用户开启“三连触摸支持”(默认依据设备能力决定);
- 当前页面 URL 命中
videoUrls; entry被调用,动态加载spin-query并轮询定位点赞按钮;- 按钮出现后挂载
touchstart/touchend监听,触摸交互即刻生效。
若用户中途关闭组件,由于本组件未定义unload钩子,loadComponent中if (component.reload && component.unload)的热重载分支不会介入,关闭后仅需刷新页面即可完全移除监听。
五、同类触摸组件的横向参考
touchComboLike是仓库中“触摸”标签(componentsTags.touch)下的成员之一。同属该标签的还有(均可在设置面板的触摸分类中找到):
double-click-control(registry/lib/components/touch/double-click-control/index.ts):触摸场景下的双击控制适配;mini-player(registry/lib/components/touch/mini-player/index.ts):小窗播放器触摸拖拽(其 touch-move.ts 中通过e.touches[0]读取触点坐标实现拖动);player-gestures(registry/lib/components/touch/player-gestures/index.ts):播放器手势控制;player-control(registry/lib/components/touch/player-control/index.ts):播放器控制条触摸优化。
它们共同构成了 Bilibili-Evolved 面向触屏设备的体验增强体系,而本文所述组件负责其中最基础的“三连”手势。
六、小结与使用建议
- 组件位置:registry/lib/components/touch/combo-like/index.ts,描述文档见同目录 index.md;
- 开启方式:触屏设备默认开启;桌面设备可在设置面板中手动开启(
设置 → 组件 → 触摸 → 三连触摸支持); - 生效页面:所有命中
videoUrls的视频类页面(普通视频、稍后再看、收藏夹连播、UP 主连播、合集页、节日活动页); - 实现要点:
select轮询定位按钮 → 禁用userSelect→ 以 200ms 时间窗区分短按与长按 → 用CustomEvent将触摸事件桥接为mousedown/mouseup/click。
理解这一组件,不仅有助于排查触屏设备上“长按点赞无反应”类问题,也为编写同类“触摸事件→鼠标事件”适配逻辑提供了可直接复用的模式参考。
【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考