├── README.md ├── bower.json ├── changelog.md ├── demo ├── index.html ├── style.css └── test.js ├── plugin └── web-socket-js │ ├── WebSocketMain.swf │ ├── swfobject.js │ └── web_socket.js └── src └── AV.push.js /README.md: -------------------------------------------------------------------------------- 1 | # LeanCloud 推送服务 JavaScript SDK 2 | 3 | ## 详细使用方法请看 [官方文档](https://leancloud.cn/docs/js_push.html) 4 | 5 | ## 官方新版本发布流程 6 | 7 | * 修改代码中修改日期及版本号 8 | * 修改 bower.json 中的版本号 9 | * 填写 changelog.md 10 | * Github 生成 release 包 11 | * 发布到 bower 12 | 13 | ``` 14 | bower register leancloud-push git@github.com:leancloud/js-push-sdk.git 15 | ``` 16 | * 修改 changelog.md 17 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leancloud-push.js", 3 | "version": "2.0.4", 4 | "homepage": "http://github.com/leancloud/js-push-sdk/", 5 | "authors": [ 6 | "WangXiao " 7 | ], 8 | "description": "LeanCloud JavaScript Push SDK", 9 | "main": "src/AV.push.js", 10 | "ignore": [ 11 | "demo", 12 | "README.md" 13 | ], 14 | "moduleType": [ 15 | "amd", 16 | "globals" 17 | ], 18 | "keywords": [ 19 | "LeanCloud", 20 | "Push" 21 | ], 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 2.0.4 2 | * 兼容 IE8、IE9,提供插件方式 3 | 4 | # 2.0.3 5 | * 修正 where 参数无法发送的 bug 6 | * 支持美国节点 7 | * 暴露 installationId 8 | 9 | # 2.0.2 10 | * 修正 event center 中的 bug 11 | * 修正 ajax 对于 2xx 的返回码都应该认为正确 12 | * 开放 off 方法,可以取消对事件的绑定 13 | * 增加 receive 方法,可以快捷的接受消息 14 | 15 | # 2.0.1 16 | * 修改接口 channel 改为 subscribe 17 | * 支持 bower 发布 18 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LeanCloud Realtime JavaScript SDK 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

请将测试代码中的 appId 换成自己的 appId 即可。

14 |
15 |

下面为显示结果,了解详情请查看 demo 源码中的注释:

