0. 痛点
做后台时,不可避免的会遇到 内嵌 iframe 的情况。 最近的 一个项目 有客户反馈无法保存,提示 token 错误。 经过沟通发现 是因为打开了 多个内嵌页(iframe),会出现此问题(使用Thinkphp5自带方法 token(),每次调用都会生成新的Token)。 修复方法也很简单,直接在 新增内嵌页时,将新生成的token进行广播。
这种情况在 后台 开发中会经常遇到,把 代码 提取出来,做了一下简单的封装,做成开源项目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页面给所有其他页面发送通知
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接收通知
//监听系统事件
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 添加自定义事件
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…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!