Bilibili-Evolved 动态图片平铺展示(legacyFeedsImageViewer)功能源码解析
【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved
Bilibili-Evolved(哔哩哔哩增强脚本)中的"动态图片平铺展示"组件,用于将 B 站动态中左右切换式的图片查看器(图片轮播/滑动切换样式)改回传统的平铺展示方式,让多条图片在卡片中直接纵向排列、一次可见,避免在查看图片时被迫逐张切换。本文以该组件在仓库中的功能文档与 TypeScript 实现为核心,结合动态卡片管理、Vue 数据读取等底层源码,讲解其工作原理、生效范围、两种动态数据结构的处理逻辑,以及它在动态详情页可能稍有延迟的原因,帮助读者理解并复现这一典型的"数据层样式修复"型增强脚本组件。
功能文档与组件定位
功能文档位于 registry/lib/components/feeds/legacy-image-viewer/index.md,全文对功能的描述非常精炼:
将动态中左右切换式的图片改回传统的平铺展示. (在动态详情中可能稍有延迟)
这句话概括了组件的三个关键信息:
- 改造对象:动态(feeds)卡片中的图片区域,即 B 站新样式下采用"左右切换式"(图片查看器 / 轮播)渲染的多图动态;
- 改造方式:改回"传统的平铺展示",即所有图片按顺序纵向平铺在卡片正文中;
- 已知限制:在动态详情页(
/opus/{id}等页面)中,由于数据结构和渲染时机不同,改造可能存在"稍有延迟"。
组件注册代码位于 registry/lib/components/feeds/legacy-image-viewer/index.ts,通过defineComponentMetadata(定义见 src/components/define.ts)声明元数据:
export const component = defineComponentMetadata({ name: 'legacyFeedsImageViewer', displayName: '动态图片平铺展示', tags: [componentsTags.feeds], urlInclude: feedsUrls, entry: () => { /* ... */ }, })name:内部标识legacyFeedsImageViewer,legacy前缀表明其意图是恢复"旧版"(legacy)的平铺图片展示形态;displayName:用户在设置面板中看到的中文名称"动态图片平铺展示";tags:归属"动态"分类(componentsTags.feeds),便于在设置面板按标签筛选;urlInclude:限定组件运行的页面范围,值为feedsUrls;entry:组件入口,页面匹配通过后执行,内部完成对每张动态卡片的图片样式改写。
ComponentMetadata的urlInclude字段语义(见 src/components/types.ts)是"设置匹配的 URL,匹配才运行此组件",因此该组件不会在其他无关页面加载执行。
生效页面范围:feedsUrls
urlInclude: feedsUrls引用的 URL 匹配表定义在 src/core/utils/urls.ts:
/** 含有动态的页面 */ export const feedsUrls = [ ...feedsUrlsWithoutDetail, /^https:\/\/t\.bilibili\.com\//, /^https:\/\/www\.bilibili\.com\/opus\/[\d]+$/, ]其中feedsUrlsWithoutDetail(src/core/utils/urls.ts)为:
/** 除动态详情页以外的含有动态的页面 */ export const feedsUrlsWithoutDetail = [ /^https:\/\/t\.bilibili\.com\/$/, /^https:\/\/space\.bilibili\.com\//, /^https:\/\/live\.bilibili\.com\/(blanc\/)?[\d]+/, ]合并后,组件生效的页面包括:
| 页面 | URL 匹配规则 | 说明 |
|---|---|---|
| 动态广场/首页 | https://t.bilibili.com/ | 动态时间线主页面 |
| 个人空间 | https://space.bilibili.com/* | 用户主页中的动态 Tab |
| 直播间 | https://live.bilibili.com/{roomId}(含blanc/前缀) | 直播间页内嵌的动态卡片 |
| 动态详情页 | https://www.bilibili.com/opus/{id} | 单条动态的详情页,对应文档中"稍有延迟"的场景 |
也就是说,"左右切换式图片改回平铺"这一改造会作用于上述所有包含动态卡片的页面,而不仅是动态主页。
底层机制:forEachFeedsCard 与动态卡片管理器
组件入口的第一步是调用forEachFeedsCard,该函数由动态卡片 API 提供,定义在 src/components/feeds/api/manager/index.ts:
export const forEachFeedsCard = async (callback: FeedsCardCallback) => { if (feedsUrls.every(url => !matchUrlPattern(url))) { return null } const success = await feedsCardsManager.startWatching() if (!success) { console.error('feedsCardsManager.startWatching() failed') return null } const { added } = callback if (added) { feedsCardsManager.cards.forEach(c => added(c)) } feedsCardCallbacks.push({ added: none, removed: none, ...callback }) return feedsCardsManager }其工作流程可以拆解为三步:
- URL 校验:再次用
feedsUrls匹配当前页面,不匹配则直接返回null,实现双保险; - 启动观察:调用
feedsCardsManager.startWatching()开始监听动态卡片的新增与移除。动态卡片是异步加载的,只有通过 MutationObserver 持续观察 DOM,才能在卡片出现的第一时间拿到它; - 注册回调:先对已存在的卡片立即执行
added回调,再把回调挂入feedsCardCallbacks,后续新出现的卡片也会自动执行。
feedsCardsManager的具体实现分为两代(见 src/components/feeds/api/manager/index.ts):
export const feedsCardsManager = (() => { const isV2 = isV2Feeds() if (isV2) { return new FeedsCardsManagerV2() } return new FeedsCardsManagerV1() })()通过isV2Feeds()判断 B 站是否启用了新版动态(v2):它读取 cookiehit-dyn-v2,当其值大于 0 且页面命中动态相关 URL 时使用 v2 管理器,否则回退到 v1 管理器。两代管理器都提供统一的cards列表与startWatching()接口,因此在组件层无需关心当前是 v1 还是 v2 数据,forEachFeedsCard会透明地完成适配。
拿到卡片对象card后,组件通过card.element(动态卡片的 DOM 根元素)获取其 Vue 实例数据。
读取 Vue 数据:getVue2Data
B 站动态页面由 Vue 2 渲染,卡片 DOM 元素上挂载了组件实例。组件使用getVue2Data(card.element)获取对应的数据对象,其实现位于 src/core/utils/index.ts:
/** 尝试获取元素对应的 Vue Data (仅适用于 Vue 2 组件) */ export const getVue2Data = (el: any) => // eslint-disable-next-line no-underscore-dangle el.__vue__ ?? el.parentElement.__vue__ ?? el.children[0].__vue__ ?? el.__vueParentComponent该函数按优先级依次尝试四种途径取到元素关联的 Vue 组件实例:
el.__vue__:元素自身绑定的 Vue 实例;el.parentElement.__vue__:父元素的 Vue 实例;el.children[0].__vue__:第一个子元素的 Vue 实例;el.__vueParentComponent:元素最近的父组件实例。
也就是说,只要卡片 DOM 结构中的任一环节挂有 Vue 实例,就能取到数据源。这也是"数据层改造"型组件的通用取数方式,v1/v2 卡片管理器内部解析卡片类型、作者、点赞转发数据时同样复用了它(见 v1.ts 与 v2.ts)。
普通动态:改写 opus.style
拿到vueData后,组件先处理"普通动态"(时间线卡片),核心逻辑如下(index.ts):
const cardType = vueData?.data?.type const path = cardType === 'DYNAMIC_TYPE_FORWARD' ? 'data.orig.modules.module_dynamic.major.opus.style' : 'data.modules.module_dynamic.major.opus.style' const imageViewerStyle: number | null = lodash.get(vueData, path, null) if (imageViewerStyle === 1) { lodash.set(vueData, path, undefined) return }要点如下:
- 区分转发动态:根据动态类型
data.type判断。转发动态(DYNAMIC_TYPE_FORWARD)的原始内容位于data.orig下,普通动态则直接位于data下; - 定位图片样式字段:两者最终都指向
modules.module_dynamic.major.opus.style。opus是动态内容载体(图文/视频等多媒体内容的通用结构),其中的style字段标记了图片区域的展示形态; - 判定左右切换式:当
style === 1时,即表示当前图片采用的是左右切换式(图片查看器/轮播)渲染; - 执行改写:用
lodash.set(vueData, path, undefined)将style置为undefined,从而触发 Vue 响应式更新,让动态卡片按默认的平铺方式重新渲染图片。置为undefined而非0,是因为 B 站渲染层对"未指定"与"明确为平铺值"的处理不同,undefined会让图片区域回落到传统平铺分支。
这一"读style、改style"的思路与 B 站数据结构解耦,即便字段路径在后续版本中调整,也只需修改path常量即可适配。
动态详情页:重组 paragraphs
普通动态卡片修改的是opus.style,但动态详情页的数据结构不同,它把图片信息放在顶部分区(MODULE_TYPE_TOP)的相册(album)中,需要把相册图片重新塞回正文段落(paragraphs)里。对应代码如下(index.ts):
// 动态详情 const modules = vueData?.data?.modules if (Array.isArray(modules)) { const moduleTop = modules.find(m => m.module_type === 'MODULE_TYPE_TOP') const moduleContent = modules.find(m => m.module_type === 'MODULE_TYPE_CONTENT') const album = moduleTop?.module_top?.display?.album const paragraphs: any[] = moduleContent?.module_content?.paragraphs if (album && paragraphs) { modules.splice(modules.indexOf(moduleTop), 1) paragraphs.push({ align: 0, para_type: 2, pic: { pics: album.pics, style: 1, }, }) } }拆解其流程:
- 查找模块:在
data.modules数组中定位MODULE_TYPE_TOP(顶部分区)和MODULE_TYPE_CONTENT(内容区)两个模块; - 取出相册数据:
module_top.display.album中保存了该动态的相册,album.pics即图片列表; - 取出正文段落:
module_content.paragraphs是动态正文的富文本段落数组; - 移除顶部相册模块:
modules.splice(modules.indexOf(moduleTop), 1)把顶部的相册展示模块从模块列表中删除,相当于移除左右切换式图片查看器的数据源; - 追加平铺段落:向
paragraphs尾部 push 一个图片段落对象:para_type: 2:标记为图片段落;pic.pics: album.pics:复用原相册的图片列表;style: 1:图片段落内的平铺样式;align: 0:默认对齐方式。
通过"删掉顶部查看器模块 + 把图片以段落形式写回正文",详情页的图片被渲染为跟随正文的平铺图片列表。由于这里需要先等待详情页的数据模块(MODULE_TYPE_TOP/MODULE_TYPE_CONTENT)渲染完成、modules数组就绪后才能重组,而卡片管理器的added回调可能在数据尚未完整挂载时即被触发,因此会出现文档中提到的"动态详情中可能稍有延迟"现象——组件入口会继续等待后续回调机会完成改造。
数据流总结与可验证性
整个组件的运行链路可以归纳为:
页面 URL 匹配 feedsUrls ↓ forEachFeedsCard 注册 added 回调(src/components/feeds/api/manager/index.ts) ↓ feedsCardsManager.startWatching() 观察动态卡片出现(v1 / v2 管理器) ↓ card.element → getVue2Data() 获取 Vue 数据(src/core/utils/index.ts) ↓ 普通动态:置 opus.style = undefined,恢复平铺 动态详情:删除 MODULE_TYPE_TOP 相册模块,将 album.pics 追加为 para_type=2 的图片段落验证要点(均可在当前仓库中直接确认):
- 组件元数据、两种改写路径的完整实现见 registry/lib/components/feeds/legacy-image-viewer/index.ts;
feedsUrls的完整匹配规则见 src/core/utils/urls.ts;forEachFeedsCard的回调注册与观察器启动逻辑见 src/components/feeds/api/manager/index.ts;- v1/v2 动态卡片管理器的分流逻辑见 src/components/feeds/api/manager/index.ts,两代实现分别在 v1.ts 与 v2.ts;
getVue2Data的取数优先级见 src/core/utils/index.ts。
使用方式与注意事项
该组件是 Bilibili-Evolved 的内置功能组件,无需额外配置选项。在安装了增强脚本后,打开脚本的设置面板,在"动态"(componentsTags.feeds)分类下即可找到"动态图片平铺展示"并开关启用。启用后,访问动态首页(t.bilibili.com)、个人空间动态、直播间动态或动态详情页(opus/{id})时会自动生效。
实际使用中可关注两点:
- 兼容前提:组件依赖 B 站页面使用 Vue 2 渲染动态卡片,且数据字段结构为
module_dynamic.major.opus/modules[].module_*。若 B 站未来调整动态数据结构(例如opus.style的取值语义变化),组件需要同步更新字段路径; - 详情页延迟:在动态详情页中,改造依赖
modules数组与相册/段落数据就绪,初次进入时可能观察到图片短暂保持左右切换式、随后才切换为平铺,这是数据结构重组所需等待的正常表现,与文档说明一致。
【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考