前端通讯那些事儿

前端通讯那些事儿.jpg

在近两年996模式下的近乎疯狂的迭代需求打磨平台的锻炼下,积累了一些前端通讯方面的一些实践经验,在这里作一个汇总。一来对本身作一个总结,二来也是给小伙伴们提供一些吸取。html

因为做者使用的是vue.js,全部主要对vue.js的组件通讯作总结。并且是.vue单文件组件的形式。用react.js的小伙伴不要失望,文章中有不少通用的通讯知识点:好比DOM经过自定义事件通讯,基于nodejs的EventEmitter通讯,多Window通讯 / Tab间通讯等等。前端

这里只讨论前端内部的通讯,不涉及先后端通讯。先后端之间的http通讯,mqtt通讯,跨域,文件上传等等等等,讲不完的。之后会单独开一篇文章作梳理。vue

DOM通讯

  • DOM经过自定义事件通讯

vue组件间通讯

  • 父子组件通讯node

    • props v-bind(缩写为:) 父->子
    • props, watch 父->子
    • $emit v-on(缩写为@) 子->父
    • v-bind:foo.sync 父<-->子
    • $parent,$refs 实例式伪通讯
  • 跨组件通讯react

    • 共用事件对象式通讯 event bus
    • 全局状态树式通讯 vuex
    • 路由式通讯 vue-router
    • 依赖注入式通讯 provide, inject
  • 基于nodejs的EventEmitter通讯

多Window通讯 / Tab间通讯

  • window.open与window.opener (基于当前window生成子window,实现父->子window的通讯)
  • localStorage/sessionStorage与window.onstorage (监听onstorage的event对象key的变化,实现tab间通讯)
  • BroadCast Channel (建立一个单独通讯通道,tab在这个通道内进行通讯)

Web worker通讯

  • Main thread与Web worker间通讯
  • 多tab共享Shared worker,tab与worker间的通讯

DOM通讯

DOM经过自定义事件通讯

触发事件 <-->增长了自定义事件DOMwebpack

DOM经过自定义事件通讯的意思是:能够为DOM增长一些自定义的事件,而后在某些状况下去触发这些事件,而后事件作出响应。git

说简单一些就是:增长了自定义事件的DOM,是一个鲜活的听话的人,发送对应的命令给它,它就会去作事。github

建立自定义事件(Creating custom events)

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { /* ... */ }, false);

// Dispatch the event.
elem.dispatchEvent(event);

增长自定义数据(Adding custom data - CustomEvent())

CustomEvent()能够经过detail属性为事件增长数据。web

var event = new CustomEvent('build', { detail: "foo" });
elem.addEventListener('build', function (e) { console.log(e.detail) });

关于应用 DOM经过自定义事件通讯 的实战,能够参考个人这篇博客:如何为DOM建立自定义事件?vue-router

vue组件间通讯

组件间通讯实在是一个老生常谈的话题,由于真的是天天都会遇到。

  • 父子组件通讯

    • props v-bind(缩写为:) 父->子

      • props, watch 父->子
      • $emit v-on(缩写为@) 子->父
      • v-bind:foo.sync 父<-->子
      • $parent,$refs 实例式伪通讯
  • 跨组件通讯

    • 共用事件对象式通讯 event bus
    • 全局状态树式通讯 vuex
    • 路由式通讯 vue-router
    • 依赖注入式通讯 provide, inject
  • 基于nodejs的EventEmitter通讯

父子组件通讯

props v-bind(缩写为:) 父->子

父组件的数据单向传递到子组件。

// 父组件 Parent.vue
<template>
  <Child :foo="hello child"></Child>
</template>

<script>
import Child from './child';
export default {
  name: 'parent',
  components: { Child },
};
</script>
// 子组件 Child.vue
<template>
  <div>{{foo}}</div>
</template>

<script>
export default {
  name: 'child',
  props: {
    foo: {
      type: String,
      default: '',
    },
  },
};
</script>

props, watch 父->子

子组件监听的父组件属性若是不只仅作相似{{foo}}这样的模板渲染,可使用watch作监听。

