HTML5 postMessage 应用

postMessage 是 HTML5 新方法,它可以实现跨域窗口之间通讯。到目前为止,只有 IE8+, Firefox 3, Opera 9, Chrome 3和 Safari 4 支持,而本篇文章主要讲述 postMessage 方法与 message 事件跨浏览器实现。


postMessage 方法 JSONP 技术不一样,前者是前端擅长跨域文档数据即时通讯,后者擅长针对跨域服务端数据通讯,postMessage 应用场景能说明这个区别:

应用场景举例

  1. webOS 使用 iframe 嵌入第三方应用,此时 webOS 与应用需要实时接收/发送各自的消息与响应事件。
  2. 页面弹出一个由 iframe 层,嵌入第三方提供的图片上传页面,文件上传完毕后需要获取返回图片地址插入到编辑器。
  3. iframe 跨域高度自适应。

HTML5 postMessage 方法

postMessage 可以实现跨域文档的消息传输(Cross Document Messaging)。

向外界窗口发送消息:

1 otherWindow.postMessage(message, targetOrigin);

otherWindow: 指目标窗口,是 window.frames 属性的成员或者由 window.open 方法创建的窗口

参数说明:

  • message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
  • targetOrigin: 是限定消息接收范围,不限制请使用 ‘*’

HTML5 message 事件

绑定消息事件:

 1 window.addEventListener('message', receiver, false);
 2 function receiver (event) {
 3   if (event.origin === 'http://example.com') {
 4     if (event.data === 'Hello world') {
 5       event.source.postMessage('Hello', event.origin);
 6     } else {
 7       alert(event.data);
 8     };
 9   };
10 };

回调函数第一个参数接收 Event 对象,有三个常用属性:

  • data: 消息
  • origin: 消息来源地址
  • source: 源 DOMWindow 对象

message 事件在低版本浏览器下模拟实现

对于支持 postMessage 方法的浏览器直接使用它;而对于 IE6、7 采用了比较成熟的 window.name 保存数据以及跨域 iframe 静态代理动态传输方案,下面简称 Cross Frame。

Cross Frame

假设在域 www.a.com 上有页面 a.html 和代理页面 proxy-a.html , 另一个域 www.b.com 上有个页面 b.html 和代理页面 proxy-b.html,a.html 需要向 b.html 中发送消息时,页面会创建一个隐藏的 iframe 指向 proxy-b.html ,并把消息赋予 iframe.name 属性,此时 proxy-b.html 可以通过 window.name 获取到消息,由于 proxy-b.html 与 b.html 是同域,proxy-b.html 可以把消息赋予 b.html。 b.html 要给 a.html 发送消息时,原理一样。

自动捕获代理 URL

在 Cross Frame 方案中,通信双方必须确切的知道静态代理文件的 URL,显然这个极大的限制了应用范围,我们可以通过一些约定改善:静态代理文件必须置于通信页面所在域根目录,且文件名必须保持一致,如 messageEvent-proxy.html。

有了上述约定,接下来可以用一些巧妙的方法让双方自动捕获代理 URL。以 http://www.a.com/a.html 通过 iframe 嵌入 http://www.b.com/b.html 保持数据交换为例进行说明:

b.html 的静态代理路径可以通过正则分析 iframe.src 后得知;而从框架 b.html 内获取父页面就比较麻烦了,因为跨域后的 parent.location.href 属性只可写入不可读取,不过还可以借用 document.referrer 属性来分析来路地址得知父页面 url。document.referrer 是一个不稳定的属性,我们可以利用 iframe 中 window.name 刷新也不会变化的特性,用此来保存父页面 a.html 的地址。

持续跟踪 URL

a.html 第一次通过提取 iframe.src 路径可得知 b.html 的地址,假若 b.html 跳转到其他域名的时候,此时就会失去对 iframe 内静态代理的联络。 好在新页面由于能够获取父页面 a.html 保存在 window.name 的静态代理,所以我们可以在新页面初始化的时候向 a.html 传递消息告诉它新的地址,这样就能持续跟踪 iframe 中的 URL。

开源事件库 messageEvent.js


“messageEvent.js”是针对上述方案封装的 message 事件与 postMessage 方法库,它让各个浏览器之间 message 的 Event 对象成员属性统一,event.data 属性能传递多达 2MB 的文本信息,并且能让 IE6-9 浏览器像其他现代浏览一样支持 Object 类型数据进行传递 (内部使用深拷贝方式)。 若应用双方页面都采用 messageEvent.js,即可轻松实现跨域通信。
/*!
 * messageEvent
 * Date: 2012-01-03 23:38
 * http://code.google.com/p/message-event/
 * (c) 2011 - 2012 TangBin, http://www.planeArt.cn
 *
 * This is licensed under the GNU LGPL, version 2.1 or later.
 * For details, see: http://creativecommons.org/licenses/LGPL/2.1/
 */
