zoukankan      html  css  js  c++  java
  • 转载:前端通信那些事儿

    本文转载自:前端通信那些事儿
    https://juejin.im/post/5e7ae5f46fb9a07cb83e4cee
    由于作者使用的是vue.js,所有主要对vue.js的组件通信做总结。而且是.vue单文件组件的形式。用react.js的小伙伴不要失望,
    文章中有很多通用的通信知识点:比如DOM通过自定义事件通信,基于nodejs的EventEmitter通信,多Window通信 / Tab间通信等等。
    这里只讨论前端内部的通信,不涉及前后端通信。前后端之间的http通信,mqtt通信,跨域,文件上传等等等等,讲不完的。以后会单独开一篇文章做梳理。
    DOM通信
    DOM通过自定义事件通信
    vue组件间通信

    父子组件通信

    props v-bind(缩写为:) 父->子
    props, watch 父->子
    $emit v-on(缩写为@) 子->父
    v-bind:foo.sync 父<-->子
    refs 实例式伪通信
    跨组件通信
    共用事件对象式通信 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通过自定义事件通信
    触发事件 <-->增加了自定义事件DOM
    DOM通过自定义事件通信的意思是:可以为DOM增加一些自定义的事件,然后在某些情况下去触发这些事件,然后事件做出响应。
    说简单一些就是:增加了自定义事件的DOM,是一个鲜活的听话的人,发送对应的命令给它,它就会去做事。
    创建自定义事件(Creating custom events)
    DOM通信
    DOM通过自定义事件通信
    触发事件 <-->增加了自定义事件DOM
    DOM通过自定义事件通信的意思是:可以为DOM增加一些自定义的事件,然后在某些情况下去触发这些事件,然后事件做出响应。
    说简单一些就是:增加了自定义事件的DOM,是一个鲜活的听话的人,发送对应的命令给它,它就会去做事。
    创建自定义事件(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属性为事件增加数据。

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

    vue组件间通信
    组件间通信实在是一个老生常谈的话题,因为真的是每天都会遇到。

    父子组件通信

    props v-bind(缩写为:) 父->子
    props, watch 父->子
    $emit v-on(缩写为@) 子->父
    v-bind:foo.sync 父<-->子
    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>
    

    refs 实例式伪通信
    父->子 props, watch
    子->父 $emit, v-on
    父<-->子 v-bind:xxx.sync
    除上述3种方法外,我们还可以直接通过获得父子组件的实例去调用它们的方法,是一种伪通信。
    子组件通过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 跨组件通信。
    共用事件对象式通信 event bus

    // 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

    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

    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>
    

    基于nodejs的EventEmitter通信
    其实与基于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);
    }
    

    组合使用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秒后替换成别的图片。

    <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间通信

    这是一种在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在这个通道内进行通信)
    除了通过上述方式之外,还可以专门建立一个通信通道去交换数据。

    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间通信

    手上项目的热力图计算曾经尝试过将计算逻辑转移到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>
    
    

    多tab共享Shared worker,tab与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();
    

    向worker发送数据

    console.log('Message posted to worker');
    myWorker.port.postMessage();
    

    向worker接收数据

    myWorker.port.onmessage = function(e) {
        console.log('Message received from worker');
    }
    


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

  • 相关阅读:
    认识js运动
    BOM下的属性和方法---上
    BOM下的属性和方法---下
    鼠标跟随提示框
    [置顶] 关于CSDN2013博客之星的一些看法
    JSP内置对象---application
    C#中foreach语句的迭代器实现机制
    EBS动态创建账户组合实现
    稀里糊涂地被评为博客之星的候选人了,那就麻烦大家帮忙投个票吧~
    UNIX/Linux进程间通信IPC---管道--全总结(实例入门)
  • 原文地址:https://www.cnblogs.com/smart-girl/p/12571250.html
Copyright © 2011-2022 走看看