阅读 3453

JS通信方式知多少?JS和多端应用通信

常见通信方式

postMessage

otherWindow.postMessage(message, targetOrigin, [transfer]);

  1. otherWindow
    • 其他窗口的一个引用,比如iframecontentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
  2. message
    • 将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
  3. targetOrigin
    • 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
  4. transfer 可选
    • 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
  • 使用
 window.parent.postMessage({
    close: true,
    type: 'shuaka',
    result:msg.result,
    errmeesage:msg.meesage,
    cardMap:msg.map
    }, 'http://192.168.60.104:3010/'
);
window.addEventListener('message', e => {
   console.log(e)
	var d = e.data;  //e.data  里面有自己所传的所有参数  可以根据参数做自己的判断
});
复制代码
  • postMessage可以解决跨窗口和跨域之前的消息通信。
  • 注意
    • 如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。
    • 当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*
    • 无法检查originsource属性会导致跨站点脚本攻击。

Ajax

  • Ajax - Asynchronous JavaScript and XML(异步的 JavaScriptXML),可以用于和服务器之间的通信用于获取数据。
let xmlhttp;
if (window.XMLHttpRequest) {
	//  IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
	xmlhttp=new XMLHttpRequest();
} else {
	// IE6, IE5 浏览器执行代码
	xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function() {
	if (xmlhttp.readyState === 4 && xmlhttp.status === 200)	{
		document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
	}
}
xmlhttp.open("GET","/try/ajax/ajax_info.txt",true);
xmlhttp.send();
复制代码

WebSocket

  • js中的WebSocket
  • WebSockethtml5提供的一种在单个TCP连接上进行双向通信的协议,解决了客户端和服务端之间的实时通信问题。浏览器和服务器只需完成一次握手,两者之间就可以创建一个持久性的TCP连接,此后服务器和客户端通过此TCP连接进行双向实时通信。
  1. 优点
    • 很多网站为了实现数据推送,所用的技术都是ajax轮询。轮询是在特定的时间间隔,由浏览器主动发起请求,将服务器的数据拉回来。轮询需要不断的向服务器发送请求,会占用很多带宽和服务器资源。WebSocket建立TCP连接后,服务器可以主动给客户端传递数据,能够更好的节省服务器资源和带宽,实现更实时的数据通讯。
    function webSocket(){
        if("WebSocket" in window) {
            console.log("您的浏览器支持WebSocket");
            var ws = new WebSocket("ws://localhost:8080"); //创建WebSocket连接
            //...
        } else { 
            console.log("您的浏览器不支持WebSocket");
        }
    }
    复制代码
  2. 使用
    • 客户端支持WebSocket的浏览器中,在创建socket后,可以通过onopenonmessageoncloseonerror四个事件对socket进行响应。WebSocket的所有操作都是采用事件的方式触发的,这样不会阻塞UI,是的UI有更快的响应时间,有更好的用户体验。
    • 浏览器通过JavaScript向服务器发出建立WebSocket连接的请求,连接建立后,客户端和服务器就可以通过TCP连接直接交换数据。当你获取WebSocket连接后,可以通多send()方法向服务器发送数据,可以通过onmessage事件接收服务器返回的数据。
    var ws = new WebSocket("ws://localhost:8080"); 
    //申请一个WebSocket对象,参数是服务端地址,同http协议使用http://开头一样,WebSocket协议的url使用ws://开头,另外安全的WebSocket协议使用wss://开头
    ws.onopen = function(){
        //当WebSocket创建成功时,触发onopen事件
        console.log("open");
        ws.send("hello"); //将消息发送到服务端
    }
    ws.onmessage = function(e){
        //当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
        console.log(e.data);
    }
    ws.onclose = function(e){
        //当客户端收到服务端发送的关闭连接请求时,触发onclose事件
        console.log("close");
    }
    ws.onerror = function(e){
        //如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
        console.log(error);
    }
    复制代码

场景

  • 由于公司内部的一个社交产品需要开发一个新的功能(笔记本),大致功能就是用户可以在自己的账号上面建一些笔记,笔记的编写格式是markdown语法。编辑区上方还有一个toolbar,类似于富文本编辑器上方那种。重点是产品有多端包括iOSAndroid以及客户端,笔记本编辑器的导出上传等需要调用对应端的方法,所以需要和各端保持统一的通信。

解决方案

方案一:发布订阅模式

  • 各端通过实现发布订阅的一个中间层,将改对象挂载到应用的全局对象,编辑器和客户端通过发布和订阅的方法去通信。
  • TS实现如下:
// communite.ts
class Event {
  public handlers: any = {}; // 事件容器,用来装事件数组(因为订阅者可以是多个)
  public note: object = { // 写死的note属性
    id: 1234,
    language: "en-US",
    themeColor: "red",
  };

  // 事件添加方法,参数有事件名和事件方法
  public addEvent(type: string, handler: (...param: any[]) => void) {
    // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
    if (!(type in this.handlers)) {
      this.handlers[type] = [];
    }
    // 将事件存入/替换
    this.handlers[type][0] = handler;
    // this.handlers[type].push(handler); // 同一事件名多个回调函数
  }

  // 触发事件两个参数(事件名,参数)
  public dispatchEvent(type: string, params?: any[], callback?: (...param: any) => void) {
    // 若没有注册该事件则抛出错误
    if (!(type in this.handlers)) {
      return new Error("未注册该事件");
    }
    // tslint:disable-next-line:no-console
    console.log("dispatchEvent params", type, params);
    // tslint:disable-next-line:whitespace
    switch(type) {
      case "touchCharSize" :
        callback ? callback() : this.handlers[type][0]();
        break;
      case "charSize":
        // tslint:disable-next-line:no-console
        console.log(params);
        // callback ? callback(content) : this.handlers[type][0](content);
        break;
      case "upload":
        const fileObject = {
          filename: "test",
          id: "123456",
          size: 1024,
          type: "pdf",
          url: "/sdad/dsads/test.pdf",
        };
        callback ? callback(fileObject) : this.handlers[type][0](fileObject);
        break;
      default:
        break;
    }
    // 触发
    // this.handlers[type].forEach((handler: (...params: any[]) => void) => {
    //   handler(...params)
    // })
  }

  // 事件移除参数(事件名,删除的事件,同一事件多个回调函数若无第二个参数则删除该事件的订阅和发布)
  public removeEventListener(type: string, handler?: () => void) {
      // 无效事件抛出
      if (!(type in this.handlers)) {
        return new Error("无效事件");
      }
      if (!handler) {
        // 直接移除事件
        delete this.handlers[type];
      } else {
        const idx = this.handlers[type].findIndex((ele: () => void) => ele === handler);
        // 抛出异常事件
        if (idx === -1) {
          return new Error("无该绑定事件");
        }
        // 移除事件
        this.handlers[type].splice(idx, 1);
        if (this.handlers[type].length === 0) {
          delete this.handlers[type];
        }
      }
    }
}

export default new Event();
复制代码
  • 当客户端需要获取某个数据时,客户端通过触发(dispatchEvent)对应的事件名称到达中间层,中间层去执行编辑器中对应事件的回调函数,在回调函数中去触发一个新的事件,将数据发给客户端,客户端在中间层监听对应的事件拿到结果去处理。下面以获取笔记本当前编辑的内容为例:
// conmmunite.ts 中间层已实现对应的touchCharSize和charSize事件处理
// 客户端触发事件:touchCharSize
// 笔记本编辑器添加:touchCharSize,并执行回调触发:charSize

import Event from './communite.ts'
// 获取笔记字数
Event.addEventListener('touchCharSize', () => {
  Event.dispatchEvent('charSize', {
    total: 46000, // 总字数
    current: 12233 // 当前字数
  });
})
Event.dispatchEvent('touchCharSize'); // 客户端触发获取字数事件

// 获取笔记字数
Event.addEventListener('charSize', () => {
  console.log('charSize');
})
复制代码
  • 由于获取字数是由客户端主动触发,所以我们需要两个事件去完成整个的通信流程,客户端拿到数据都是在笔记本编辑器触发的事件里面。

方案二:各自实现

  • 发布订阅模式中,我们需要一个中间层去存储我们的事件,两端通信触发比较复杂,所以有了第二种实现方案。
  • 由于应用中可以执行应用内的JavaScript方法,所以客户端获取笔记本编辑器中内一些信息时,可以通过执行JavaScript的方法。
// 类似安卓中的实现
WebView webView;
Button buttonLeft;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main4);
    webView=findViewById(R.id.webview);
    buttonLeft = findViewById(R.id.btnLeft);

    WebSettings webSettings=webView.getSettings();
    webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//缓存
    webSettings.setJavaScriptEnabled(true);
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);//允许弹窗
    webView.getSettings().setDomStorageEnabled(true);
    webView.loadUrl("file:///android_asset/evaluate.html");
    buttonLeft.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(final View v) {
            webView.post(new Runnable() {
                @RequiresApi(api = Build.VERSION_CODES.KITKAT)
                @Override
                public void run() {
                   webView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
                   @Override
                   public void onReceiveValue(String s) {
                       buttonLeft.setText(s);
                    }
                  });
                  // 执行JavaScript函数
                  webView.loadUrl("javascript:detanx()");
                }
            });
        }
    });
    webView.setWebChromeClient(new WebChromeClient(){
        @Override
        public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
            AlertDialog.Builder builder=new AlertDialog.Builder(Main4Activity.this);
            builder.setTitle("alert1");
            builder.setMessage(message);
            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    result.confirm();
                }
            });
            builder.setCancelable(false);
            builder.create().show();
            return true;
        }
    });
}
复制代码
  • index.html中添加一个对应的方法。
