Vue组件相关操作

79 阅读4分钟

Vue通信

1.非父子组件通信

  • 在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。

  • 两种方式:

    • Provide/Inject
    • Mitt全局事件总线(第三方库)

1.1Provide和Inject

  • Provide/Inject用于非父子组件之间共享数据

    • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
    • 在这种情况下,如果还使用props一层一层接收传递实在是太麻烦
  • 这种情况下,使用Procide和Inject是一个好的选择

    • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖

      提供者

    • 父组件有一个 provide 选项来提供数据

    • 子组件有一个 inject 选项来开始使用这些数据

  • 你可以将依赖注入看作是"long range props"

    • 父组件不需要知道哪些子组件使用它 provide 的 property
    • 子组件不需要知道 inject 的 property 来自哪里

1.2代码操作

  • App.vue

    <template>
      <div>
        <Home></Home>
        <button @click="addName">+name</button>
      </div>
    </template>
    <script>
      import Home from './Home.vue';
      import { computed } from "vue"
      export default {
        components: {
          Home
        },
        provide: {
          name:"tim",
          age:18,
          length:computed(()=> this.names.length)
          
        }
        data() {
          return {
            names:["abc","cba","nba"]
          }
        },
        methods: {
          addName(){
            this.names.push("tim")
          }
        }
      }
    </script>
    
  • Home.vue

    <template>
      <div>
        <HomeContent></HomeContent>
      </div>
    </template><script>
      import HomeContent from './HomeContent.vue';
    ​
      export default {
        components: {
          HomeContent
        }
      }
    </script><style scoped></style>
    
  • HomeContent.vue

    <template>
      <div>
        HomeContent: {{name}} - {{age}} - {{length.value}}
      </div>
    </template><script>
      export default {
        inject: ["name", "age", "length"],
      }
    </script><style scoped></style>
    

2.全局事件总线mitt库

  • Vue3从实例中移除了 onon、off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库
  • Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;
  • 这里我们主要的是mitt库的使用;
  • 安装这个库

    npm install mitt
    
  • 我们可以封装一个工具utils/eventbus.js

    //eventbus.js
    import mitt from "mitt";
    ​
    const emitter = mitt()
    ​
    export default emitter;
    
  • 以下代码演示

    1. App.vue

      <template>
        <div>
          <home/>
          <about/>
        </div>
      </template><script>
        import Home from './Home.vue';
        import About from './About.vue';
      ​
        export default {
          components: {
            Home,
            About
          }
        }
      </script><style scoped></style>
      
    2. About.vue

      <template>
        <div>
          <button @click="btnClick">按钮点击</button>
        </div>
      </template><script>
        import emitter from './utils/eventbus';
      ​
        export default {
          methods: {
            btnClick() {
              console.log("about按钮的点击");
              emitter.emit("tim", {name: "tim", age: 18});
              // emitter.emit("kobe", {name: "kobe", age: 30});
            }
          }
        }
      </script><style scoped></style>
      
    3. HomeContent.vue

      <template>
        <div>
        </div>
      </template><script>
        import emitter from './utils/eventbus';
      ​
        export default {
          created() {
            emitter.on("tim", (info) => {
              console.log("tim:", info);
            });
      ​
            emitter.on("kobe", (info) => {
              console.log("kobe:", info);
            });
      ​
            emitter.on("*", (type, info) => {
              console.log("* listener:", type, info);
            })
          }
        }
      </script><style scoped></style>
      


3.插槽

  • 在开发中,我们会经常封装一个个可复用的组件:

    • 为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。
    • 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
    • 我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
  • 举个栗子:假如我们定制一个通用的导航组件 - NavBar

    • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
    • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
    • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
    • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

3.1如何使用插槽呢?

  1. 这个时候我们就可以来定义插槽slot:

    • 插槽的使用过程其实是抽取共性、预留不同;
    • 我们会将共同的元素、内容依然在组件内进行封装;
    • 同时会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素;
  2. 如何使用slot呢?

    • Vue中将 元素作为承载分发内容的出口;
    • 在封装组件中,使用特殊的元素就可以为封装组件开启一个插槽
    • 该插槽插入什么内容取决于父组件如何使用;
  • App.vue

    <template>
      <div>
        <MySlotCpn>
          <button>我是插槽button</button>
        </MySlotCpn>
        <MySlotCpn>
          <MyButton></MyButton>
        </MySlotCpn>
      </div>
    </template><script>
      import MySlotCpn from './MySlotCpn.vue';
      import MyButton from './MyButton.vue';
    ​
      export default {
        components: {
          MySlotCpn,
          MyButton
        }
      }
    </script><style scoped></style>
    
  • MySlotCpn.vue

    <template>
      <div>
        <h2>组件开始</h2>
        <slot>
          <i>我是默认的i元素</i>
        </slot>
        <slot>
          <i>我是默认的i元素</i>
        </slot>
        <slot>
          <i>我是默认的i元素</i>
        </slot>
        <h2>组件结束</h2>
      </div>
    </template><script>
      export default {
        
      }
    </script><style scoped></style>
    
  • MyButton.vue

    <template>
      <div>
        <button>myself button</button>
      </div>
    </template><script>
      export default {
        
      }
    </script><style scoped></style>
    