父组件中的传入子组件props的变量发生变化时,能够经过watch监听对应的prop属性,作出对应的操做。
这也算是一种父子组件通讯的方式。

// 父组件
<Child :foo="parent.foo" @child-msg-emit="childMsgOn"></Child>
// 子组件
watch: {
    foo(val) {
      console.log("foo更新为:", val);
    }
},

$emit v-on(缩写为@) 子->父

子组件经过$emit向父组件传递数据。
父组件经过v-on接收数据。
两者须要约定好相同的事件名。

// 父组件 Parent.vue
<template>
  <Child :foo="hello child" @child-msg-emit="childMsgOn"></Child>
</template>

<script>
import Child from './child';
export default {
  name: 'parent',
  components: { Child },
  methods: {
    childMsgOn(msg) {
      console.log(msg); //'hello parent'
    },
  },
};
</script>
// Child.vue
<template>
  <div>{{foo}}</div>
</template>

<script>
export default {
  name: 'child',
  props: {
    foo: {
      type: String,
      default: '',
    },
  },
  mounted() {
    this.$emit('child-msg-emit', 'hello parent');
  },
};
</script>

v-bind:foo.sync 父<-->子

除了用$emit和v-on,父组件传入子组件的prop能够双向绑定吗?能够用.sync。

可能有小伙伴对这个.sync修饰符不熟悉,但它其实很是有用。
sync是一个语法糖,简化v-bind和v-on为v-bind.sync和this.$emit('update:xxx')。为咱们提供了一种子组件快捷更新父组件数据的方式。

首先将传递给foo的值放在一个变量中。

...
  <Child :foo="parent.foo" @child-msg-emit="childMsgOn"></Child>
  data() {
     return {
         parent: { foo: "hello child" }
     }
  },
  methods: {
    childMsgOn(msg) {
      console.log(msg); //'hello parent'
      this.parent.foo = msg;
    },
  }
...
<Child
  v-bind:foo="parent.foo"
  v-on:child-msg-emit="childMsgOn"
></Child>

在vue中,父组件向子组件传递的props是没法被子组件直接经过this.props.foo = newFoo去修改的。
除非咱们在组件this.$emit("child-msg-emit", newFoo),而后在父组件使用v-on作事件监听child-msg-emit事件。如果想要可读性更好,能够在$emit的name上改成update:foo,而后v-on:update:foo。

有没有一种更加简洁的写法呢???
那就是咱们这里的.sync操做符。
能够简写为:

<Child v-bind:foo.sync="parent.foo"></Child>

子组件触发:this.$emit("update:foo", newFoo);

而后在子组件经过this.$emit("update:foo", newFoo);去触发,注意这里的事件名必须是update:xxx的格式,由于在vue的源码中,使用.sync修饰符的属性,会自定生成一个v-on:update:xxx的监听。

<Child v-bind:foo="parent.foo" v-on:update:foo="childMsgOn"></Child>

若是想从源码层面理解v-bind:foo.sync,能够参考个人这篇文章:如何理解vue中的v-bind?

$parent,$refs 实例式伪通讯

父->子 props, watch
子->父 $emit, v-on
父<-->子 v-bind:xxx.sync

除上述3种方法外,咱们还能够直接经过得到父子组件的实例去调用它们的方法,是一种伪通讯。

子组件经过$parent能够拿到父组件的vue实例,从而调用属性和方法。
父组件能够经过$refs拿到子组件的vue实例,从而调用属性和方法。

// parent.vue
<Child ref="child" :foo="parent.foo" @child-msg-emit="childMsgOn"></Child>
  methods: {
    parentMethod() {
      console.log("I am a parent method");
    },
    $refCall() {
        this.$refs.child.childMethod(); // I am a child method
    }
  }
// child.vue
  methods: {
    childMethod() {
      console.log("I am a child method");
    },
    $parentCall() {
        this.$parent.parentMethod(); // I am a parent method
    }
  }

跨组件通讯

想一想一种状况,有这样一个组件树。
红色组件想和黄色组件进行通讯。
image

红色组件能够经过逐级向上$emit,而后经过props逐级向下watch,最后更新黄色组件。