// index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
  <title></title>
</head>
<body>
  <div id="detanx"></div>
  <script>
    function detanx() {
      document.getElementById('detanx').innerHTML = 'detanx';
    }
  </script>
</body>
</html>
复制代码
  • 应用调用JavaScript就实现了,然后应用在全局挂载一个对象(Event),对象上包含一些方法,我们通过Event这个对象,在JavaScript中去执行对应事件的方法,我们再通过JavaScript去实现一个统一的回调函数,在应用中我们触发对应的事件后,应用通过执行统一的那个回调函数去返回对应的结果,在回调函数中,我们通过一个唯一的标识去表示每一次触发的事件,让后去执行对应事件回调回来后的函数。下面是个示例:
// 缓存
const list = {}
// 触发方法并缓存
function dispatchEvent() {
  // 生成唯一标识
  const markid = new Date().getTime().toString();
  list.markid = setContent;
  Event.getContent(noteid, markid);
}
// 设置内容执行函数
function setContent(result) {
  const { content } = result
  console.log(result);
}
// 应用处理完成统一回调函数
function commonCallbak (markid, result) {
  if(list.markid) {
    list.markid(result);
    // 移除缓存
    delete list.markid;
  } else {
    throw new Error('markid is not exist.')
  }
}
复制代码
  • 这种实现可以省去中间层的实现,但是前提是多端都能支持这种调用JavaScript的方式。

总结

  • 通信的方式有很多例如websocketAjaxpostMessage,发布订阅模式等等,根据不同的具体业务场景我们用到的技术方案肯定也是不同的,没有哪种方案是最好的,只有最适合的,我们只有对每种方案的优缺点有足够的了解才会知道当前哪种方案是最好的。