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

[油猴开发指南]关于脚本如何处理iframe的碎碎念

[复制链接]
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    15

    主题

    479

    帖子

    836

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    836

    卓越贡献活跃会员热心会员突出贡献三好学生荣誉开发者喜迎中秋

    发表于 2022-8-14 12:30:36 | 显示全部楼层 | 阅读模式

    这应该是个很经典的问题了,首先要明确的是,iframe可以看做一个独立的页面,每个iframe都有自己的src,这个src就是页面的链接。你可以试着在iframe链接上右键,新标签页打开,就可以看到这个独立页面了。脚本@match可以填iframe链接,此时应当认为代码是在这个独立网页下运行的,处理方式与普通页面无异。如果主页面与iframe页面的处理没有交集,那只需同时@match两个链接,并根据location.href分别处理不同逻辑即可(或者写成2个脚本)。本文主要讨论有交集的情况,即主页面与iframe应当如何交互的问题。

    同源iframe

    如果你还不知道什么是同源跨域,请先阅读此文。同源的交互是比较简单的,所有代码可以全放在主页面下处理。主页面先获取iframe所在的元素,iframe.contentWindow即目标window,iframe.contentDocument即目标document,可以用iframe.contentDocument.querySelector获取iframe内的元素,也可以直接对其进行修改。这里要注意的是,iframe的加载时机与主页面并不同步,特别是运行于document-start阶段的脚本,有可能会出现iframe加载比主页面慢的情况,这时contentDocument会取到null。解决方法很简单,监听一下iframe的load事件即可,为便于处理,我们可以把这个过程封装一下:

    function getIframeDocument(iframe) {
        return new Promise(resolve => {
            if (iframe.contentDocument) {
                resolve(iframe.contentDocument);
            } else {
                iframe.addEventListener('load', e => {
                    resolve(iframe.contentDocument);
                });
            }
        });
    }

    跨域iframe

    对于跨域的情况,iframe.contentDocument无论如何都是null,contentWindow虽然存在,但上面的大多数属性都不可用。跨域访问是有限制的,这种限制不难理解,不过我们的脚本有办法绕过,主要就是靠postMessage和GM_addValueChangeListener两种方法(后者实际上并不适合于iframe,后文细说)。绕过跨域限制并不是说我们可以直接操作另一个页面的元素,而是让脚本同时运行在两个页面下,通过消息机制在不同页面间交换信息,“指挥”另一个页面该做什么。

    postMessage

    先来看看用法:
    targetWindow.postMessage(message, targetOrigin)
    targetWindow 接收消息的window引用
    message 发送的消息内容
    targetOrigin 接收消息的目标origin
    如果主页面要向iframe发消息,targetWindow就是iframe.contentWindow;iframe向主页面发消息,targetWindow则是window.top。message的类型没有限制,但一般推荐用对象,以便于接收方的处理。targetOrigin是一个字符串(不知道是啥可以输出location.origin看看),填'*'表示无限制,否则就必须完全匹配才发送,一般都建议指定明确的origin。这可能是个令人困惑的参数,我都有目标window了,为何还需要origin?这主要是出于安全考虑,举个例子,A网站中有一个iframe B,B向A发敏感消息,某钓鱼网站也嵌套了这个B,如果B不指定origin,那消息就会发到钓鱼网站那里去,从而造成信息泄露。当然如果我们的脚本消息无关紧要,那用通配符也没事。
    接收消息的一方直接监听message事件就行,消息内容在data属性上。下面这个例子展示了b站向百度发消息并影响百度页面的过程(打开百度执行脚本):

    // ==UserScript==
    // @name         跨域交互
    // @description  ...
    // @namespace    ...
    // @author       ...
    // @version      1.0
    // @match        https://www.baidu.com/*
    // @match        https://www.bilibili.com/*
    // @grant        none
    // @run-at       document-idle
    // ==/UserScript==
    
    (function() {
        'use strict';
        if (location.href.includes('baidu')) {
            const iframe = document.createElement('iframe');
            iframe.src = 'https://www.bilibili.com/';
            iframe.style.display = 'none';
            document.body.appendChild(iframe);
            window.addEventListener('message', e => {
                if (e.data.magic === true) {
                    document.body.innerHTML = 'Magic!';
                }
            });
        }
        if (location.href.includes('bilibili')) {
            window.top.postMessage({
                magic: true
            }, 'https://www.baidu.com');
        }
    })();

    可以看到,b站并没有直接操作百度页面(实际上也没有这个权限),而是通过一条消息,让百度自己改变了自己的页面。当然实际运营中百度不会理睬b站的消息,处理消息的实际上是我们的脚本,做出改变的也是脚本,这就是脚本绕过跨域限制的原理。
    上面的例子如果我们反过来,让百度向b站发消息会怎样(还记得iframe.contentWindow吗)?测试一下就会发现,iframe根本收不到。这其实是运行时机的问题,当iframe刚刚创建时,脚本还没有注入(注意iframe中的脚本与主页面的脚本是不同实例),监听是在注入后才开始的,而消息早在注入前就已发出,所以被错过了。解决方法也不难,让iframe先向主页面发个消息,告诉他我准备好了,主页面收到以后再发送相关指令即可。
    现在来设想一个比较复杂的情况:脚本运行于所有域名,我们需要让所有iframe都获取到主页面的标题(或者是别的什么有用的东西),应该怎么做?这个问题好像不难,主页面下window.frames保存了所有iframe的window引用,遍历一下逐个发送就行了。这个做法的缺陷在于没有考虑iframe嵌套的情况,是的,iframe是可以嵌套iframe的,而window.frames只保存第一级的子页面,再往下就没有了。相比之下,iframe的window.top直接指向最顶级的主页面(window.parent才是上一级),所以这个问题应该反过来处理,由iframe向主页面发消息,主页面收到后逐个回应。剩下的问题是怎么回应,即主页面怎么得到targetWindow,这也很好解决,在收到消息时,e.source即消息来源方的window,基于这一点,一个双向通信就可以被建立起来了:

    // ==UserScript==
    // @name         获取主页面标题
    // @description  ...
    // @namespace    ...
    // @author       ...
    // @version      1.0
    // @include      *
    // @grant        none
    // @run-at       document-idle
    // ==/UserScript==
    
    (function() {
        'use strict';
        if (window === window.top) {
            window.addEventListener('message', e => {
                if (e.data.myMessage && e.data.myMessage.command === 'getTitle') {
                    e.source.postMessage({
                        myMessage: {
                            command: 'sendTitle',
                            data: document.title
                        }
                    }, '*');
                }
            });
        } else {
            window.addEventListener('message', e => {
                if (e.data.myMessage && e.data.myMessage.command === 'sendTitle') {
                    console.log("主页面标题是:" + e.data.myMessage.data);
                }
            });
            window.top.postMessage({
                myMessage: {
                    command: 'getTitle'
                }
            }, '*');
        }
    })();

    GM_addValueChangeListener

    GM_addValueChangeListener是油猴提供的API,他无需取得目标window,而是靠监听存储空间的变化(通过GM_setValue改变),由于同一个脚本共用同一存储空间,只要主页面和iframe轮流修改数据,监听数据的变化就约等于是在通信(就好比在同一块留言板上聊天)。
    GM_addValueChangeListener(name, callback)
    name 数据名称,就是GM_setValue的第一个参数
    callback 回调函数
    回调函数的参数:
    name 数据名称
    oldValue 旧值
    newValue 新值
    remote 修改是否来自另一个脚本实例
    对于第4个参数,什么叫另一个实例呢?iframe中的脚本与主页面的脚本就被认为是不同实例,可以据此来判断修改来源。当主页面监听value变化时,他自己写的“留言”也会被自己监听到,判断remote就可以过滤掉自己的消息。这看起来没有问题,那如果页面中有2个iframe会怎样?主页面需要分别对2个iframe做出应答,如何判断消息来自谁?发给谁?这就是GM_addValueChangeListener的局限性,当“参会”人员超过2个时,身份识别就会变得困难起来。鉴于消息是可以自定义的,所以有一个解决思路就是:在发送的消息里附带身份信息,比如把location.href放进消息里,这总不会弄混吧?为了回答我是谁和发给谁的问题,自身和目标的href都得放进去。这样虽然可行,但我们的消息会变得很臃肿,代码上也需要做额外的判断,不可谓不麻烦。
    还有另一个隐患:假如我们在不同标签页打开同一个网站,所有这些对value的修改都是在同一存储空间里进行,GM_addValueChangeListener会同时观察到所有修改,这时候依据href来识别身份就不再可靠了,你该如何判断链接来自哪个标签页?这个问题基本无解,即使有办法,也只能往消息里塞进更多内容,制造更多麻烦,结果上只是对postMessage拙劣的模仿。这就是为什么我一开始说,GM_addValueChangeListener做iframe交互并不合适。事实上,他更适用于你无法获取到window的情况,比如跨标签页的交互,而且最好是一对一的交互。

    postMessage也能跨标签页?

    答案是可以,但仅限于你用window.open打开的标签页。window.open是有返回值的,就是新标签页的window,自然也可以拿来postMessage。这里要注意的问题是,一个新页面从打开到脚本注入是需要时间的(即使是document-start),如果你open完马上postMessage,另一边肯定收不到,听起来是不是跟前面的iframe一样?解决方式也一样,由被打开的页面向主页面发消息即可,window.opener指向主页面。

    总结

    postMessage的前提是取得目标window的引用,由于目标明确,在跨域交互上更可靠,同时要注意两边加载时机的问题,应由后加载的一方先发消息。
    GM_addValueChangeListener适用于无法取得window的情况,虽然使用上更自由,但局限性也更大。

    已有2人评分好评 油猫币 贡献 理由
    朱焱伟 + 1 + 7 赞一个!
    王一之 + 1 + 8 + 1 很给力!

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

  • TA的每日心情
    开心
    7 天前
  • 签到天数: 22 天

    [LV.4]偶尔看看III

    12

    主题

    296

    帖子

    231

    积分

    高级工程师

    Rank: 6Rank: 6

    积分
    231

    活跃会员热心会员三好学生

    发表于 2022-8-14 13:04:19 | 显示全部楼层
    真的佩服,这种高质量的帖子越多越好啊
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-14 13:31:42 | 显示全部楼层
    纳入脚本开发指南可以吗哥哥
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:55
  • 签到天数: 100 天

    [LV.6]常住居民II

    171

    主题

    2253

    帖子

    2299

    积分

    管理员

    Rank: 10Rank: 10Rank: 10

    积分
    2299

    荣誉开发者喜迎中秋热心会员活跃会员突出贡献三好学生管理员家财万贯

    发表于 2022-8-14 13:38:28 | 显示全部楼层
    李恒道 发表于 2022-8-14 13:31
    纳入脚本开发指南可以吗哥哥

    不如先整理整理
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

  • TA的每日心情
    开心
    昨天 09:42
  • 签到天数: 206 天

    [LV.7]常住居民III

    25

    主题

    669

    帖子

    6745

    积分

    荣誉开发者

    精通各种语言的HelloWord!

    Rank: 10Rank: 10Rank: 10

    积分
    6745

    活跃会员三好学生热心会员荣誉开发者家财万贯喜迎中秋

    发表于 2022-8-14 15:02:46 | 显示全部楼层
    学知识的一天
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-14 15:19:04 | 显示全部楼层

    我现在就去学git
    ....
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-14 15:19:26 | 显示全部楼层

    话说画图的软件哥哥有推荐吗==
    好多都是我手绘的...
    感觉太草了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:55
  • 签到天数: 100 天

    [LV.6]常住居民II

    171

    主题

    2253

    帖子

    2299

    积分

    管理员

    Rank: 10Rank: 10Rank: 10

    积分
    2299

    荣誉开发者喜迎中秋热心会员活跃会员突出贡献三好学生管理员家财万贯

    发表于 2022-8-14 15:21:40 | 显示全部楼层
    李恒道 发表于 2022-8-14 15:19
    话说画图的软件哥哥有推荐吗==
    好多都是我手绘的...
    感觉太草了

    我一般用这个

    https://draw.io/
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

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

    [LV.1]初来乍到

    15

    主题

    479

    帖子

    836

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    836

    卓越贡献活跃会员热心会员突出贡献三好学生荣誉开发者喜迎中秋

    发表于 2022-8-14 20:23:53 | 显示全部楼层
    李恒道 发表于 2022-8-14 13:31
    纳入脚本开发指南可以吗哥哥

    可以可以,哈哈,我记得你好几月前就说过要整理,结果到现在只有新建文件夹
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 23:59
  • 签到天数: 88 天

    [LV.6]常住居民II

    386

    主题

    3405

    帖子

    3388

    积分

    管理员

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

    Rank: 10Rank: 10Rank: 10

    积分
    3388

    喜迎中秋国庆纪念章荣誉开发者家财万贯管理员

    发表于 2022-8-15 03:32:40 | 显示全部楼层
    cxxjackie 发表于 2022-8-14 20:23
    可以可以,哈哈,我记得你好几月前就说过要整理,结果到现在只有新建文件夹 ...

    说起来你可能我不信
    我不知道为啥
    我对git有一种天然的排斥
    甚至svn四五天都能上手
    git我从大学就开始学
    前前后后学了四五遍了
    上手就gg
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

    发表回复

    本版积分规则

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