显然这是一种很愚蠢的方法,在vue中有多种方式去作更加快速的跨组件通讯,好比event bus 跨组件通讯,vue-router 区分新增与编辑,vuex 全局状态树和provide, inject 跨组件通讯。

共用事件对象式通讯 event bus

image

名字听起来高大上,但其实使用起来很简单。
下面演示一个注册为plugin的用法。

// plugins/bus/bus.js
import Vue from 'vue';
const bus = new Vue();
export default bus;
// plugins/bus/index.js
import bus from './bus';
export default {
  install(Vue) {
    Vue.prototype.$bus = (() => bus)();
  },
};
// main.js
import bus from 'src/plugins/bus';
Vue.use(bus);

注册为全局plugin以后,就能够经过this.$bus使用咱们的event bus了。

红色组件发送事件:

this.$bus.$emit('yellowUpdate', 'hello yellow.');

黄色组件接收事件:

this.$bus.$on('yellowUpdate',(payload)=>{
    console.log(payload); // hello yellow
});
  • 优势:最快捷的跨组件通讯方式,支持双工通讯(通讯专业的我告诉你们,双工能够理解为双向通讯),上手简单。
  • 缺点:事件在组件间穿透,数据传递层级关系不明显,出现bug难以快速定位

全局状态树式通讯 vuex

image

vuex是vue生态很重要的一个附加plugin,进行前端的状态管理。
除前端状态管理以外,由于这是一个全局的状态树,状态在全部组件都是共享的,所以vuex其实也是一个跨组件通讯的方式。

定义store

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const state = {
  userInfo: {
      id: '',
      name: '',
      age: '',
  },
};

const mutations = {
  UPDATE_USER(state, info) {
    state.userInfo = info;
  },
};

export default new Vuex.Store({
  state,
  mutations
});

红色组件:更新状态树的state:mapMutation

<script>
import { mapMutations } from 'vuex';
export default {
  name: 'set-state',
  methods: {
        ...mapMutations(['UPDATE_USER']),
  },
  created(){
      this.UPDATE_USER({ id: 1, name: 'foo', age: 25 });
  }
}
</script>

黄色组件:得到状态树的state:mapState

<template>
     <ul>
       <li>{{ user.id }}</li>
       <li>{{ user.name }}</li>
       <li>{{ user.age }}</li>
     </ul>
</template>
 <script>
import { mapState } from 'vuex';
export default {
  name: 'get-state',
  computed: {
    ...mapState({
      user: 'userInfo',
    })
  },
}
</script>

路由式通讯 vue-router

没想到吧,vue-router不只仅能够作路由管理,还能够区分组件的编辑和新增状态。
由于对于一个新增或者编辑组件,数据基本上都是一致的,通常都是在同一个组件内增长一个标识去区分新增或者编辑。

这个标识能够是组件自身的一个status属性,也能够是经过props传入的status属性。

也能够不加这种标识,直接经过vue-router去作到。并且用vue-router还能够直接将数据带过去。

父组件经过vue-router的query带数据过去。

this.$router.push({ name: 'componentPost', query: { type: 'edit', data } });

新增/编辑子组件获得数据并作填充。

created() {
    // 判断到是编辑状态
    if(this.$route.query.type==="edit"){
        const data = this.$route.query.data;
        // do some thing with data
    }
}

依赖注入式通讯 provide, inject

image

provide,inject实际上是一种“解决后代组件想访问共同的父组件,$parent层级过深且难以维护问题“的利器。其中心思想是依赖注入。

学习过react的同窗应该知道context这个概念,在vue中,provide/inject与之很相似。

经过vue官方的例子咱们作一个解释:

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>

google-map-region和google-map-markers若是都想得到祖先元素google-map实例,而后调用它的方法getMap。

// google-map-region这样作
this.$parent.getMap();
// google-map-markers这样作
this.$parent.$parent.getMap();

若是还在google-map-markers 组件下还有子组件呢?this.$parent.$parent.$parent.getMap();

这种代码还能看吗???并且后期组件结构有变更的话,根本没法维护。

为了解决这个问题,可使用provide/inject。