var messageEvent = {

        version: '0.2.1',
        
        /**
         * 跨域向窗口中发送数据
         * @param       {DOMWindow}     目标窗口
         * @param       {String}        消息
         * @param       {String}        (可选) 限定URL接受消息, 默认 '*'
         */
        postMessage: function (win, message, targetOrigin, proxy) {
                var that = this;
                
                targetOrigin = targetOrigin || '*';
                
                // 修复 IE8、IE9 原生 postMessage 方法以及
                // IE6、7 window.name 方式跨域不能传递 Object 类型消息的问题,
                // 这里统一把 Object 转化成 String 用来传递
                message = this._toString(message);
                
                if (message === null) {
                        throw new Error('Error message type!');
                };
                
                // IE8+ 浏览器与其他现代浏览器均原生支持 postMessage 方法
                if (win.postMessage) {
                        // 对 messageEvent.js 触发的 message 事件进行特定的标记,
                        // 让监听程序不会因为第三方程序干扰
                        message = '_MESSAGEEVENT_' + message;
                        
                        win.postMessage(message, targetOrigin);
                        return;
                };
                
                // 找出目标窗口在 window.frames 里的成员名
                var targetName = this._getFramesName(win);

                
                // iframe 必须有 name 属性
                // 按照 W3C 对 name 命名规范:
                // 必须是字母 [A-Za-z] 开头,可以使用 [0-9] 数字、 "-" 、 ":" 或者 "." 的组合
                // 这里只进行简单验证,确保与 messageEvent-proxy.html 的 XSS 安全验证保持一致
                if (!targetName || /\(|\)/.test(targetName)) {
                        throw new Error('Please set the name to the iframe!');
                };
                
                var event = {
                        timeStamp: + new Date,
                        windowName: window.name, /** @inner */
                        targetOrigin: targetOrigin /** @inner */
                };
                
                try {
                        // 同域无需代理
                        var ONMESSAGE = window.frames[targetName]['_MESSAGEEVENT_'];
                        if (ONMESSAGE) {
                                event.data = message;
                                event.source = window;
                                event.origin = location.href.match(/[A-Za-z]+:\/{0,3}([^\/]+)/)[0];
                                ONMESSAGE(event);
                                return;
                        };
                } catch (e) {
                };
                
                // 获取远程跨域代理页面路径
                proxy = proxy || this._getProxy(targetName);

                if (!proxy) {
                        throw new Error('The wrong proxy address!');
                };
                
                // 创建跨域 iframe 代理
                var iframe = document.createElement('iframe');
                iframe.name = message;
                iframe.style.display = 'none';
                
                // iframe 使用完毕及时清理,释放内存占用
                var iframeLoad = function () {
                        that._removeEvent(iframe, 'load', iframeLoad);
                        iframe.src = 'about:blank';
                        iframe.parentNode.removeChild(iframe);
                };
                this._addEvent(iframe, 'load', iframeLoad);
                
                var hash = [
                        '#version=' + this.version, /** @inner */
                        '&targetName=' + targetName /** @inner */
                ];
                        
                for (var i in event) {
                        if (event.hasOwnProperty(i)) {
                                hash.push('&', i, '=', event[i]);
                        };
                };
                
                iframe.src = proxy + hash.join('');
                document.getElementsByTagName('head')[0].appendChild(iframe);
                
                // 以下代码必须在 iframe appendChild 之后
                // 让IE6,7 动态创建的 iframe 内部可读取 window.name 属性
                iframe.contentWindow.name = iframe.name;
        },
        
        
        /**
         * 添加消息事件
         * @param       {Function}      回调函数
         */
        add: function (callback) {
                this._listeners.push(callback);
        },
        
        
        /**
         * 卸载消息事件
         * @param       {Function}      (可选)待卸载的函数,为空则卸载全部
         */
        remove: function (callback) {
                var listeners = this._listeners;
                
                if (callback) {
                        for (i = 0; i < listeners.length; i ++) {
                                if (callback === listeners[i]) {
                                        listeners.splice(i--, 1);
                                };
                        };
                } else {
                        this._listeners = [];
                };
        },
        
        
        /**
         * 投递事件
         * @param       {Object}        Event
         * @inner
         */
        dispatch: function (event) {
                var listeners = this._listeners, callback;
                for (var i = 0; callback = listeners[i++]; ) {
                        callback.call(window, event);
                };
        },
        
        
        // 浏览器元素添加事件方法
        _addEvent: function (elem, type, callback) {
                var el = 'addEventListener',
                        dom = elem[el],
                        add = dom ? el : 'attachEvent',
                        type = dom ? type : 'on' + type;
                
                elem[add](type, callback, false);
        },
        
        
        // 浏览器原生卸载事件方法
        _removeEvent: function (elem, type, callback) {
                var el = 'removeEventListener',
                        dom = elem[el],
                        remove = dom ? el : 'detachEvent',
                        type = dom ? type : 'on' + type;
                
                elem[remove](type, callback, false);
        },
        
        
        // 获取 window 在 frames 里的成员名
        _getFramesName: function (win) {
                var name, frames = window.frames;
                
                // 因为目标 window 一般会跨域,所以需要 try
                try {
                        for (var i in frames) {
                                if (frames[i] == win) {
                                        name = i;
                                };
                        };
                } catch (e) {};
                
                return name;
        },
        
        
        // 获取 contentWindow 所属的 iframe 元素
        _getIframe: function (contentWindow) {
                var iframes = document.getElementsByTagName('iframe'),
                        ileng = iframes.length;
                for (var i = 0; i < ileng; i ++) {
                        if (iframes[i].contentWindow == contentWindow) {
                                return iframes[i];
                        };
                };
        },
        
        
        // 获取 messageEvent-proxy.html 路径
        _getProxy: function (framesName) {
                var proxy, file = '/messageEvent-proxy.html';
                
                if (this._cache[framesName]) {
                        proxy = this._cache[framesName];
                } else {
                        var iframe = this._getIframe(window.frames[framesName]);
                        if (iframe) {
                                var a = document.createElement('a');
                                a.href = iframe.src;
                                proxy = a.href.match(/[A-Za-z]+:\/{0,3}([^\/]+)/)[0];
                                this._cache[framesName] = proxy;
                        };
                };
                
                return proxy && proxy + file;
        },
        
        
        // 把对象转换成 JSON 字符串
        _toString: function (object) {
                if (typeof object !== 'string') {
                        return window.JSON && JSON.stringify
                        ? '_ISOBJECT_' + JSON.stringify(object)
                        : null;
                };
                return object;
        },
        
        
        // 尝试恢复 Object 类型消息
        _toObject: function (string) {
                var source = string.split('_ISOBJECT_')[1];
                
                if (source) {
                        try {
                                string = JSON.parse(source);
                        } catch (e) {};
                };
                
                return string;
        },
        
        
        // 消息事件的所有回调函数
        _listeners: [],
        
        
        // 用来缓存各个窗口的主机地址
        _cache: {
                parent: function (name, namespaces) {

                        if (name.indexOf(namespaces) === 0) {
                                return name.split(namespaces)[1];
                        };
                        
                        var rurl = /[A-Za-z]+:\/{0,3}([^\/]+)/,
                                referrer = document.referrer,
                                home = referrer && referrer.match(rurl)[0];
                        
                        if (home) {
                                window.name = namespaces + home;
                                return home;
                        };
                        
                }(window.name, '_PARENTHOST_')
        }
        
};

