Skip to content

Instantly share code, notes, and snippets.

@andy12530
Last active August 29, 2015 14:04
Show Gist options
  • Save andy12530/9dcf71ad59ce8ee48276 to your computer and use it in GitHub Desktop.
Save andy12530/9dcf71ad59ce8ee48276 to your computer and use it in GitHub Desktop.
跨浏览器,用于iframe之间通信的基础组件
G.add('js/util/event/event_emitter.js', [], function (require, exports, module) {
// Events
// -----------------
// Thanks to:
// - https://github.com/documentcloud/backbone/blob/master/backbone.js
// - https://github.com/joyent/node/blob/master/lib/events.js
// Regular expression used to split event strings
var eventSplitter = /\s+/
// A module that can be mixed in to *any object* in order to provide it
// with custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = new Events();
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
function Events() {
}
// Bind one or more space separated events, `events`, to a `callback`
// function. Passing `"all"` will bind the callback to all events fired.
Events.prototype.on = function(events, callback, context) {
var cache, event, list
if (!callback) return this
cache = this.__events || (this.__events = {})
events = events.split(eventSplitter)
while (event = events.shift()) {
list = cache[event] || (cache[event] = [])
list.push(callback, context)
}
return this
}
// Remove one or many callbacks. If `context` is null, removes all callbacks
// with that function. If `callback` is null, removes all callbacks for the
// event. If `events` is null, removes all bound callbacks for all events.
Events.prototype.off = function(events, callback, context) {
var cache, event, list, i
// No events, or removing *all* events.
if (!(cache = this.__events)) return this
if (!(events || callback || context)) {
delete this.__events
return this
}
events = events ? events.split(eventSplitter) : Object.keys(cache)
// Loop through the callback list, splicing where appropriate.
while (event = events.shift()) {
list = cache[event]
if (!list) continue
if (!(callback || context)) {
delete cache[event]
continue
}
for (i = list.length - 2; i >= 0; i -= 2) {
if (!(callback && list[i] !== callback ||
context && list[i + 1] !== context)) {
list.splice(i, 2)
}
}
}
return this
}
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.prototype.trigger = function(events) {
var cache, event, all, list, i, len, rest = [], args
if (!(cache = this.__events)) return this
events = events.split(eventSplitter)
// Fill up `rest` with the callback arguments. Since we're only copying
// the tail of `arguments`, a loop is much faster than Array#slice.
for (i = 1, len = arguments.length; i < len; i++) {
rest[i - 1] = arguments[i]
}
// For each event, walk through the list of callbacks twice, first to
// trigger the event, then to trigger any `"all"` callbacks.
while (event = events.shift()) {
// Copy callback lists to prevent modification.
if (all = cache.all) all = all.slice()
if (list = cache[event]) list = list.slice()
// Execute event callbacks.
if (list) {
for (i = 0, len = list.length; i < len; i += 2) {
list[i].apply(list[i + 1] || this, rest)
}
}
// Execute "all" callbacks.
if (all) {
args = [event].concat(rest)
for (i = 0, len = all.length; i < len; i += 2) {
all[i].apply(all[i + 1] || this, args)
}
}
}
return this
}
Events.prototype.emit = Events.prototype.trigger;
// Mix `Events` to object instance or Class function.
Events.mixTo = function(receiver) {
var proto = Events.prototype
for (var p in proto) {
if (proto.hasOwnProperty(p)) {
receiver[p] = proto[p]
}
}
};
module.exports = Events;
});
G.add('js/util/iframe/rpc3.js', ['js/util/event/event_emitter.js', 'jquery'], function(require, exports, module) {
"use strict";
var $ = require('jquery');
var channelCounter = 0;
var Events = require('js/util/event/event_emitter.js');
var JSON = window.JSON ? window.JSON : {
stringify: GJ.jsonEncode,
parse: GJ.jsonDecode
};
var emptyFn = function() {};
var isFunction = function(obj) {
return typeof obj === 'function';
};
var RPC = function(config) {
var self = this;
config = config || {};
config.isHost = config.remote ? true : false;
if(config.isHost && window.name) {
config.channel = 'RPC_CHANNEL_P' + (channelCounter++);
} else {
config.channel = 'RPC_CHANNEL_' + (channelCounter++);
}
if (window.postMessage || document.postMessage) {
config.protocol = "1";
} else {
config.protocol = "2";
}
var callbacks = {};
var methods = config.method || {};
var messageID = 1;
Events.mixTo(RPC.Transport.prototype);
var transport = new RPC.Transport(config);
var send = function(message) {
transport.send(message);
};
transport.on('ready', function() {
if (isFunction(config.onReady)) {
setTimeout(function () {
config.onReady.call(self);
}, 0);
}
});
transport.on('message', function(message) {
if (message.method) { // exec method
execMethod(message);
} else if (message.callbackId) { // exec callback
var callback = callbacks[message.callbackId];
if (callback) {
callback(message.result);
}
}
});
var Fn = function(method, params, callback) {
var message = {
jsonrpc: "2.0",
params: params,
method: method,
callbackId: messageID
};
if (isFunction(params)) {
callback = params;
}
if (isFunction(callback)) {
callbacks[messageID] = callback;
}
messageID++;
setTimeout(function() {
send(message);
}, 0);
};
Fn.set = function(fnName, method) {
methods[fnName] = method;
};
Fn.destroy = function() {
messageID = 0;
methods = {};
callbacks = {};
transport.destroy();
};
function execMethod(message) {
var result;
var fn = methods[message.method];
if (isFunction(fn)) {
try {
if (Object.prototype.toString.call(message.params) !== '[object Array]') {
message.params = [message.params];
}
result = fn.apply({}, message.params);
} catch (ex) {
throw new Error("Exec function error: " + ex.message);
}
if (message.callbackId) {
send({
callbackId: message.callbackId,
result: result
});
}
}
}
transport.init();
Fn.iframe = config.iframe;
return Fn;
};
RPC.behavior = {};
if (module) {
module.exports = RPC;
}
(function(Behavior) {
Behavior.FLAG = '__RPC__';
Behavior.handleMessage = function(message) {
return Behavior.FLAG + JSON.stringify(message);
};
Behavior.verify = function(message) {
var result = {
message: undefined,
trust: false
};
if (message.indexOf(Behavior.FLAG) === 0) {
result.message = message.replace(Behavior.FLAG, '');
result.trust = true;
}
return result;
};
Behavior.navigator = function(config, transport) {
var pub = {
incoming: function(message) {
var verifyInfo = Behavior.verify(message);
if (true === verifyInfo.trust) {
message = verifyInfo.message;
} else {
return;
}
if (message === '"ready"') {
transport.emit('ready');
if (config.isHost) {
pub.outgoing('ready');
}
} else {
transport.emit('message', JSON.parse(message));
}
},
outgoing: function(message) {
message = Behavior.handleMessage(message);
if (config.isHost) {
window.navigator[config.channel + '_remote'](message);
} else {
window.navigator[config.channel + '_host'](message);
}
},
init: function() {
if (config.isHost) {
window.navigator[config.channel + '_host'] = pub.incoming;
} else {
window.navigator[config.channel + '_remote'] = pub.incoming;
pub.outgoing('ready');
}
}
};
return pub;
};
Behavior.postMessage = function(config, transport) {
var postFnHost;
var pub = {
incoming: function(message) {
if(message.channel === config.channel) {
transport.emit('message', message);
}
},
outgoing: function(message) {
if (message === 'ready') {
message = {
channel: config.channel,
isReady: true
};
} else {
message.channel = config.channel;
}
message = Behavior.handleMessage(message);
postFnHost.postMessage(message, "*");
},
init: function() {
GJ.addEvent(window, 'message', function(event) {
var message = event.data;
var verifyInfo = Behavior.verify(message);
if (true === verifyInfo.trust) {
message = verifyInfo.message;
} else {
return;
}
message = JSON.parse(message);
if (message.channel === config.channel) {
if (message.isReady) {
if (config.isHost) {
pub.outgoing('ready');
}
transport.emit('ready');
} else {
pub.incoming(message);
}
}
});
if (config.isHost) {
var i = config.iframe;
postFnHost = ("postMessage" in i.contentWindow) ? i.contentWindow : i.contentWindow.document;
} else {
postFnHost = ("postMessage" in window.parent) ? window.parent : window.parent.document;
pub.outgoing('ready');
}
}
};
return pub;
};
})(RPC.behavior);
(function(RPC) {
var extend = function(obj, ext, overwrite) {
for (var prop in ext) {
if (prop in obj) {
var member = ext[prop];
if (typeof member === 'object') {
extend(obj[prop], member, overwrite);
} else if (overwrite) {
obj[prop] = ext[prop];
}
} else {
obj[prop] = ext[prop];
}
}
return obj;
};
var createFrame = function(config) {
var iframe = null;
try {
iframe = document.createElement('<IFRAME name="'+ config.channel +'">');
} catch (e) {
}
if (!iframe || iframe.nodeName !== "IFRAME") {
iframe = document.createElement("IFRAME");
iframe.name = config.channel;
}
config.props = config.props || {};
if (typeof config.container === "string") {
config.container = document.getElementById(config.container);
}
extend(iframe.style, config.props.style, true);
if (!config.container) {
// This needs to be hidden like this, simply setting display:none and the like will cause failures in some browsers.
extend(iframe.style, {
position: "absolute",
top: "-2000px",
// Avoid potential horizontal scrollbar
left: "0px"
}, true);
}
// HACK: IE cannot have the src attribute set when the frame is appended
// into the container, so we set it to "javascript:false" as a
// placeholder for now. If we left the src undefined, it would
// instead default to "about:blank", which causes SSL mixed-content
// warnings in IE6 when on an SSL parent page.
config.props.src = 'javascript:false';
// transfer properties to the frame
extend(iframe, config.props, true);
iframe.border = iframe.frameBorder = 0;
iframe.allowTransparency = true;
if (config.container) {
config.container.appendChild(iframe);
} else {
config.container = document.body;
$('body').prepend(iframe);
}
// set the frame URL to the proper value (we previously set it to
// "javascript:false" to work around the IE issue mentioned above)
// if (config.onLoad) {
// $(iframe).on('load', config.onLoad);
// }
iframe.src = config.remote;
return iframe;
};
RPC.Transport = function(config) {
var self = this, stack = [];
var messages = [];
Events.mixTo(this);
if(!config.isHost) {
config.channel = window.name;
}
switch (config.protocol) {
case "1":
stack = new RPC.behavior.postMessage(config, self);
break;
case "2":
stack = new RPC.behavior.navigator(config, self);
break;
}
config.onLoad = function() {
stack.init();
};
this.on('ready', function () {
self.send = function(message) {
stack.outgoing(message);
};
for(var i = 0; i< messages.length; i++) {
self.send(messages[i]);
}
});
this.init = function () {
if (config.isHost) {
config.iframe = createFrame(config);
}
config.onLoad();
};
this.send = function(message) {
messages.push(message);
};
this.destroy = function () {
config.iframe.parentNode.removeChild(config.iframe);
};
};
})(RPC);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment