在近兩年996模式下的近乎瘋狂的迭代需求打磨平臺的鍛鍊下,積累了一些前端通訊方面的一些實踐經驗,在這裏作一個彙總。一來對本身作一個總結,二來也是給小夥伴們提供一些吸取。html
因爲做者使用的是vue.js,全部主要對vue.js的組件通訊作總結。並且是.vue單文件組件的形式。用react.js的小夥伴不要失望,文章中有不少通用的通訊知識點:好比DOM經過自定義事件通訊,基於nodejs的EventEmitter通訊,多Window通訊 / Tab間通訊等等。前端
這裏只討論前端內部的通訊,不涉及先後端通訊。先後端之間的http通訊,mqtt通訊,跨域,文件上傳等等等等,講不完的。之後會單獨開一篇文章作梳理。vue
父子組件通訊node
跨組件通訊react
觸發事件 <-->增長了自定義事件DOM
webpack
DOM經過自定義事件通訊的意思是:能夠爲DOM增長一些自定義的事件,而後在某些狀況下去觸發這些事件,而後事件作出響應。git
說簡單一些就是:增長了自定義事件的DOM,是一個鮮活的聽話的人,發送對應的命令給它,它就會去作事。github
var event = new Event('build'); // Listen for the event. elem.addEventListener('build', function (e) { /* ... */ }, false); // Dispatch the event. elem.dispatchEvent(event);
CustomEvent()能夠經過detail屬性爲事件增長數據。web
var event = new CustomEvent('build', { detail: "foo" }); elem.addEventListener('build', function (e) { console.log(e.detail) });
關於應用 DOM經過自定義事件通訊 的實戰,能夠參考個人這篇博客:如何爲DOM建立自定義事件?。vue-router
組件間通訊實在是一個老生常談的話題,由於真的是天天都會遇到。
父子組件通訊
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>
子組件監聽的父組件屬性若是不只僅作相似{{foo}}這樣的模板渲染,可使用watch作監聽。
父組件中的傳入子組件props的變量發生變化時,能夠經過watch監聽對應的prop屬性,作出對應的操做。
這也算是一種父子組件通訊的方式。
// 父組件 <Child :foo="parent.foo" @child-msg-emit="childMsgOn"></Child>
// 子組件 watch: { foo(val) { console.log("foo更新爲:", val); } },
子組件經過$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>
除了用$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?。
父->子 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 } }
想一想一種狀況,有這樣一個組件樹。
紅色組件想和黃色組件進行通訊。
紅色組件能夠經過逐級向上$emit,而後經過props逐級向下watch,最後更新黃色組件。
顯然這是一種很愚蠢的方法,在vue中有多種方式去作更加快速的跨組件通訊,好比event bus 跨組件通訊,vue-router 區分新增與編輯,vuex 全局狀態樹和provide, inject 跨組件通訊。
名字聽起來高大上,但其實使用起來很簡單。
下面演示一個註冊爲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 });
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不只僅能夠作路由管理,還能夠區分組件的編輯和新增狀態。
由於對於一個新增或者編輯組件,數據基本上都是一致的,通常都是在同一個組件內增長一個標識去區分新增或者編輯。
這個標識能夠是組件自身的一個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實際上是一種「解決後代組件想訪問共同的父組件,$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和依賴注入
其實與基於vue實例的event bus很相似。都是很簡單的雙向通訊的,基於訂閱發佈模型的通訊方式。
若是是基於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/page/tab都是單獨的一個vue實例,單獨的vuex實例,即便是nodejs的e、EventEmitter,也是一個單獨的emitter實例。
這要怎麼辦呢?其實瀏覽器爲咱們提供了多種方式去作這件事。
假設下面這樣一個場景:點擊圖片打開一個新的window,1秒後替換成別的圖片。
<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進行操做。
這是一種在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之間的通訊了。
除了經過上述方式以外,還能夠專門創建一個通訊通道去交換數據。
const bc = new BroadcastChannel('test_channel'); bc.postMessage('This is a test message.');
只要與父window創建同名BroadcastChannel便可。
const bc = new BroadcastChannel('test_channel'); bc.onmessage = function (event) { console.log(event); // 'This is a test message.'包含在event對象中。 }
手上項目的熱力圖計算曾經嘗試過將計算邏輯轉移到worker子線程計算,可是因爲種種緣由沒有成功,可是積累了這方面的經驗。
// src/workers/test.worker.js onmessage = function(evt) { // 工做線程收到主線程的消息 console.log("worker thread :", evt); // {data:{msg:」Hello worker thread.「}} // 工做線程向主線程發送消息 postMessage({ msg: "Hello main thread." }); };
// 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>
更多如何在vue項目中使用Main thread與Web worker間通訊的demo能夠查看:一次失敗的用web worker提高速度的實踐
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();
console.log('Message posted to worker'); myWorker.port.postMessage();
myWorker.port.onmessage = function(e) { console.log('Message received from worker'); }
地址:http://mdn.github.io/simple-s...
worker-1
worker-2
共享了什麼,共享了一個乘法worker,worker1和worker2均可以用,在這裏是乘法運算。
在這邊博文中咱們學習到了DOM通訊,vue組件間通訊,多Window通訊 / Tab間通訊,Web worker通訊等等前端通訊的知識點。
可是要知道,這些僅僅是實際開發中的可選項集合,具體使用什麼樣的技術,仍是要結合具體的應用場景。
並且在前端突飛猛進的更新換代中,會有老的技術消失,會有新的技術出現。必定要保持stay hungry的態度。
參考資料:
期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:
- SegmentFault技術圈:ES新規範語法糖
- SegmentFault專欄:趁你還年輕,作個優秀的前端工程師
- 知乎專欄:趁你還年輕,作個優秀的前端工程師
- Github博客: 趁你還年輕233的我的博客
- 前端開發QQ羣:660634678
- 微信公衆號: 生活在瀏覽器裏的咱們 / excellent_developers
努力成爲優秀前端工程師!