// 监听消息
if (window.postMessage) {

        messageEvent._addEvent(window, 'message', function (event) {
                var data = event.data,
                        src = event;
                        
                if (typeof data !== 'string') {
                        return;
                };
                
                data = data.split('_MESSAGEEVENT_')[1];
                        
                if (!data) {
                        return;
                };
                
                event = {};
                
                for (var i in src) {
                        event[i] = src[i];
                };
                
                event.data = messageEvent._toObject(data);
                messageEvent.dispatch(event);
        });
        
} else {

        // IE6、7
        window['_MESSAGEEVENT_'] = function (event) {
                var origin = event.origin,
                        windowName = event.windowName,
                        targetOrigin = event.targetOrigin;
                
                
                if (windowName) {
                        messageEvent._cache[windowName] = origin;
                        
                        if (windowName === 'parent') {
                                window.name = '_PARENTHOST_' + origin;
                        };
                        
                        if (event.data === '_NULL_') {
                                return;
                        };
                };
                
                if (targetOrigin === '*' || targetOrigin.indexOf(document.URL) === 0) {
                        event.type = 'message';
                        event.target = window;
                        event.data = messageEvent._toObject(event.data);
                        messageEvent.dispatch(event);
                };
                
        };
        
        // 页面初始化的时候尝试向父窗口传递一个”空“消息,
        // 这个消息包含了当前页面主机信息,
        // 这样 iframe 内容页面哪怕跳转到其他域名,
        // 仍然能让父窗口保持对当前页面的联络
        if (window.parent != window) {
                messageEvent.postMessage(parent, '_NULL_');
        };
        
};


/*
    给 messageEvent 提供 Object 类型消息的支持
        http://www.JSON.org/json2.js
        @see http://www.JSON.org/js.html
*/
var JSON;JSON||(JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;creturn e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;ctypeof rep[c]=="string"&&(d=rep[c],e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));return e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;delse typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();