// google-map.vue
<script>
export default {
  name: "child",
  provide() {
      return {
          getMap: this.getMap
      }
  }
};
</script>
// google-map-region.vue,google-map-markers.vue等后代组件这样使用
<script>
export default {
  name: "child",
  inject: ['getMap']
  mouted(){
       this.getMap(); // 这样就能够访问到google-map的getMap方法了。
  }
};
</script>

当注入一个属性时,能够将注入值做为默认值或者数据入口。能够经过from更名字。

// google-map.vue
<script>
export default {
  name: "child",
  provide() {
      return {
          foo: 'hello inject, I am foo',
          bar: 'hello inject, I am bar',
          getMap: this.getMap
      }
  }
};
</script>
// google-map-region.vue,google-map-markers.vue等后代组件这样使用
<script>
export default {
  name: "child",
  inject: {
      primitiveFoo: 'foo',
      specialFoo: {
         from: 'bar',
         default: '默认属性'
      },
       googleMapGetMap: 'getMap',
  },
  mouted() {
      // 这样就能够访问到google-map的foo属性了。
      this.primitiveFoo; // 'hello inject, I am foo'
      this.specialFoo; // 'hello inject, I am bar'
      this.googleMapGetMap(); // 这样就能够访问到google-map的getMap方法了。
  }
};
</script>

具体能够参考:provide / inject依赖注入

基于nodejs的EventEmitter通讯

其实与基于vue实例的event bus很相似。都是很简单的双向通讯的,基于订阅发布模型的通讯方式。
image

若是是基于webpack,vue/react等等现代化的基于nodejs开启本地服务器和打包发布的项目,能够在项目中使用nodejs的EventEmitter。

按照本身喜欢的名称overwrite原来的方法:

import { EventEmitter } from 'events';

class Emitter extends EventEmitter {
  $emit(eventName, cargo) {
    this.emit(eventName, cargo);
  }
  $on(eventName, callback) {
    this.on(eventName, callback);
  }
  $off(eventName, callback) {
    this.removeListener(eventName, callback);
  }
}

export default new Emitter();

红色组件使用emitter $emit发送事件

import emitter from '../emitter';
emitter.$emit('foo-bar-baz', 'hello yellow');

黄色组件使用emitter $on接收事件

import emitter from '../emitter';
emitter.$on('foo-bar-baz', (msg)=>{
    console.log(msg); // 'hello yellow'
});

最后使用$off销毁事件。
如果在vue中,建议在beforeDestroy()生命周期中使用,而且须要将$on的callback赋值为一个具名回调。

mounted(){
    this.fooBarBazHandler =  (msg)=>{
        console.log(msg); // 'hello yellow'
    }
    emitter.$on('foo-bar-baz', this.fooBarBazHandler);
}
beforeDestroy() {
    emitter.$off('iText-confirm', this.fooBarBazHandler);
},

组合使用watch,vuex,event bus可能起到意想不到的效果,我手上开发的PC端聊天模块,就是基于watch,vuex和event bus实现的,很是强大。

我相信你们在实际开发中能够找到本身的最佳实践。

多Window / Tab / Page 通讯

这是一个很是常见的场景,当你打开了一个页面须要与另外一个页面作数据传递时,组件间通讯那一套是行不通的。
由于每一个window/page/tab都是单独的一个vue实例,单独的vuex实例,即便是nodejs的e、EventEmitter,也是一个单独的emitter实例。

这要怎么办呢?其实浏览器为咱们提供了多种方式去作这件事。

  • window.open与window.opener (基于当前window生成子window,实现父->子window的通讯)
  • localStorage与window.onstorage (监听onstorage的event对象key的变化,实现tab间通讯)
  • BroadCast Channel (建立一个单独通讯通道,tab在这个通道内进行通讯)
  • Shared worker (开启一个共享的工做线程,tab在这个共享线程内进行通讯)