3.2具名插槽

  • 事实上,我们希望达到的效果是插槽对应的显示,这个时候我们就可以使用 具名插槽

    • 具名插槽顾名思义就是给插槽起一个名字, 元素有一个特殊的 attribute:name;
    • 一个不带 name 的slot,会带有隐含的名字 default;
  • App.vue

    <template>
    <div>
        <NavBar :name="name">
          <template #left>
            <button>左边的按钮</button>
          </template>
          <template #center>
            <h2>我是标题</h2>
          </template>
          <template #right>
            <i>右边的i元素</i>
          </template>
            <!-- 下面这个name是data中的name -->
          <template #[name]>
            <i>why内容</i>
          </template>
        </NavBar>
      </div>
    </template>
    <script>
      import NavBar from './NavBar.vue';
    ​
      export default {
        components: {
          NavBar
        },
        data() {
          return {
            name: "tim"
          }
        }
      }
    </script><style scoped></style>
    
  • NavBar.vue

    <template>
      <div class="nav-bar">
        <!-- <slot name="default"></slot> -->
        <div class="left">
          <slot name="left"></slot>
        </div>
        <div class="center">
          <slot name="center"></slot>
        </div>
        <div class="right">
          <slot name="right"></slot>
        </div>
        <div class="addition">
          <slot :name="name"></slot>
        </div>
      </div>
    </template><script>
      export default {
        props: {
          name: String
        }
        // data() {
        //   return {
        //     name: "tim"
        //   }
        // }
      }
    </script><style scoped>
      .nav-bar {
        display: flex;
      }
    ​
      .left, .right, .center {
        height: 44px;
      }
    ​
      .left, .right, .addition {
        width: 80px;
        background-color: red;
      }
    ​
      .center {
        flex: 1;
        background-color: blue;
      }
    </style>
    

3.3作用域插槽

  • 在Vue中有渲染作用域的概念:

    • 父级模板里的所有内容都是在父级作用域中编译的;
    • 子模板里的所有内容都是在子作用域中编译的;
  • 如何理解这句话呢?我们来看一个案例:

    • 在我们的案例中ChildCpn自然是可以让问自己作用域中的title内容的;
    • 但是在App中,是访问不了ChildCpn中的内容的,因为它们是跨作用域的访问
  • App.vue

    <template>
      <div>
        <!-- 编译作用域 -->
        <!-- <child-cpn>
          <button>{{title}}</button>
        </child-cpn> -->
    ​
        <ShowNames :names="names">
          <template v-slot="tim">
            <button>{{tim.item}}-{{tim.index}}</button>
          </template>
        </ShowNames>
    ​
        <ShowNames :names="names" v-slot="coderwhy">
          <button>{{coderwhy.item}}-{{coderwhy.index}}</button>
        </ShowNames>
    ​
        <!-- 注意: 如果还有其他的具名插槽, 那么默认插槽也必须使用template来编写 -->
        <ShowNames :names="names">
          <template v-slot="tim">
            <button>{{coderwhy.item}}-{{coderwhy.index}}</button>
          </template>
    ​
          <template v-slot:tim>
            <h2>我是why的插入内容</h2>
          </template>
        </ShowNames>
    ​
        <ShowNames :names="names">
          <template v-slot="slotProps">
            <strong>{{slotProps.item}}-{{slotProps.index}}</strong>
          </template>
        </ShowNames>
      </div>
    </template><script>
      import ChildCpn from './ChildCpn.vue';
      import ShowNames from './ShowNames.vue';
    ​
      export default {
        components: {
          ChildCpn,
          ShowNames
        },
        data() {
          return {
            names: ["why", "kobe", "james", "curry"]
          }
        }
      }
    </script><style scoped></style>
    
  • ChildCpn.vue

    <template>
      <div>
        <slot></slot>
      </div>
    </template><script>
      export default {
        data() {
          return {
            title: "我是title"
          }
        }
      }
    </script><style scoped></style>
    
  • ShowNames.vue

    <template>
      <div>
        <template v-for="(item, index) in names" :key="item">
          <slot :item="item" :index="index"></slot>
    ​
          <slot name="tim"></slot>
        </template>
      </div>
    </template><script>
      export default {
        props: {
          names: {
            type: Array,
            default: () => []
          }
        }
      }
    </script><style scoped></style>