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

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

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

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

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);
});

对于 新增窗口、关闭窗口和刷新页面,会通过 进行广播

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);
});

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);
});

在线测试地址:http://www.miaoqiyuan.cn/products/frame-controller/,核心代码如下:

/**
 * 源码名称:FrameController.js
 * 实现功能:优雅的处理单页多框架(<iframe>)窗口管理同步问题
 * 作者主页:http://www.miaoqiyuan.cn/
 * 联系邮箱:mqycn@126.com
 * 使用说明:http://www.miaoqiyuan.cn/p/framecontroller-js
 * 最新版本:https://gitee.com/mqycn/FrameController.js
 */
(function() {

    if (window.top == window.self) {

        //父窗口
        window.FrameController = (function() {

            var topFrameId = 'parent', //父窗口ID
                data = {
                    events: {}, //事件
                    counter: {}, //事件计数器
                    _count: 1 //窗口ID
                };

            window._data = data;

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

            /**
             * 事件广播
             */
            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;

            };

            /**
             * 添加监听事件
             */
            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 removeAllListener = function() {
                delete data.events[this.frameId];
                delete data.counter[this.frameId];
            };

            return {
                frameId: topFrameId,
                broadcast: broadcast,
                addListener: addListener,
                removeListener: removeListener,
                removeAllListener: removeAllListener,
                getId: getId
            }
        })();
    } else {

        //子窗口
        window.FrameController = (function() {
            var TopController = window.top.FrameController,
                frameData = {
                    frameId: TopController.getId()
                };

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

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

            //获取窗口数量
            var getCount = function() {
                return FrameController.broadcast('frame._online') + 1;
            };

            window[listenerName](listenerPrefix + 'load', function() {

                //窗口注册事件
                FrameController.broadcast('frame.add', {
                    msg: '新增窗口'
                });

                //计数事件,仅用于统计框架数
                FrameController.addListener('frame._online', function() {});
            });

            window[listenerName]('unload', function() {

                //窗口关闭事件
                FrameController.broadcast('frame.remove', {
                    msg: '关闭窗口'
                });
                FrameController.removeAllListener();

            });

            //兼容IE8和之前的浏览器
            var bindFrameData = function(func) {
                return function(event, data) {
                    func.call(frameData, event, data);
                };
            }

            return {
                broadcast: broadcast,
                addListener: bindFrameData(TopController.addListener),
                removeListener: bindFrameData(TopController.removeListener),
                removeAllListener: bindFrameData(TopController.removeAllListener),
                count: getCount
            }
        })();
    }

})();

在 父窗口中 直接调用 FrameController.js 即可。对于子窗口(iframe),调用 FrameController.addListener 可以创建 监听事件,FrameController.broadcast 可以对 所有子窗口 进行 广播。比如:

//同步通知
FrameController.addListener('broadcast', function(e) {
    $('#msg').val(e.data.msg);
    console.log(e.frameId, e.event, e.data);
});

//发送广播
$('#send').click(function() {
    var nums = FrameController.broadcast('broadcast', {
        msg: $('#msg').val()
    });
    console.log('通知成功:', nums);
});

通知消息结构:

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.