前端通訊那些事兒

前端通訊那些事兒.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的態度。

參考資料:

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!
相關文章
相關標籤/搜索