/*
        给 jQuery 库提供跨浏览器绑定 message 事件的能力
        jQuery(window).bind('message', function (event) {
                alert(event.data)
        })
        以及数据发送方法
        jQuery.postMessage('iframe', 'hello world', '*', proxy)
*/
(function ($, messageEvent) {

        if (!$ || !messageEvent) {
                return;
        };
        
        /**
         * 跨域向窗口中发送数据
         * @param       {DOMWindow}     窗口
         * @param       {String}        消息
         * @param       {String}        (可选) 限定URL接受消息, 默认 '*'
         */
        $.postMessage = function (win, message, targetOrigin, proxy) {
                messageEvent.postMessage.apply(messageEvent, arguments);
        };

        
        /**
         * 添加消息事件处理函数
         * @param       {Function}      函数
         * @return      {Object jQuery}
         */
        $.fn.message = function (callback) {
                return this.bind('message', callback);
        };
        
        
        /*
                让 jQuery.event 增加 bind 方法绑定 message 事件:
                jQuery(window).bind('message', function (event) {
                        alert(event.data)
                })
         */
        var poll;
        $.event.special.message = {
        
                setup: function () {
                        var that = this;
                        if (this != window) {
                                return;
                        };
                        
                        poll = function (e) {
                                var event = new $.Event('message', e);
                                event.data = e.data;

                                setEventData(that, e.data);
                                $.event.trigger(event, null, that);
                        };
                        messageEvent.add(poll);
                        
                },
                
                teardown: function () {
                        if (this != window) {
                                return;
                        };
                        messageEvent.remove(poll);
                }
                
        };
        
        
        /*
                由于 jQuery 把 event.data 用来保存 bind 方法传入的附加数据,
                而未考虑到会与 html5 的 message 事件的 data 属性冲突,
                jQuery 这个不合理的设计导致需要操作其底层缓存才能解决问题
        */
        var setEventData = function (elem, data) {
                var eventsCache = $.data(elem, 'events');
                var messageCache = eventsCache && eventsCache.message;
                if (messageCache) {
                        $.each(messageCache, function (name, val) {
                                val.data = data;
                        });
                };
        };
        
})(this.jQuery, this.messageEvent);

 

接口

add(callback) 添加 message 事件
remove(callback) 卸载 message 事件
postMessage(otherWindow, message, targetOrigin) 向外部窗口发送消息

通过 jQuery 使用它

jQuery 是一个应用比较广泛的 DOM 库,它的事件机制非常强大而精妙,可以实现自定义事件。若页面引用了 jQuery, messageEvent.js 会为为它提供支持,你可以用熟悉的jQuery api 风格编程,如:
jQuery(window).bind('message', function (event) {
      alert(event.data)
});

jQuery(window).message(function (event) {
      alert(event.data)
});

jQuery.postMessage(iframe.contentWindow, 'hello world', '*');

jQuery(window).unbind('message');

 

由于 jQuery 把包装后的 Event 对象用 data 属性来保存 bind 方法传入的额外数据,导致与 message 事件自身的 event.data 属性冲突——这是一个设计错误。为了让 message 事件能够正确获取 event.data,messageEvent.js 通过操作 jQuery 底层缓存强制覆盖了 bind 方法传入的附加数据 (只针对 message 类型)。当然,我仍然期待 jQuery 未来版本能够取消掉 bind 方法的鸡肋特性。

messageEvent-proxy.html






messageEvent.js proxy




 

 

 Comments

    1. 回复
      km3945 2012 年 01 月 05 日

      postMessage(otherWindow, message, targetOrigin)为何源码中是4个参数.还有一个proxy

       
      • 回复
        唐 斌 2012 年 01 月 05 日

        proxy 是可选参数,已经不公开了,因为程序可自动获取到这个值。 它可以自定义静态代理的路径从而摆脱 “messageEvent-proxy.html 必须置于应用所在域跟目录” 的双方约定。这个参数可在特殊条件下使用。

         
    2. 回复
      defms 2012 年 01 月 14 日

      chrome下测试,能成功传输数据,不过还是显示:
      而且错误数一直在增加,应该有个小bug~

      2632Unsafe JavaScript attempt to access frame with URL
      http://localhost/messageEvent/_test/iframe.html from frame with URL http://127.0.0.1/messageEvent/_test/. Domains, protocols and ports must match.

       
      • 回复
        唐 斌 2012 年 01 月 17 日

        可能是chrome调试器的内部BUG,调试器也是基于html 与 css 构建的界面,我展开 event 对象的时候就遇到跨域错误。

         
    3. 回复
      ken 2012 年 10 月 08 日

      接受端如果完全是php怎么办呢?还需要用js提交一次?

 

 

 

 

 

 

 

 

 

 

 

 

转载于:https://www.cnblogs.com/weijunqiang/articles/3127180.html

你可能感兴趣的:(HTML5 postMessage 应用)