最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    正文概述 掘金(那猫小帅)   2021-01-24   500

    0. 痛点

    做后台时,不可避免的会遇到 内嵌 iframe 的情况。 最近的 一个项目 有客户反馈无法保存,提示 token 错误。 经过沟通发现 是因为打开了 多个内嵌页(iframe),会出现此问题(使用Thinkphp5自带方法 token(),每次调用都会生成新的Token)。 修复方法也很简单,直接在 新增内嵌页时,将新生成的token进行广播。

    使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    这种情况在 后台 开发中会经常遇到,把 代码 提取出来,做了一下简单的封装,做成开源项目FrameController.js。

    考虑到后台可能要兼容非常老的平台,特别针对 IE7、IE8 做了兼容处理,基本可以实现所有浏览器的兼容。

    另外本源码不依赖任何JS类库,可以拿来即用。

    1. 原理

    原理非常简单,一个简单的 事件订阅、发布流程而已。

    为了方便调用,统一调用同一个js,通过 window.top == window.self 来判断是子窗口或父窗口。

    1.1 父窗口

    数据模型

    var data = {
        events: { //事件
          _PAGE_ID:{
          	EVENT_NAME_1: function(){},
            EVENT_NAME_2: function(){}
          }
        },
        counter: {//事件计数器
          _PAGE_ID:{
            EVENT_NAME_1: 1, //EVENT_NAME_2只绑定了一次
            EVENT_NAME_2: 5 //EVENT_NAME_2绑定了5次,所以会重复执行5次
          }
        },
        _count: 1 //窗口ID
      }
    

    其中窗口ID通过 getId() 生成。

    获取窗口ID

    /**
     * 获取新的窗口编号
     */
    var getId = function() {
        return 'frame_' + data._count++;
    };
    

    收集事件

    /**
     * 添加监听事件
     */
    var addListener = function(event, func) {
    
        var _events = data.events,
            _counter = data.counter,
            _id = this.frameId;
    
        if (!(_id in _events)) {
            _events[_id] = {};
            _counter[_id] = {};
        }
    
        if (!(event in _events[_id])) {
            _events[_id][event] = {};
            _counter[_id][event] = 1;
        }
    
        _events[_id][event][_counter[_id][event]++] = func;
    };
    

    移除事件

    /**
     * 删除监听事件
     * 如果不指定func会删除所有本类型的事件
     */
     var removeListener = function(event, func) {
        var _events = data.events,
            _id = this.frameId;
    
        if ((_id in _events) && (event in _events[_id])) {
            var _funcs = _events[_id][event];
            for (var _funcId in _funcs) {
                if (_funcs[_funcId] == func || !func) {
                    delete _funcs[_funcId];
                    if (!!func) {
                        //如果指定 func,只会删除一个
                        break;
                    }
                }
            }
        }
    };
    

    广播事件

    /**
     * 事件广播
     */
    var broadcast = function(event, value) {
    
        var _events = data.events,
            count = 0;
    
        if (topFrameId === this.frameId) {
            value = {
                type: topFrameId,
                target: window,
                data: value,
                frameId: this.frameId
            };
        }
    
        for (var _frameId in _events) {
            if (_frameId != value.frameId && (event in _events[_frameId])) {
                for (var _funcId in _events[_frameId][event]) {
                    _events[_frameId][event][_funcId](value);
                    count++;
                }
            }
        }
    
        return count;
    
    };
    

    导出

    定义 FrameController,方便子窗口调用。

    window.FrameController = {
        frameId: topFrameId,
        broadcast: broadcast,
        addListener: addListener,
        removeListener: removeListener,
        removeAllListener: removeAllListener,
        getId: getId
    }
    

    为了避免变量冲突,可以通过闭包的方式只定义FrameController,具体见FrameController.js中的源码。

    1.2 iframe窗口

    现在就可以拿到父窗口的FrameController,用addListener函数添加事件了。

    获取子窗口数据

    var TopController = window.top.FrameController, //得到父窗口
        frameData = { //iframe页面数据
            frameId: TopController.getId()  //iframe窗口ID
        };
    

    事件添加删除再封装

    到这里基本功能已经都完成了,还存在的小问题就是无论哪个绑定的事件都会绑定在父窗口,iframe窗口中使用的时候需要用call改变下this的执行

    var bindFrameData = function(func) {
    	// 注意:老版本IE不支持bind,需要这样写
        return function(event, data) {
            func.call(frameData, event, data);
        };
    }
    var addListener = bindFrameData(TopController.addListener)
    var removeListener = bindFrameData(TopController.removeListener)
    

    广播事件

    重写broadcast的方法,自动附带iframe页面数据数据

     //广播
    var broadcast = function(event, value) {
        return TopController.broadcast.call(frameData, event, {
            event: event,
            type: 'child',
            target: window,
            data: value,
            frameId: frameData.frameId
        });
    };
    

    导出

    同样也用TopController保存所有方法

    var TopController = {
        broadcast: broadcast,
        addListener: bindFrameData(TopController.addListener),
        removeListener: bindFrameData(TopController.removeListener),
        removeAllListener: bindFrameData(TopController.removeAllListener),
        count: getCount
    }
    

    1.3 系统内置事件

    现在已经可以通过 TopController的API方法调用了,可以在iframe初始化时增加系统级事件,方便用户调用

    为了兼容更多的浏览器,先抹平系统addEventListener的事件名差异

    //窗口加载或关闭
    var listenerName = 'attachEvent';
    var listenerPrefix = 'on';
    if ('addEventListener' in window) {
        listenerName = 'addEventListener';
        listenerPrefix = '';
    }
    

    窗口增加事件

    //窗口注册事件
    window[listenerName](listenerPrefix + 'load', function() {
        FrameController.broadcast('frame.add', {
            msg: '新增窗口'
        });
    });
    

    窗口关闭事件

    //窗口关闭事件
    window[listenerName]('unload', function() {
        //窗口关闭事件
        FrameController.broadcast('frame.remove', {
            msg: '关闭窗口'
        });
    });
    

    iframe窗口关闭后,注册的事件还在,所以unload时也需移除下(对使用者来说是自动的)

    window[listenerName]('unload', function() {
        //窗口关闭事件
        bindFrameData(TopController.removeAllListener);
    });
    

    统计iframe个数

    我们可以在iframe页面加载时增加frame._online事件,然后调用frame._online计算执行次数就可以得到打开的iframe窗口的个数(广播事件本窗口不会执行,还需要手动+1)。

    //计数事件,仅用于统计框架数
    window[listenerName](listenerPrefix + 'load', function() {
        FrameController.addListener('frame._online', function() {});
    });
    
    //获取窗口数量
    var getCount = function() {
        return FrameController.broadcast('frame._online') + 1;
    };
    

    2. 使用说明

    在编写例子之前,先回一下消息体结构

    {
        event: '事件名称',
        type: 'child',
        target: '内嵌页的window',
        data: '传递的数据,即FrameController.broadcast(event, data)的data',
        frameId: '内嵌页标志'
    }
    

    2.1 某个iframe页面给所有其他页面发送通知

    使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    var addLog = function(from, event, data) {
        var _old = $('#log').html().substring(0, 3000);
        $('#log').html(
            (logTpl + _old)
            .replace('#EVENT#', event)
            .replace('#DATA#', JSON.stringify(data))
            .replace('#SOURCE#', from)
        );
        console.log('event:', event, 'data:', data);
    };
     
    //同步通知
    FrameController.addListener('broadcast', function(e) {
        $('#msg').val(e.data.msg);
        addLog(e.frameId, e.event, e.data);
    });
     
    //发送广播
    $('#send').click(function() {
        var nums = FrameController.broadcast('broadcast', {
            msg: $('#msg').val()
        });
        $('#log').html('通知成功:' + nums + '\n\n' + $('#log').html());
    });
     
    //更新输入状态
    $('#msg').change(function() {
        FrameController.broadcast('change', {
            text: '输入框内容已更改:' + $(this).val()
        });
    });
     
    //更新状态
    FrameController.addListener('change', function(e) {
        addLog(e.frameId, e.event, e.data);
    });
    

    2.2 新增iframe、关闭iframe后,其他iframe接收通知

    使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    //监听系统事件
    var addLog = function(from, event, data) {
        var _old = $('#log').html().substring(0, 3000);
        $('#log').html(
            (logTpl + _old)
            .replace('#EVENT#', event)
            .replace('#DATA#', JSON.stringify(data))
            .replace('#SOURCE#', from)
        );
        console.log('event:', event, 'data:', data);
    };
     
    //监听系统事件
    FrameController.addListener('frame.remove', function(e) {
        addLog(e.frameId, e.event, e.data);
    });
    FrameController.addListener('frame.add', function(e) {
        addLog(e.frameId, e.event, e.data);
    });
    

    2.3 添加自定义事件

    使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    var logTpl = '事件:#EVENT# 来源:#SOURCE#\n数据:#DATA#\n\n',
        addLog = function(from, event, data) {
            var _old = $('#log').html().substring(0, 3000);
            $('#log').html(
                (logTpl + _old)
                .replace('#EVENT#', event)
                .replace('#DATA#', JSON.stringify(data))
                .replace('#SOURCE#', from)
            );
            console.log('event:', event, 'data:', data);
        },
        msgEventListener = function(e) {
            $('#log').html('自定义事件已经触发,添加多次会触发多次\n\n' + $('#log').html());
        };
     
     
    //添加自定义事件
    $('#add_custom').click(function() {
        FrameController.addListener('broadcast', msgEventListener);
    });
     
    //删除自定义事件
    $('#remove_custom').click(function() {
        FrameController.removeListener('broadcast', msgEventListener);
    });
    

    2.4 处理ThinkPHP Token

    //注意:依赖jQuery
    $(function(){
      //模拟ThinkPHP Token
      $('input[name=__token__]').val(new Date().getTime());
    
      //ThinkPHP Token 广播
      FrameController.broadcast('token', {
          token: $('input[name=__token__]').val(),
      });
    
      //收到 ThinkPHP Token 处理
      FrameController.addListener('token', function(e) {
          $('input[name=__token__]').val(e.data.token);
      });
    });
    

    这样最后打开的页面中的token会自动同步到所有页面

    2.5监听系统事件

    //监听系统事件
    FrameController.addListener('frame.remove', function(e) {
        console.log(e.frameId, e.event, e.data);
    });
    FrameController.addListener('frame.add', function(e) {
        console.log(e.frameId, e.event, e.data);
    });
    
    //获取窗口个数
    $('#count').click(function() {
        alert('当前打开 ' + FrameController.count() + ' 个窗口');
    });
    

    3. 演示和源码

    代码已经开源,地址:gitee.com/mqycn/Frame…

    在线测试地址:www.miaoqiyuan.cn/products/fr…

    内嵌的iframe地址:www.miaoqiyuan.cn/products/fr…


    起源地下载网 » 使用FrameController.js优雅的处理单页多框架窗口(<iframe>)管理同步问题

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元