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

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

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

    [LV.1]初来乍到

    22

    主题

    862

    回帖

    1360

    积分

    荣誉开发者

    积分
    1360

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

    发表于 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的每日心情
    开心
    2024-4-14 00:00
  • 签到天数: 119 天

    [LV.6]常住居民II

    29

    主题

    598

    回帖

    535

    积分

    专家

    积分
    535

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-8-14 13:04:19 | 显示全部楼层
    真的佩服,这种高质量的帖子越多越好啊
    入驻爱发电 让这世界充满爱 https://afdian.net/a/vpannice
    回复

    使用道具 举报

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

    [LV.7]常住居民III

    636

    主题

    5187

    回帖

    6069

    积分

    管理员

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

    积分
    6069

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

    发表于 2022-8-14 13:31:42 | 显示全部楼层
    纳入脚本开发指南可以吗哥哥
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

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

    使用道具 举报

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

    [LV.7]常住居民III

    292

    主题

    3894

    回帖

    3812

    积分

    管理员

    积分
    3812

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

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

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

    使用道具 举报

  • TA的每日心情
    开心
    昨天 18:11
  • 签到天数: 708 天

    [LV.9]以坛为家II

    27

    主题

    733

    回帖

    7215

    积分

    荣誉开发者

    精通各种语言的HelloWord!

    积分
    7215

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

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

    使用道具 举报

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

    [LV.7]常住居民III

    636

    主题

    5187

    回帖

    6069

    积分

    管理员

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

    积分
    6069

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

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

    我现在就去学git
    ....
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

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

    使用道具 举报

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

    [LV.7]常住居民III

    636

    主题

    5187

    回帖

    6069

    积分

    管理员

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

    积分
    6069

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

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

    话说画图的软件哥哥有推荐吗==
    好多都是我手绘的...
    感觉太草了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

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

    使用道具 举报

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

    [LV.7]常住居民III

    292

    主题

    3894

    回帖

    3812

    积分

    管理员

    积分
    3812

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

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

    我一般用这个

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

    使用道具 举报

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

    [LV.1]初来乍到

    22

    主题

    862

    回帖

    1360

    积分

    荣誉开发者

    积分
    1360

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

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

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

    使用道具 举报

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

    [LV.7]常住居民III

    636

    主题

    5187

    回帖

    6069

    积分

    管理员

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

    积分
    6069

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

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

    说起来你可能我不信
    我不知道为啥
    我对git有一种天然的排斥
    甚至svn四五天都能上手
    git我从大学就开始学
    前前后后学了四五遍了
    上手就gg
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

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

    使用道具 举报

    发表回复

    本版积分规则

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