news 2026/9/19 11:44:51

Bilibili-Evolved 动态图片平铺展示(legacyFeedsImageViewer)功能源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bilibili-Evolved 动态图片平铺展示(legacyFeedsImageViewer)功能源码解析

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,全文对功能的描述非常精炼:

将动态中左右切换式的图片改回传统的平铺展示. (在动态详情中可能稍有延迟)

这句话概括了组件的三个关键信息:

  1. 改造对象:动态(feeds)卡片中的图片区域,即 B 站新样式下采用"左右切换式"(图片查看器 / 轮播)渲染的多图动态;
  2. 改造方式:改回"传统的平铺展示",即所有图片按顺序纵向平铺在卡片正文中;
  3. 已知限制:在动态详情页(/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:内部标识legacyFeedsImageViewerlegacy前缀表明其意图是恢复"旧版"(legacy)的平铺图片展示形态;
  • displayName:用户在设置面板中看到的中文名称"动态图片平铺展示";
  • tags:归属"动态"分类(componentsTags.feeds),便于在设置面板按标签筛选;
  • urlInclude:限定组件运行的页面范围,值为feedsUrls
  • entry:组件入口,页面匹配通过后执行,内部完成对每张动态卡片的图片样式改写。

ComponentMetadataurlInclude字段语义(见 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 }

其工作流程可以拆解为三步:

  1. URL 校验:再次用feedsUrls匹配当前页面,不匹配则直接返回null,实现双保险;
  2. 启动观察:调用feedsCardsManager.startWatching()开始监听动态卡片的新增与移除。动态卡片是异步加载的,只有通过 MutationObserver 持续观察 DOM,才能在卡片出现的第一时间拿到它;
  3. 注册回调:先对已存在的卡片立即执行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 组件实例:

  1. el.__vue__:元素自身绑定的 Vue 实例;
  2. el.parentElement.__vue__:父元素的 Vue 实例;
  3. el.children[0].__vue__:第一个子元素的 Vue 实例;
  4. 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.styleopus是动态内容载体(图文/视频等多媒体内容的通用结构),其中的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, }, }) } }

拆解其流程:

  1. 查找模块:在data.modules数组中定位MODULE_TYPE_TOP(顶部分区)和MODULE_TYPE_CONTENT(内容区)两个模块;
  2. 取出相册数据module_top.display.album中保存了该动态的相册,album.pics即图片列表;
  3. 取出正文段落module_content.paragraphs是动态正文的富文本段落数组;
  4. 移除顶部相册模块modules.splice(modules.indexOf(moduleTop), 1)把顶部的相册展示模块从模块列表中删除,相当于移除左右切换式图片查看器的数据源;
  5. 追加平铺段落:向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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 11:41:33

DeepSeek-R1架构解析:MLA与MoE协同的高效推理实践

简介:本资源是一份面向AI研发工程师、大模型算法研究员及进阶技术学习者的DeepSeek-R1模型架构深度解析PDF,聚焦其在长上下文建模、高效注意力机制与稀疏化结构设计上的核心突破。文档系统梳理了128K超长上下文实现原理(基于YaRN的RoPE扩展&a…

作者头像 李华
网站建设 2026/9/19 11:37:25

RAD Studio 10.4.2 安装避坑指南:从镜像校验到编译验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 11:36:57

区块链+IPFS+SSI构建可验证疫苗凭证系统

简介:本资源是一份面向计算机与信息安全领域研究者及区块链开发者的学术型技术方案,聚焦于利用区块链技术解决疫苗接种证书在跨境流动中的隐私保护、可信验证与去中心化共享难题。文档提出融合智能合约、IPFS与自主主权身份(SSI)的…

作者头像 李华
网站建设 2026/9/19 11:36:09

SparkStreaming Driver HA:Checkpoint恢复与生产落地

你有没有经历过凌晨三点被电话叫醒,打开监控一看:Kafka Lag 以肉眼可见的速度往上飙,SparkStreaming 作业的 Driver 已经不知道什么时候挂掉了?如果你还没有给 Driver 做 HA(High Availability)&#xff0c…

作者头像 李华