上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

[油猴脚本开发指南] 监听B站动态的加载的模块

[复制链接]
  • TA的每日心情
    难过
    3 天前
  • 签到天数: 13 天

    [LV.3]偶尔看看II

    16

    主题

    26

    回帖

    245

    积分

    荣誉开发者

    积分
    245

    荣誉开发者油中2周年生态建设者喜迎中秋

    发表于 2023-9-3 03:16:41 | 显示全部楼层 | 阅读模式

    本帖最后由 Yiero 于 2023-9-3 14:09 编辑

    监听B站的动态加载

    因为最近写的脚本有好几个都涉及[B站动态], 而B站动态是分段加载的, 所以也分享一下这方面的思路.

    如果不想看我扯一大堆的话, 可以直接跳到文章的最后看具体的代码.

    目录

    本文将涉及:

    • 简单使用 MutationObserver .
    • 在使用 MutationObserver 之前, 如何判断加载页面元素的加载逻辑?
    • 自定义事件的派发, 并携带自定义数据
    • 工具类: DynamicLoad

    为了方便读者理解, 本文中的代码统一使用JavaScript, 除了最后的工具类有一个TypeScript版本.

    本文不介绍 MutationObserver 的理论, 如果这个API感到陌生的读者,

    可以查阅 [油猴开发指南-MutationObserver], [MDN-MutationObserver].

    思路

    监听B站动态加载其实很简单,
    直接在动态列表的容器上绑定一个[MutationObserver]就可以,
    不过有一个问题, MutationObserver 返回的 Array<MutationRecord> 中的信息太多了, 如果只是单次处理还好, 多次处理就会非常麻烦.

    基于这一点, 我们可以自己写一个工具类, 用来将MutationObserver返回的信息先进行统一处理, 直接再返回处理后的数据.

    动态列表容器的类: .bili-dyn-list__items

    每一条动态容器的类: .bili-dyn-list__items


    如何使用MutationObserver观察页面的加载逻辑

    由于每个网站的加载逻辑是不同的, 我们需要先观察B站动态的加载逻辑.

    首先打开开发者工具(F12), 进入B站动态首页, 选中任意一个动态,
    可以找到其父元素.bili-dyn-list__item, 这是动态容器.

    还可以找到.bili-dyn-list__item的父元素.bili-dyn-list__items, 这是动态列表容器.

    image-20230903014003022.png

    然后我们一直滚动页面, 直到页面加载新的动态, 可以发现B站的动态是直接在.bili-dyn-list__items动态列表容器的基础上新增Node节点,
    这是最简单的元素加载, 可以直接监听.bili-dyn-list__items容器的Node节点加载即可.

    image-20230903014207780.png

    于是我们绑定页面的动态列表容器, 观察其节点增删:

    new MutationObserver( ( recordList ) => {
        console.log( recordList );
    } ).observe( document.querySelector( '.bili-dyn-list__items' ), {
        childList: true
    } )

    返回的recordList如下:

    image-20230903014715902.png

    经过分析可以发现, 当新的动态加载时, 触发的 MutationObserver 回调中的每条 MutationRecord 中的 addedNodes[0] 就是一条新加载的动态.

    所以, 我们可以通过以下代码获取新加载的动态:

    const dynamicUpdateObserver = new MutationObserver( ( recordList ) => {
        recordList.forEach( record => {
            record.addedNodes.forEach( ( addedNode ) => {
                // 新加载的动态
                const dynamicItem = addedNode;
    
                /* 对新加载的动态进行操作 */
                console.log( dynamicItem );
    
            } );
        } );
    } );
    
    dynamicUpdateObserver.observe( document.querySelector( '.bili-dyn-list__items' ), {
        childList: true
    } )

    整合加载动态

    但是前文也说到, 这种方式处理起来很麻烦, 因为我们无法将新加载的动态节点和 MutationObserver 分离.

    比如我们要处理每一条动态, 当页面初始化的时候,
    使用 const dynamicList = document.querySelectorAll('.bili-dyn-list__item') 获取到了初始化加载的动态 dynamicList (
    NodeList 的类型进行处理), 然后新加载的动态(比如上一小节的获取到的 dynamicItem )我们又要用 Node 的类型处理新加载的动态.

    虽然可以分离出一个函数处理Node, 比如写了一个函数parse(),
    可以通过 dynamicList.forEach( ( dynamicItem ) => { parse( dynamicItem ) } ) / parse( dynamicItem ) 的形式去处理,
    但是毕竟类型不统一还是麻烦, 而且中间隔着一个 MutationObserver , 不好做模块化.


    模块化

    为什么我要强调模块化? 因为本身 MutationObserver 就是一个比较乱的东西, 这不是说这个API不好, 而是他的内容太多太全了,
    像是上面的代码, 为了获取一条新加载的动态, 在回调函数中需要经过两次遍历才能拿到, 后期维护起来就会很麻烦.

    我们应该希望的是, 我们有一个函数( parseController )可以对动态进行解析, 然后当有新动态加载时,
    直接将新的动态列表传给 parseController , 而不是需要通过 MutationObserver 解析出动态之后, 在 MutationObserver 内部
    对动态进行解析.

    简单来说就是, 数据一个层, 逻辑处理一个层:

    /* 逻辑处理层 */
    // 假如有一个函数parse(node), 用于处理动态
    
    // 动态处理控制函数, 用于进行动态的逻辑处理.
    function parseController() {
        // 初始化的动态
        const dynamicList = document.querySelectorAll( '.bili-dyn-list__item' );
        dynamicList.forEach( ( dynamicItem ) => { parse( dynamicItem ) } );
    
        // 新加载的动态, 监听动态更新事件
        window.addEventListener( 'dynamicUpdate', ( e ) => {
            // 返回的事件e中, e.detail就是所有新加载动态的列表. 
            const dynamicList = e.detail;
            dynamicList.forEach( ( dynamicItem ) => { parse( dynamicItem ) } );
        } );
    }

    自定义事件

    基于事件模型系统, 我们可以通过监听某些事件, 异步地在某个时机触发事件. 比如click事件会在用户点击页面时触发一个对应的回调函数,
    再比如上文中的 dynamicUpdate 事件.

    [dispatchEvent] 函数支持我们手动派发一个事件,
    比如: dispatchEvent( new Event('input') ) 可以手动触发一个input事件,
    [元素规则校验和检测的触发] 一文中可以看到其使用场景.

    但是我们需要的 dynamicUpdate 事件并不是js原生的事件, 所以我们需要自定义一个事件:
    通过 [CustomEvent] API可以创建一个带有自定义数据的自定义事件;
    使用 [Event] API只能派发一个可以被监听的事件, 无法携带数据:

    // 自定义数据
    const appendDynamicList = 'CustomData';
    
    // 派发事件`dynamicUpdate`, 第二个参数传入一个对象, 其中的detail键用于传入自定义数据
    window.dispatchEvent( new CustomEvent( 'dynamicUpdate', {
        detail: appendDynamicList
    } ) );
    
    // 监听事件`dynamicUpdate`, 并接受传入的数据
    window.addEventListener( 'dynamicUpdate', ( e ) => {
        const appendDynamicList = e.detail;
        console.log( appendDynamicList )  // -> 'CustomData'
    } );

    自定义事件参考资料: [创建和触发 events]


    数据处理层

    基于自定义事件, 我们就能够在 MutationObserver 事件触发的时候, 再次派发一个事件, 将数据传给逻辑处理层:

    /* 数据处理层 */
    const dynamicUpdateObserver = new MutationObserver( ( recordList ) => {
        // 声明一个数组用于储存新加载的动态, 统一类型为Node[], 并且一次性将新增动态分发而不是每条动态分发一个事件
        const appendDynamicList = [];       // add
    
        // 将新加载的动态转化成Array<Node>的形式
        recordList.forEach( record => {
            // 将新加载的动态放到appendDynamicList上
            appendDynamicList.push( ...Array.from( record.addedNodes ) );   // change
        } );
    
        // 分发事件`dynamicUpdate`, 返回新动态列表
        window.dispatchEvent( new CustomEvent( 'dynamicUpdate', {   // add
            detail: appendDynamicList
        } ) );
    } );
    
    dynamicUpdateObserver.observe( document.querySelector( '.bili-dyn-list__items' ), {
        childList: true
    } );

    具体代码实现

    Ts

    代码

    /** 动态加载类 */
    class DynamicLoad {
        constructor() {
            // 绑定动态更新监听
            this.updateObserver();
        }
    
        /**
         * 获取当前所有动态
         * @Return { Node[] }
         * */
        get items(): Node[] {
            // 为了返回的动态列表风格统一, 将NodeList转成Array<Node>
            return Array.from( document.querySelectorAll( '.bili-dyn-list__item' ) );
        }
    
        /**
         * 动态加载监听器, 当动态更新时(加载), 分发一个事件(dynamicUpdate)提示动态更新, 并返回新加载的动态Node[]
         * */
        private updateObserver(): void {
            const dynamicUpdateObserver = new MutationObserver( ( recordList ) => {
                // 新加载的动态
                const appendDynamicList: Node[] = [];
    
                // 将新加载的动态转化成Array<Node>的形式
                recordList.forEach( record => {
                    appendDynamicList.push( ...Array.from( record.addedNodes ) );
                } )
    
                // 分发事件`dynamicUpdate`, 返回新动态
                window.dispatchEvent( new CustomEvent( 'dynamicUpdate', {
                    detail: appendDynamicList
                } ) )
            } );
    
            dynamicUpdateObserver.observe( <HTMLElement> document.querySelector( '.bili-dyn-list__items' ), {
                childList: true
            } );
        }
    }

    使用

    // 声明DynamicLoad类, 并绑定上页面监听器
    const dynamicLoad = new DynamicLoad();
    
    // 获取已加载的动态
    const loadedDynamicList: Node[] = dynamicLoad.items;
    console.log( loadedDynamicList );
    
    // 监听动态更新事件, 获取新加载的动态
    window.addEventListener( 'dynamicUpdate', ( e ) => {
        const result = e as CustomEvent;
    
        const appendDynamicList: Node[] = result.detail;
        console.log( appendDynamicList );
    } );

    Js

    代码

    /** 动态加载类 */
    class DynamicLoad {
        constructor() {
            this.#updateObserver();
        }
    
        /**
         * 获取当前所有动态
         * @return { Node[] }
         * */
        get items() {
            return Array.from( document.querySelectorAll( ".bili-dyn-list__item" ) );
        }
    
        /**
         * 动态加载监听器, 当动态更新时(加载), 分发一个事件(dynamicUpdate)提示动态更新, 并返回新加载的动态NodeList
         * */
        #updateObserver() {
            const dynamicUpdateObserver = new MutationObserver( ( recordList ) => {
                const appendDynamicList = [];
                recordList.forEach( ( record ) => {
                    appendDynamicList.push( ...Array.from( record.addedNodes ) );
                } );
                window.dispatchEvent( new CustomEvent( "dynamicUpdate", {
                    detail: appendDynamicList
                } ) );
            } );
            dynamicUpdateObserver.observe( document.querySelector( ".bili-dyn-list__items" ), {
                childList: true
            } );
        }
    }

    使用

    // 声明DynamicLoad类, 并绑定上页面监听器
    const dynamic = new DynamicLoad();
    
    // 获取目前已加载的所有动态
    const loadedDynamicList = dynamic.items;
    console.log( loadedDynamicList );
    
    // 监听动态加载
    window.addEventListener( "dynamicUpdate", ( e ) => {
        // 获取新加载的动态
        const appendDynamicList = e.detail;
        console.log( appendDynamicList );
    } );
    已有2人评分好评 油猫币 贡献 理由
    朱焱伟 + 1 + 7 很给力!
    王一之 + 1 + 8 + 1 很给力!

    查看全部评分 总评分:好评 +2  油猫币 +15  贡献 +1 

  • TA的每日心情
    开心
    2024-3-13 10:14
  • 签到天数: 211 天

    [LV.7]常住居民III

    294

    主题

    3906

    回帖

    3826

    积分

    管理员

    积分
    3826

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2023-9-3 14:06:32 | 显示全部楼层
    感谢哥哥的分享!
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    862

    回帖

    1361

    积分

    荣誉开发者

    积分
    1361

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2023-9-3 22:35:59 | 显示全部楼层
    可以对已加载的动态也派发一次事件,这样调用起来统一点,代码不用分成2块处理,就是new得放到后面。
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    昨天 08:38
  • 签到天数: 478 天

    [LV.9]以坛为家II

    0

    主题

    12

    回帖

    203

    积分

    高级工程师

    积分
    203
    发表于 2023-9-6 11:16:01 | 显示全部楼层
    学习了,
    回复

    使用道具 举报

  • TA的每日心情
    开心
    14 小时前
  • 签到天数: 570 天

    [LV.9]以坛为家II

    60

    主题

    486

    回帖

    714

    积分

    专家

    脚本猫首席体验官

    积分
    714

    油中2周年生态建设者新人报道油中3周年挑战者 lv2喜迎中秋

    发表于 2023-10-10 22:23:57 | 显示全部楼层
    ggnb!
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    637

    主题

    5196

    回帖

    6078

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6078

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2024-3-20 01:11:34 | 显示全部楼层
    已收录独立站
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表