16 |
17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | padding: 0px; 4 | } 5 | 6 | body { 7 | padding-top: 20px; 8 | text-align: center; 9 | font-size: 13px; 10 | } 11 | 12 | h1 { 13 | font-size: 22px; 14 | } 15 | 16 | #result { 17 | text-align: left; 18 | margin-top: 30px; 19 | padding-left: 20%; 20 | } 21 | 22 | #result p { 23 | font-size: 15px; 24 | margin: 2px; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /demo/test.js: -------------------------------------------------------------------------------- 1 | // 请换成自己的 appId 和 appKey 2 | var appId = 'a5CDnmOX94uSth8foK9mjHfq-gzGzoHsz'; 3 | var appKey = 'Ue3h6la9zH0IxkUJmyhLjk9h'; 4 | var push; 5 | 6 | // 每次调用生成一个聊天实例 7 | createNew(); 8 | 9 | function createNew() { 10 | push = AV.push({ 11 | appId: appId, 12 | appKey: appKey 13 | }); 14 | 15 | // 可以链式调用 16 | push.open(function() { 17 | showLog('可以接收推送'); 18 | }); 19 | 20 | // 监听推送消息 21 | push.on('message', function(data) { 22 | showLog('message'); 23 | showLog(JSON.stringify(data)); 24 | }); 25 | 26 | // receive 方法是监听 message 的快捷方法 27 | push.receive(function(data) { 28 | showLog('Receive 方法显示和监听 message 事件一致'); 29 | showLog(JSON.stringify(data)); 30 | }); 31 | 32 | // 监听网络异常 33 | push.on('reuse', function() { 34 | showLog('网络中断正在重试'); 35 | }); 36 | 37 | // 发送一条推送 38 | push.send({ 39 | // channels: ['aaa'], 40 | data: {LeanCloud: 123} 41 | }, function(result) { 42 | if (result) { 43 | showLog('推送成功发送'); 44 | } else { 45 | showLog('error'); 46 | } 47 | }); 48 | 49 | push.subscribe(['test123'], function(data) { 50 | showLog('关注新的频道'); 51 | }); 52 | 53 | push.send({ 54 | channels: ['test123'], 55 | data: {test123: 123} 56 | }); 57 | 58 | setTimeout(function() { 59 | 60 | // 如果不加 channels,可以简单的使用 send 方法发送一个 json 61 | push.send({ 62 | abc: 123 63 | }); 64 | 65 | push.unsubscribe(['test123'], function(data) { 66 | showLog('取消关注新的频道'); 67 | 68 | push.send({ 69 | channels: ['test123'], 70 | data: {test123: 123} 71 | }); 72 | }); 73 | 74 | }, 5000); 75 | } 76 | 77 | function showLog(msg) { 78 | console.log(msg); 79 | var div = document.getElementById('result'); 80 | var p = document.createElement('p'); 81 | p.innerText = msg; 82 | div.appendChild(p); 83 | } 84 | -------------------------------------------------------------------------------- /plugin/web-socket-js/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leancloud/js-push-sdk/eb3332965f2951908676714120572b22eb42e333/plugin/web-socket-js/WebSocketMain.swf -------------------------------------------------------------------------------- /plugin/web-socket-js/swfobject.js: -------------------------------------------------------------------------------- 1 | /* SWFObject v2.2 2 | is released under the MIT License 3 | */ 4 | var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab 2 | // License: New BSD License 3 | // Reference: http://dev.w3.org/html5/websockets/ 4 | // Reference: http://tools.ietf.org/html/rfc6455 5 | 6 | (function() { 7 | 8 | if (window.WEB_SOCKET_FORCE_FLASH) { 9 | // Keeps going. 10 | } else if (window.WebSocket) { 11 | return; 12 | } else if (window.MozWebSocket) { 13 | // Firefox. 14 | window.WebSocket = MozWebSocket; 15 | return; 16 | } 17 | 18 | var logger; 19 | if (window.WEB_SOCKET_LOGGER) { 20 | logger = WEB_SOCKET_LOGGER; 21 | } else if (window.console && window.console.log && window.console.error) { 22 | // In some environment, console is defined but console.log or console.error is missing. 23 | logger = window.console; 24 | } else { 25 | logger = {log: function(){ }, error: function(){ }}; 26 | } 27 | 28 | // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. 29 | if (swfobject.getFlashPlayerVersion().major < 10) { 30 | logger.error("Flash Player >= 10.0.0 is required."); 31 | return; 32 | } 33 | if (location.protocol == "file:") { 34 | logger.error( 35 | "WARNING: web-socket-js doesn't work in file:///... URL " + 36 | "unless you set Flash Security Settings properly. " + 37 | "Open the page via Web server i.e. http://..."); 38 | } 39 | 40 | /** 41 | * Our own implementation of WebSocket class using Flash. 42 | * @param {string} url 43 | * @param {array or string} protocols 44 | * @param {string} proxyHost 45 | * @param {int} proxyPort 46 | * @param {string} headers 47 | */ 48 | window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { 49 | var self = this; 50 | self.__id = WebSocket.__nextId++; 51 | WebSocket.__instances[self.__id] = self; 52 | self.readyState = WebSocket.CONNECTING; 53 | self.bufferedAmount = 0; 54 | self.__events = {}; 55 | if (!protocols) { 56 | protocols = []; 57 | } else if (typeof protocols == "string") { 58 | protocols = [protocols]; 59 | } 60 | // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. 61 | // Otherwise, when onopen fires immediately, onopen is called before it is set. 62 | self.__createTask = setTimeout(function() { 63 | WebSocket.__addTask(function() { 64 | self.__createTask = null; 65 | WebSocket.__flash.create( 66 | self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); 67 | }); 68 | }, 0); 69 | }; 70 | 71 | /** 72 | * Send data to the web socket. 73 | * @param {string} data The data to send to the socket. 74 | * @return {boolean} True for success, false for failure. 75 | */ 76 | WebSocket.prototype.send = function(data) { 77 | if (this.readyState == WebSocket.CONNECTING) { 78 | throw "INVALID_STATE_ERR: Web Socket connection has not been established"; 79 | } 80 | // We use encodeURIComponent() here, because FABridge doesn't work if 81 | // the argument includes some characters. We don't use escape() here 82 | // because of this: 83 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions 84 | // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't 85 | // preserve all Unicode characters either e.g. "\uffff" in Firefox. 86 | // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require 87 | // additional testing. 88 | var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); 89 | if (result < 0) { // success 90 | return true; 91 | } else { 92 | this.bufferedAmount += result; 93 | return false; 94 | } 95 | }; 96 | 97 | /** 98 | * Close this web socket gracefully. 99 | */ 100 | WebSocket.prototype.close = function() { 101 | if (this.__createTask) { 102 | clearTimeout(this.__createTask); 103 | this.__createTask = null; 104 | this.readyState = WebSocket.CLOSED; 105 | return; 106 | } 107 | if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { 108 | return; 109 | } 110 | this.readyState = WebSocket.CLOSING; 111 | WebSocket.__flash.close(this.__id); 112 | }; 113 | 114 | /** 115 | * Implementation of {@link DOM 2 EventTarget Interface} 116 | * 117 | * @param {string} type 118 | * @param {function} listener 119 | * @param {boolean} useCapture 120 | * @return void 121 | */ 122 | WebSocket.prototype.addEventListener = function(type, listener, useCapture) { 123 | if (!(type in this.__events)) { 124 | this.__events[type] = []; 125 | } 126 | this.__events[type].push(listener); 127 | }; 128 | 129 | /** 130 | * Implementation of {@link DOM 2 EventTarget Interface} 131 | * 132 | * @param {string} type 133 | * @param {function} listener 134 | * @param {boolean} useCapture 135 | * @return void 136 | */ 137 | WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { 138 | if (!(type in this.__events)) return; 139 | var events = this.__events[type]; 140 | for (var i = events.length - 1; i >= 0; --i) { 141 | if (events[i] === listener) { 142 | events.splice(i, 1); 143 | break; 144 | } 145 | } 146 | }; 147 | 148 | /** 149 | * Implementation of {@link DOM 2 EventTarget Interface} 150 | * 151 | * @param {Event} event 152 | * @return void 153 | */ 154 | WebSocket.prototype.dispatchEvent = function(event) { 155 | var events = this.__events[event.type] || []; 156 | for (var i = 0; i < events.length; ++i) { 157 | events[i](event); 158 | } 159 | var handler = this["on" + event.type]; 160 | if (handler) handler.apply(this, [event]); 161 | }; 162 | 163 | /** 164 | * Handles an event from Flash. 165 | * @param {Object} flashEvent 166 | */ 167 | WebSocket.prototype.__handleEvent = function(flashEvent) { 168 | 169 | if ("readyState" in flashEvent) { 170 | this.readyState = flashEvent.readyState; 171 | } 172 | if ("protocol" in flashEvent) { 173 | this.protocol = flashEvent.protocol; 174 | } 175 | 176 | var jsEvent; 177 | if (flashEvent.type == "open" || flashEvent.type == "error") { 178 | jsEvent = this.__createSimpleEvent(flashEvent.type); 179 | } else if (flashEvent.type == "close") { 180 | jsEvent = this.__createSimpleEvent("close"); 181 | jsEvent.wasClean = flashEvent.wasClean ? true : false; 182 | jsEvent.code = flashEvent.code; 183 | jsEvent.reason = flashEvent.reason; 184 | } else if (flashEvent.type == "message") { 185 | var data = decodeURIComponent(flashEvent.message); 186 | jsEvent = this.__createMessageEvent("message", data); 187 | } else { 188 | throw "unknown event type: " + flashEvent.type; 189 | } 190 | 191 | this.dispatchEvent(jsEvent); 192 | 193 | }; 194 | 195 | WebSocket.prototype.__createSimpleEvent = function(type) { 196 | if (document.createEvent && window.Event) { 197 | var event = document.createEvent("Event"); 198 | event.initEvent(type, false, false); 199 | return event; 200 | } else { 201 | return {type: type, bubbles: false, cancelable: false}; 202 | } 203 | }; 204 | 205 | WebSocket.prototype.__createMessageEvent = function(type, data) { 206 | if (window.MessageEvent && typeof(MessageEvent) == "function" && !window.opera) { 207 | return new MessageEvent("message", { 208 | "view": window, 209 | "bubbles": false, 210 | "cancelable": false, 211 | "data": data 212 | }); 213 | } else if (document.createEvent && window.MessageEvent && !window.opera) { 214 | var event = document.createEvent("MessageEvent"); 215 | event.initMessageEvent("message", false, false, data, null, null, window, null); 216 | return event; 217 | } else { 218 | // Old IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. 219 | return {type: type, data: data, bubbles: false, cancelable: false}; 220 | } 221 | }; 222 | 223 | /** 224 | * Define the WebSocket readyState enumeration. 225 | */ 226 | WebSocket.CONNECTING = 0; 227 | WebSocket.OPEN = 1; 228 | WebSocket.CLOSING = 2; 229 | WebSocket.CLOSED = 3; 230 | 231 | // Field to check implementation of WebSocket. 232 | WebSocket.__isFlashImplementation = true; 233 | WebSocket.__initialized = false; 234 | WebSocket.__flash = null; 235 | WebSocket.__instances = {}; 236 | WebSocket.__tasks = []; 237 | WebSocket.__nextId = 0; 238 | 239 | /** 240 | * Load a new flash security policy file. 241 | * @param {string} url 242 | */ 243 | WebSocket.loadFlashPolicyFile = function(url){ 244 | WebSocket.__addTask(function() { 245 | WebSocket.__flash.loadManualPolicyFile(url); 246 | }); 247 | }; 248 | 249 | /** 250 | * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. 251 | */ 252 | WebSocket.__initialize = function() { 253 | 254 | if (WebSocket.__initialized) return; 255 | WebSocket.__initialized = true; 256 | 257 | if (WebSocket.__swfLocation) { 258 | // For backword compatibility. 259 | window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; 260 | } 261 | if (!window.WEB_SOCKET_SWF_LOCATION) { 262 | logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); 263 | return; 264 | } 265 | if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && 266 | !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && 267 | WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { 268 | var swfHost = RegExp.$1; 269 | if (location.host != swfHost) { 270 | logger.error( 271 | "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + 272 | "('" + location.host + "' != '" + swfHost + "'). " + 273 | "See also 'How to host HTML file and SWF file in different domains' section " + 274 | "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + 275 | "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); 276 | } 277 | } 278 | var container = document.createElement("div"); 279 | container.id = "webSocketContainer"; 280 | // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents 281 | // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). 282 | // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash 283 | // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is 284 | // the best we can do as far as we know now. 285 | container.style.position = "absolute"; 286 | if (WebSocket.__isFlashLite()) { 287 | container.style.left = "0px"; 288 | container.style.top = "0px"; 289 | } else { 290 | container.style.left = "-100px"; 291 | container.style.top = "-100px"; 292 | } 293 | var holder = document.createElement("div"); 294 | holder.id = "webSocketFlash"; 295 | container.appendChild(holder); 296 | document.body.appendChild(container); 297 | // See this article for hasPriority: 298 | // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html 299 | swfobject.embedSWF( 300 | WEB_SOCKET_SWF_LOCATION, 301 | "webSocketFlash", 302 | "1" /* width */, 303 | "1" /* height */, 304 | "10.0.0" /* SWF version */, 305 | null, 306 | null, 307 | {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, 308 | null, 309 | function(e) { 310 | if (!e.success) { 311 | logger.error("[WebSocket] swfobject.embedSWF failed"); 312 | } 313 | } 314 | ); 315 | 316 | }; 317 | 318 | /** 319 | * Called by Flash to notify JS that it's fully loaded and ready 320 | * for communication. 321 | */ 322 | WebSocket.__onFlashInitialized = function() { 323 | // We need to set a timeout here to avoid round-trip calls 324 | // to flash during the initialization process. 325 | setTimeout(function() { 326 | WebSocket.__flash = document.getElementById("webSocketFlash"); 327 | WebSocket.__flash.setCallerUrl(location.href); 328 | WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); 329 | for (var i = 0; i < WebSocket.__tasks.length; ++i) { 330 | WebSocket.__tasks[i](); 331 | } 332 | WebSocket.__tasks = []; 333 | }, 0); 334 | }; 335 | 336 | /** 337 | * Called by Flash to notify WebSockets events are fired. 338 | */ 339 | WebSocket.__onFlashEvent = function() { 340 | setTimeout(function() { 341 | try { 342 | // Gets events using receiveEvents() instead of getting it from event object 343 | // of Flash event. This is to make sure to keep message order. 344 | // It seems sometimes Flash events don't arrive in the same order as they are sent. 345 | var events = WebSocket.__flash.receiveEvents(); 346 | for (var i = 0; i < events.length; ++i) { 347 | WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); 348 | } 349 | } catch (e) { 350 | logger.error(e); 351 | } 352 | }, 0); 353 | return true; 354 | }; 355 | 356 | // Called by Flash. 357 | WebSocket.__log = function(message) { 358 | logger.log(decodeURIComponent(message)); 359 | }; 360 | 361 | // Called by Flash. 362 | WebSocket.__error = function(message) { 363 | logger.error(decodeURIComponent(message)); 364 | }; 365 | 366 | WebSocket.__addTask = function(task) { 367 | if (WebSocket.__flash) { 368 | task(); 369 | } else { 370 | WebSocket.__tasks.push(task); 371 | } 372 | }; 373 | 374 | /** 375 | * Test if the browser is running flash lite. 376 | * @return {boolean} True if flash lite is running, false otherwise. 377 | */ 378 | WebSocket.__isFlashLite = function() { 379 | if (!window.navigator || !window.navigator.mimeTypes) { 380 | return false; 381 | } 382 | var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; 383 | if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { 384 | return false; 385 | } 386 | return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; 387 | }; 388 | 389 | if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { 390 | // NOTE: 391 | // This fires immediately if web_socket.js is dynamically loaded after 392 | // the document is loaded. 393 | swfobject.addDomLoadEvent(function() { 394 | WebSocket.__initialize(); 395 | }); 396 | } 397 | 398 | })(); 399 | -------------------------------------------------------------------------------- /src/AV.push.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author wangxiao 3 | * @date 2015-07-31 4 | * 5 | * 每位工程师都有保持代码优雅的义务 6 | * Each engineer has a duty to keep the code elegant 7 | */ 8 | 9 | void function(win) { 10 | 11 | // 当前版本 12 | var VERSION = '2.0.4'; 13 | 14 | // 获取命名空间 15 | var AV = win.AV || {}; 16 | win.AV = AV; 17 | 18 | // AMD 加载支持 19 | if (typeof define === 'function' && define.amd) { 20 | define('AV', [], function() { 21 | return AV; 22 | }); 23 | } 24 | 25 | // 配置项 26 | var config = { 27 | // 心跳时间 28 | heartbeatsTime: 30 * 1000 29 | }; 30 | 31 | // 命名空间,挂载一些工具方法 32 | var tool = {}; 33 | 34 | // 命名空间,挂载私有方法 35 | var engine = {}; 36 | 37 | // realtime 对象内,会被派发的全部事件名 38 | var eNameIndex = { 39 | // websocket 连接建立 40 | open: 'open', 41 | // websocket 连接关闭 42 | close: 'close', 43 | // 接受到新的推送 44 | message: 'message', 45 | // 断开重连 46 | reuse: 'reuse', 47 | // 各种错误 48 | error: 'error' 49 | }; 50 | 51 | // 创建一个新的 realtime 对象,挂载所有 realtime 中的方法 52 | var newPushObject = function() { 53 | 54 | // 缓存一些已经实例化的变量 55 | var cache = { 56 | // 基础配置,包括 appId,appKey 等 57 | options: undefined, 58 | // WebSocket 实例 59 | ws: undefined, 60 | // 心跳的计时器 61 | heartbeatsTimer: undefined, 62 | // 事件中心 63 | ec: undefined, 64 | // 是否是用户关闭,如果不是将会断开重连 65 | closeFlag: false, 66 | // reuse 事件的重试 timer 67 | reuseTimer: undefined 68 | }; 69 | 70 | // WebSocket Open 71 | var wsOpen = function() { 72 | tool.log('WebSocket opened.'); 73 | engine.loginPush(cache.options); 74 | // 启动心跳 75 | engine.heartbeats(); 76 | engine.guard(); 77 | cache.ec.emit(eNameIndex.open); 78 | }; 79 | 80 | // WebSocket Close 81 | var wsClose = function() { 82 | tool.log('WebSocket closed.'); 83 | // 派发全局 close 事件,表示 realtime 已经关闭 84 | cache.ec.emit(eNameIndex.close); 85 | }; 86 | 87 | // WebSocket Message 88 | var wsMessage = function(msg) { 89 | var data = JSON.parse(msg.data); 90 | // 派发推送事件 91 | if (data.cmd === 'data') { 92 | engine.ackPush(data.ids); 93 | var i; 94 | var l = data.msg.length; 95 | for (i = 0; i < l; i ++) { 96 | cache.ec.emit(eNameIndex.message, data.msg[i]); 97 | } 98 | } 99 | }; 100 | 101 | var wsError = function(data) { 102 | throw(data); 103 | // TODO: 增加更加详细的错误处理 104 | }; 105 | 106 | // WebSocket send message 107 | var wsSend = function(data) { 108 | cache.ws.send(JSON.stringify(data)); 109 | }; 110 | 111 | var _channel = function(argument, callback, isRemove) { 112 | var channels = []; 113 | if (typeof argument === 'string') { 114 | channels.push(argument); 115 | } 116 | else { 117 | channels = argument; 118 | } 119 | engine.channels(channels, callback, isRemove); 120 | }; 121 | 122 | engine.getId = function(options) { 123 | 124 | // 兼容 js sdk 与 push sdk 一起使用时,共用 installationId ,该存储地址与 JS SDK 中名字一致 125 | var itemName = 'AV/' + options.appId + '/installationId'; 126 | var installationId = tool.storage(itemName); 127 | 128 | if (installationId) { 129 | return installationId; 130 | } 131 | else { 132 | var id = tool.getId(); 133 | options.id = id; 134 | engine.sendId(options, function(data) { 135 | tool.storage(itemName, id); 136 | }); 137 | return id; 138 | } 139 | }; 140 | 141 | engine.sendId = function(options, callback) { 142 | tool.ajax({ 143 | url: 'https://' + cache.options.host + '/1.1/installations', 144 | method: 'post', 145 | appId: options.appId, 146 | appKey: options.appKey, 147 | data: { 148 | deviceType: options.deviceType, 149 | installationId: options.id, 150 | channels: options.channels 151 | } 152 | }, function(data) { 153 | if (data) { 154 | if (callback) { 155 | callback(data); 156 | cache.ec.emit('leancloud-send-id-ok'); 157 | } 158 | } 159 | else { 160 | setTimeout(function() { 161 | engine.sendId(options); 162 | }, 5000); 163 | } 164 | }); 165 | }; 166 | 167 | engine.sendPush = function(options, callback) { 168 | tool.ajax({ 169 | url: 'https://' + cache.options.host + '/1.1/push', 170 | method: 'post', 171 | appId: options.appId, 172 | appKey: options.appKey, 173 | data: options 174 | }, function(data, error) { 175 | if (data) { 176 | if (callback) { 177 | callback(data); 178 | } 179 | } 180 | else { 181 | if (error.code === 403 || error.code === 404) { 182 | throw(error.error); 183 | } else { 184 | setTimeout(function() { 185 | engine.sendPush(options, callback); 186 | }, 5000); 187 | } 188 | } 189 | }); 190 | }; 191 | 192 | engine.channels = function(channels, callback, isRemove) { 193 | var data = { 194 | installationId: cache.options.id, 195 | deviceType: cache.options.deviceType 196 | }; 197 | 198 | if (isRemove) { 199 | data.channels = { 200 | __op: 'Remove', 201 | objects: channels 202 | }; 203 | } 204 | else { 205 | data.channels = channels; 206 | } 207 | tool.ajax({ 208 | url: 'https://' + cache.options.host + '/1.1/installations', 209 | method: 'post', 210 | appId: cache.options.appId, 211 | appKey: cache.options.appKey, 212 | data: data 213 | }, function(data) { 214 | if (callback) { 215 | callback(data); 216 | } 217 | }); 218 | }; 219 | 220 | engine.createSocket = function(server) { 221 | var ws = new WebSocket(server); 222 | cache.ws = ws; 223 | 224 | // TODO: 此处需要考虑 WebSocket 重用 225 | // TODO: 需要考虑网络状况,是用户自己主动 close websocket 还是网络问题 226 | ws.addEventListener('open', wsOpen); 227 | ws.addEventListener('close', wsClose); 228 | ws.addEventListener('message', wsMessage); 229 | ws.addEventListener('error', wsError); 230 | }; 231 | 232 | // 心跳程序 233 | engine.heartbeats = function() { 234 | wsSend({}); 235 | cache.ws.addEventListener('message', function() { 236 | if (cache.heartbeatsTimer) { 237 | clearTimeout(cache.heartbeatsTimer); 238 | } 239 | cache.heartbeatsTimer = setTimeout(function() { 240 | wsSend({}); 241 | }, config.heartbeatsTime); 242 | }); 243 | }; 244 | 245 | // 守护进程,会派发 reuse 重连事件 246 | engine.guard = function() { 247 | cache.ec.on(eNameIndex.close, function() { 248 | if (!cache.closeFlag) { 249 | cache.ec.emit(eNameIndex.reuse); 250 | } 251 | }); 252 | }; 253 | 254 | engine.connect = function(options) { 255 | var server = options.server; 256 | if (server && tool.now() < server.expires) { 257 | engine.createSocket(server.server); 258 | } 259 | else { 260 | cache.ec.emit(eNameIndex.error); 261 | throw('WebSocket connet failed.'); 262 | } 263 | }; 264 | 265 | engine.ackPush = function(idList) { 266 | wsSend({ 267 | cmd: 'ack', 268 | appId: cache.options.appId, 269 | installationId: cache.options.id, 270 | ids: idList 271 | }); 272 | }; 273 | 274 | engine.loginPush = function(options) { 275 | wsSend({ 276 | cmd: 'login', 277 | appId: options.appId, 278 | installationId: options.id 279 | }); 280 | }; 281 | 282 | engine.getServer = function(options, callback) { 283 | var appId = options.appId; 284 | // 是否获取 wss 的安全链接 285 | var secure = options.secure; 286 | var url = ''; 287 | var protocol = 'http://'; 288 | if (win && win.location.protocol === 'https:') { 289 | protocol = 'https://'; 290 | } 291 | var node = ''; 292 | switch (options.region) { 293 | case 'cn': 294 | node = 'g0'; 295 | break; 296 | case 'us': 297 | node = 'a0'; 298 | break; 299 | default: 300 | throw('There is no this region.'); 301 | } 302 | url = protocol + 'router-' + node + '-push.leancloud.cn/v1/route?appId=' + appId ; 303 | if (secure) { 304 | url += '&secure=1'; 305 | } 306 | tool.ajax({ 307 | url: url 308 | }, function(data, error) { 309 | if (data) { 310 | data.expires = tool.now() + data.ttl * 1000; 311 | cache.server = data; 312 | callback(data); 313 | } 314 | else { 315 | if (error.code === 403 || error.code === 404) { 316 | throw(error.error); 317 | } else { 318 | cache.ec.emit(eNameIndex.error); 319 | } 320 | } 321 | }); 322 | }; 323 | 324 | return { 325 | installationId: '', 326 | cache: cache, 327 | open: function(callback) { 328 | var me = this; 329 | engine.getServer(cache.options, function(data) { 330 | if (data) { 331 | engine.connect({ 332 | server: cache.server 333 | }); 334 | } 335 | }); 336 | if (callback) { 337 | cache.ec.on(eNameIndex.open, callback); 338 | } 339 | // 断开重连 340 | cache.ec.once(eNameIndex.reuse + ' ' + eNameIndex.error, function() { 341 | if (cache.reuseTimer) { 342 | clearTimeout(cache.reuseTimer); 343 | } 344 | cache.reuseTimer = setTimeout(function() { 345 | me.open(); 346 | }, 5000); 347 | }); 348 | return this; 349 | }, 350 | // 表示关闭 WebSocket 连接,并且回收内存 351 | close: function() { 352 | cache.closeFlag = true; 353 | cache.ws.close(); 354 | return this; 355 | }, 356 | on: function(eventName, callback) { 357 | cache.ec.on(eventName, callback); 358 | return this; 359 | }, 360 | once: function(eventName, callback) { 361 | cache.ec.once(eventName, callback); 362 | return this; 363 | }, 364 | off: function(eventName, fun) { 365 | cache.ec.off(eventName, fun); 366 | return this; 367 | }, 368 | emit: function(eventName, data) { 369 | cache.ec.emit(eventName, data); 370 | return this; 371 | }, 372 | send: function(argument, callback) { 373 | var obj = { 374 | appId: cache.options.appId, 375 | appKey: cache.options.appKey 376 | }; 377 | // 如果没有这些保留字段,则传入的就是 data 数据 378 | if (!argument.channels && 379 | !argument.where && 380 | !argument.expiration_time && 381 | !argument.expiration_interval && 382 | !argument.push_time && 383 | !argument.prod) { 384 | 385 | obj.data = argument; 386 | engine.sendPush(obj, callback); 387 | } 388 | else { 389 | for (var k in argument) { 390 | obj[k] = argument[k]; 391 | } 392 | engine.sendPush(obj, callback); 393 | } 394 | return this; 395 | }, 396 | // 订阅频道 397 | subscribe: function(argument, callback) { 398 | _channel(argument, callback); 399 | return this; 400 | }, 401 | // 取消订阅 402 | unsubscribe: function(argument, callback) { 403 | _channel(argument, callback, true); 404 | return this; 405 | }, 406 | // 接受消息 407 | receive: function(callback) { 408 | if (!callback) { 409 | throw('Receive must hava callback.'); 410 | } 411 | cache.ec.on(eNameIndex.message, function(data) { 412 | callback(data); 413 | }); 414 | return this; 415 | } 416 | }; 417 | }; 418 | 419 | // 主函数,启动通信并获得 pushObject 420 | // 因为只有需要接收 Push 的时候才需要开启服务器连接,所以这个方法没有 callback 421 | AV.push = function(options) { 422 | if (typeof options !== 'object') { 423 | throw('AV.push need a argument at least.'); 424 | } 425 | else if (!options.appId) { 426 | throw('Options must have appId.'); 427 | } 428 | else if (!options.appKey) { 429 | throw('Options must have appKey.'); 430 | } 431 | else { 432 | 433 | // 通过判断插件库中的对象是否存在来检测是否需要关掉安全链接,在需要兼容 flash 的时候需要关掉,默认开启。 434 | var secure = win.WebSocket.loadFlashPolicyFile ? false : true; 435 | 436 | options = { 437 | // LeanCloud 中唯一的服务 id 438 | appId: options.appId, 439 | // LeanCloud 中检测客户端权限的 key 440 | appKey: options.appKey, 441 | // clientId 对应的就是 peerId,如果不传入服务器会自动生成,客户端没有持久化该数据。 442 | peerId: options.clientId, 443 | // 是否关闭 WebSocket 的安全链接,即由 wss 协议转为 ws 协议,关闭 SSL 保护。默认开启。 444 | secure: typeof(options.secure) === 'undefined' ? secure : options.secure, 445 | // 服务器地区选项,默认为中国大陆 446 | region: options.region || 'cn', 447 | // 推送的频道 448 | channels: options.channels || [], 449 | // 服务端用来记录和区分 SDK 的字段 450 | deviceType: 'web' 451 | }; 452 | 453 | switch(options.region) { 454 | case 'cn': 455 | options.host = 'leancloud.cn'; 456 | break; 457 | case 'us': 458 | options.host = 'us-api.leancloud.cn'; 459 | break; 460 | } 461 | 462 | var pushObject = newPushObject(); 463 | pushObject.cache.options = options; 464 | // 这个 id 是针对设备的抽象 465 | options.id = engine.getId(options); 466 | // 暴露 installationId 467 | pushObject.installationId = options.id; 468 | pushObject.cache.ec = tool.eventCenter(); 469 | return pushObject; 470 | } 471 | }; 472 | 473 | // 赋值版本号 474 | AV.push.version = VERSION; 475 | 476 | // 挂载私有方法 477 | AV.push._tool = tool; 478 | AV.push._engine = engine; 479 | 480 | // 空函数 481 | tool.noop = function() {}; 482 | 483 | // 获取一个唯一 id,碰撞概率:基本不可能 484 | tool.getId = function() { 485 | // 与时间相关的随机引子 486 | var getIdItem = function() { 487 | return Date.now().toString(36) + Math.random().toString(36).substring(2, 3); 488 | }; 489 | return 'AV-' + getIdItem() + '-' + getIdItem() + '-' + getIdItem(); 490 | }; 491 | 492 | // 输出 log 493 | tool.log = function(msg) { 494 | console.log(msg); 495 | }; 496 | 497 | tool.ajax = function(options, callback) { 498 | var url = options.url; 499 | var method = options.method || 'get'; 500 | var xhr = new XMLHttpRequest(); 501 | xhr.open(method, url); 502 | if (method === 'post' || method === 'put') { 503 | xhr.setRequestHeader('Content-Type', 'application/json'); 504 | } 505 | if (options.appId) { 506 | xhr.setRequestHeader('X-AVOSCloud-Application-Id', options.appId); 507 | } 508 | if (options.appKey) { 509 | xhr.setRequestHeader('X-AVOSCloud-Application-Key', options.appKey); 510 | } 511 | 512 | // IE9 中需要设置所有的 xhr 事件回调,不然可能会无法执行后续操作 513 | xhr.onprogress = function(){}; 514 | xhr.ontimeout = function(){}; 515 | xhr.timeout = 0; 516 | 517 | xhr.onload = function(data) { 518 | // 检测认为 2xx 的返回都是成功 519 | if (xhr.status >= 200 && xhr.status < 300) { 520 | callback(JSON.parse(xhr.responseText)); 521 | } else { 522 | callback(null, JSON.parse(xhr.responseText)); 523 | } 524 | }; 525 | xhr.onerror = function(data) { 526 | callback(null, data); 527 | throw('Network error.'); 528 | }; 529 | xhr.send(JSON.stringify(options.data)); 530 | }; 531 | 532 | // 获取当前时间的时间戳 533 | tool.now = function() { 534 | return Date.now(); 535 | }; 536 | 537 | // 储存 538 | tool.storage = function(name, value) { 539 | if (value) { 540 | if (typeof value === 'object') { 541 | value = JSON.stringify(value); 542 | } 543 | win.localStorage.setItem(name, value); 544 | } 545 | else { 546 | var result = win.localStorage.getItem(name); 547 | if (/^[\{|\[]/.test(result) && /[\}|\]]$/.test(result)) { 548 | result = JSON.parse(result); 549 | } 550 | return result; 551 | } 552 | }; 553 | 554 | // 小型的私有事件中心 555 | tool.eventCenter = function() { 556 | var eventList = {}; 557 | var eventOnceList = {}; 558 | 559 | var _on = function(eventName, fun, isOnce) { 560 | if (!eventName) { 561 | throw('No event name.'); 562 | } 563 | else if (!fun) { 564 | throw('No callback function.'); 565 | } 566 | var list = eventName.split(/\s+/); 567 | var tempList; 568 | if (!isOnce) { 569 | tempList = eventList; 570 | } 571 | else { 572 | tempList = eventOnceList; 573 | } 574 | for (var i = 0, l = list.length; i < l; i ++) { 575 | if (list[i]) { 576 | if (!tempList[list[i]]) { 577 | tempList[list[i]] = []; 578 | } 579 | tempList[list[i]].push(fun); 580 | } 581 | } 582 | }; 583 | 584 | var _off = function(eventName, fun, isOnce) { 585 | var tempList; 586 | if (!isOnce) { 587 | tempList = eventList; 588 | } else { 589 | tempList = eventOnceList; 590 | } 591 | if (tempList[eventName]) { 592 | var i = 0; 593 | var l = tempList[eventName].length; 594 | for (; i < l; i ++) { 595 | if (tempList[eventName][i] === fun) { 596 | tempList[eventName][i] = null; 597 | // 每次只清除一个相同事件绑定 598 | return; 599 | } 600 | } 601 | } 602 | }; 603 | 604 | function cleanNull(list) { 605 | var tempList = []; 606 | var i = 0; 607 | var l = list.length; 608 | if (l) { 609 | for (; i < l; i ++) { 610 | if (list[i]) { 611 | tempList.push(list[i]); 612 | } 613 | } 614 | return tempList; 615 | } else { 616 | return null; 617 | } 618 | } 619 | 620 | return { 621 | on: function(eventName, fun) { 622 | _on(eventName, fun); 623 | return this; 624 | }, 625 | once: function(eventName, fun) { 626 | _on(eventName, fun, true); 627 | return this; 628 | }, 629 | emit: function(eventName, data) { 630 | if (!eventName) { 631 | throw('No emit event name.'); 632 | } 633 | var i = 0; 634 | var l = 0; 635 | if (eventList[eventName]) { 636 | i = 0; 637 | l = eventList[eventName].length; 638 | for (; i < l; i ++) { 639 | if (eventList[eventName][i]) { 640 | eventList[eventName][i].call(this, data); 641 | } 642 | } 643 | eventList[eventName] = cleanNull(eventList[eventName]); 644 | } 645 | if (eventOnceList[eventName]) { 646 | i = 0; 647 | l = eventOnceList[eventName].length; 648 | for (; i < l; i ++) { 649 | if (eventOnceList[eventName][i]) { 650 | eventOnceList[eventName][i].call(this, data); 651 | _off(eventName, eventOnceList[eventName][i], true); 652 | } 653 | } 654 | eventOnceList[eventName] = cleanNull(eventOnceList[eventName]); 655 | } 656 | return this; 657 | }, 658 | off: function(eventName, fun) { 659 | _off(eventName, fun); 660 | return this; 661 | } 662 | }; 663 | }; 664 | 665 | } (window); 666 | --------------------------------------------------------------------------------