window.open与window.opener (基于当前window生成子window,实现父->子window的通讯

假设下面这样一个场景:点击图片打开一个新的window,1秒后替换成别的图片。
image

<img :src="src"  @click="openChildWindow()"/>
openChildWindow(){
     // window.open会返回子window对象
     this.childWindow = window.open("https://foo.bar.com/baz.jpg");
     setTimeout(()=>{
        // 经过this.childWindow访问到子对象进行操做
         this.childWindow.location.replace("https://foo.bar.com/baz.png");
     }, 1000)
}

this.childWindow.opener就是当前的window实例,在子window内也能够访问到父window进行操做。

localStorage/sessionStorage与window.onstorage (监听onstorage的event对象key的变化,实现tab间通讯

image

这是一种在tab已经打开后,没法明显创建父子关系的场景下经常使用的方法。

Tab A:在localStorage/sessionStorage中set一个新值

window.localStorage.setItem('localRefresh', +new Date());
window.sessionStorage.setItem('sessionRefresh', +new Date());

Tab B:监听storage的变化

window.onstorage = (e) => {
  if (e.key === 'localRefresh') {
      // do something
  }
  if (e.key === 'sessionRefresh'') {
      // do something
  }
};

这样咱们就实现TabA和TabB之间的通讯了。

BroadCast Channel (建立一个单独通讯通道,tab在这个通道内进行通讯)

除了经过上述方式以外,还能够专门创建一个通讯通道去交换数据。

image

window建立一个channel而且发送消息给tab和iframe

const bc = new BroadcastChannel('test_channel');
bc.postMessage('This is a test message.');

tab和iframe接收channel数据

只要与父window创建同名BroadcastChannel便可。

const bc = new BroadcastChannel('test_channel');
bc.onmessage = function (event) { 
    console.log(event); // 'This is a test message.'包含在event对象中。
}

Web worker通讯

Main thread与Web worker间通讯

image

手上项目的热力图计算曾经尝试过将计算逻辑转移到worker子线程计算,可是因为种种缘由没有成功,可是积累了这方面的经验。

worker线程

// src/workers/test.worker.js
onmessage = function(evt) {
  // 工做线程收到主线程的消息
  console.log("worker thread :", evt); // {data:{msg:”Hello worker thread.“}}
  // 工做线程向主线程发送消息
  postMessage({
    msg: "Hello main thread."
  });
};

main线程

// src/pages/worker.vue
<template>
  <div>Main thread</div>
</template>

<script>
import TestWorker from "../workers/test.worker.js";

export default {
  name: "worker",
  created() {
    const worker = new TestWorker();
   // 主线程向工做线程发送消息
    worker.postMessage({ msg: "Hello worker thread." });
   // 主线程接收到工做线程的消息
    worker.onmessage = function(event) {
      console.log("main thread", event); // {data:{msg:"Hello main thread."}}
    };
  }
};
</script>

image

更多如何在vue项目中使用Main thread与Web worker间通讯的demo能够查看:一次失败的用web worker提高速度的实践

多tab共享Shared worker,tab与worker间的通讯

image

Shared worker是一种web worker技术。
mdn的这个demo为咱们清晰地展现了如何使用SharedWorker,实现tab对worker的共享。

SharedWorker的执行脚本worker.js

onconnect = function(e) {
  var port = e.ports[0];

  port.onmessage = function(e) {
   var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }

}

Tab A与Tab B都新建名为worker.js的SharedWorker

var myWorker = new SharedWorker("worker.js");
myWorker.port.start();
向worker发送数据
console.log('Message posted to worker');
myWorker.port.postMessage();
向worker接收数据
myWorker.port.onmessage = function(e) {
    console.log('Message received from worker');
}

地址:http://mdn.github.io/simple-s...
worker-1
image
worker-2
image

共享了什么,共享了一个乘法worker,worker1和worker2均可以用,在这里是乘法运算。
image

总结

在这边博文中咱们学习到了DOM通讯,vue组件间通讯,多Window通讯 / Tab间通讯,Web worker通讯等等前端通讯的知识点。
可是要知道,这些仅仅是实际开发中的可选项集合,具体使用什么样的技术,仍是要结合具体的应用场景。
并且在前端突飞猛进的更新换代中,会有老的技术消失,会有新的技术出现。必定要保持stay hungry的态度。

参考资料:

期待和你们交流,共同进步,欢迎你们加入我建立的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
相关文章
相关标签/搜索