序言:slot是创建组件复用的强大工具,尽管不是很容易理解,我们来看一看slot是什么以及如何在应用中使用它。

1、什么是slot?

slot是vue组件的一种机制,允许你以严格父子关系以外的方式组合组件。slot为你提供了将内容放置在新位置或使组件更加通用。理解它们最好的办法就是通过实践,我们以一个最简单的例子开始:

<template>
  <div>
    <component-b>hello world</component-b>
  </div>
</template>

<script>
  import vue from 'vue'

  const ComponentB = vue.component('component-b', {
    template:`
      <div><slot></slot></div>
    `
  });
  
  export default {
    data() {
      return {

      }
    },
    components: {
      ComponentB
    }
  }
</script>

运行结果;

图片描述

我们可以看到<component-b>标签里面的内容将会替换slot标签,这就是所谓的内容分发,slot标签起到的就是这个作用。

2、后备内容

后备内容是指当你没有指定分发内容时,可以给slot指定一个内容。

<template>
  <div>
    <component-b>指定的分发内容</component-b>
    <component-b></component-b>
  </div>
</template>

<script>
  import vue from 'vue'

  const ComponentB = vue.component('component-b', {
    template:`
      <div><slot>我是一个默认值</slot></div>
    `
  });
  
  export default {
    data() {
      return {

      }
    },
    components: {
      ComponentB
    }
  }
</script>

运行结果:

图片描述

3、多个具名插槽

您可以向组件添加多个插槽,但如果这样做,则除了其中一个插槽之外的所有插槽都需要具有名称。如果没有名称,则为默认插槽。以下是创建多个插槽的方法:

<template>
  <div>
    <component-b>
      <template v-slot:top>我是顶部</template>
      <p>我是主体一</p>
      <p>我是主题二</p>
      <template v-slot:bottom>我是底部</template>
    </component-b>
  </div>
</template>

<script>
  import vue from 'vue'

  const ComponentB = vue.component('component-b', {
    template:`
      <div>
        <slot name="top"></slot>
        <slot></slot>
        <slot name="bottom"></slot>
      </div>
    `
  });
  
  export default {
    data() {
      return {

      }
    },
    components: {
      ComponentB
    }
  }
</script>

运行结果:

图片描述

4、插槽作用域

举个例子,比如我写了一个可以实现条纹相间的列表组件,发布后,使用者可以自定义每一行的内容或样式(普通的slot就可以完成这个工作)。而作用域插槽的关键之处就在于,父组件能接收来自子组件的slot传递过来的参数,具体看案例和注释。

<template>
  <div>
    <component-b :items="user" oddBackgroudColor="#D3DCE6" evenBackgroudColor="#E5E9F2">
      <template v-slot:list="props">
        /*通过props对象接收子组件插槽传过来的参数*/
        <span>{{user[props.$index].id}}</span>
        <span>{{user[props.$index].name}}</span>
        <span>{{user[props.$index].age}}</span>
      </template>
    </component-b>
  </div>
</template>

<script>
  import vue from 'vue'

  const ComponentB = vue.component('component-b', {
    data() {
      return {

      }
    },
    template:`
      <ul>
        <li v-for="(item, index) in items" style="line-height:2.2;list-style-type:none" :style="index % 2 === 0?'background-color:' + oddBackgroudColor:'background-color:' + evenBackgroudColor">
          /*slot的$index可以传递到父组件中*/
          <slot name="list" :$index="index"></slot>
        </li>
      </ul>
    `,
    props:{
      items:Array,
      oddBackgroudColor:String,
      evenBackgroudColor:String
    }
  });
  
  export default {
    data() {
      return {
        "user":[
          {id: 1, name: '张三', age: 20},
          {id: 2, name: '李四', age: 22},
          {id: 3, name: '王五', age: 27},
          {id: 4, name: '张龙', age: 27},
          {id: 5, name: '赵虎', age: 27}
        ]
      }
    },
    components: {
      ComponentB
    }
  }
</script>

运行结果:
图片描述

5、slot的作用

5.1、组件复用

通常情况下,独立出来的组件DOM结构一般都是固定的,通过props选项接收传过来的参数,进而展示在页面上。而slot使得组件更加通用化,slot允许在组件外部自定义组件局部的DOM结构,组件更加多样化,适应更多场景。以下列举一个简单例子说明:

<template>
  <div>
    <Alert type="success">
      <strong>Success!</strong> Looks good to me!
    </Alert>
    <Alert type="error">
      <strong>Error!</strong> Oooops...
    </Alert>
    <Alert type="warnning">
      <strong>Warning!</strong> Something not good.
    </Alert>
  </div>
</template>

<script>
import Alert from './Alert'

export default {
  data() {
    return {
    }
  },
  components: {
    Alert
  }
}
</script>
//Alert组件
<template id="alert-template">
  <div :class="alertClasses" v-show="show">
    <slot><strong>Default!</strong>hello world</slot>
    <span class="Alert__close" @click="show = false">X</span>
  </div>
</template>

<script>
import vue from 'vue'
export default vue.component('alert', {
  data() {
    return {
      "show": true
    }
  },
  template: '#alert-template',
  props: ['type'],
  computed: {
    alertClasses() {
      return {
        "Alert-Success": this.type === "success",
        "Alert-Error": this.type === "error",
        "Alert-Warnning": this.type === "warnning"
      }
    }
  }
})
</script>
<style scoped>
.Alert__close {
  font-weight: bold;
  cursor: pointer;
}
.Alert-Success {
  color: green;
}
.Alert-Warnning {
  color: #aa0;
}
.Alert-Error {
  color: red;
}
</style>

运行结果:
图片描述

5.2、功能复用

Vue组件不仅仅是HTML和CSS。它们是用JavaScript构建的,所以它们也是关于功能的。插槽可用于创建一次功能并在多个位置使用它。

//MyModal.vue
<template>
  <div class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <slot name="header"></slot>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">×</span>
          </button>
        </div>
        <div class="modal-body">
          <slot name="body"></slot>
        </div>
        <div class="modal-footer">
          <!--
            using `v-bind` shorthand to pass the `closeModal` method
            to the component that will be in this slot
          -->
          <slot name="footer" :closeModal="closeModal"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {

    }
  },
  methods: {
    closeModal() {
      // Do what needs to be done to close the modal... and maybe remove it from the DOM
    }
  }
}
</script>
<style scoped>
  @import url('./css/bootstrap.css');
</style>
<template>
  <my-modal>
    <template #header><!-- using the shorthand for `v-slot` -->
      <h5>Awesome Interruption!</h5>
    </template>
    <template #body>
      <p>We interrupt your use of our application to
      let you know that this application is awesome 
      and you should continue using it every day for 
      the rest of your life!</p>
    </template>
    <!-- pull in `closeModal` and use it in a button’s click handler -->
    <template #footer="{closeModal}">
      <button @click="closeModal">
        Take me back to the app so I can be awesome
      </button>
    </template>
  </my-modal>
</template>

<script>
import MyModal from './MyModal'
export default {
  data() {
    return {

    }
  },
  components: {
    MyModal
  }
}
</script>

现在,当您使用此组件时,可以向页脚添加一个可以关闭模​modal的按钮。通常,在Bootstrap模式的情况下,您可以只添加data-dismiss="modal"一个按钮,但我们希望隐藏Bootstrap特定的东西,远离将插入此modal组件的组件。所以我们传递给他们一个他们可以调用的函数。

5.3、无渲染组件

无渲染组件是一个不需要渲染任何自己的HTML的组件。相反,它只管理状态和行为。它会暴露一个单独的作用域,让父组件或消费者完全控制应该渲染的内容。

无渲染组件可以在没有任何额外的元素情况之下精确的渲染你所传递的内容。

这么做有什么用呢?
最大的作用就是:表现和行为分离。由于无渲染组件只处理状态和行为,所以它们不会对设计或布局强加任何的决策。这意味着,如果你能够找到一种方法,将所有的行为从UI组件(比如下面的标签输入控件)中移出,将其转换成一个无组件的组件,那么你就可以重用无渲染组件来实现任何布局效果的标签输入控件。

我们看看下面标签输入控件实例:

子组件:

// 无渲染组件结构
<script>
  import vue from 'vue'
  export default vue.component('renderless-component-example', {
    props: ["value", "newTag"],
    methods: {
      removeTag(tag) {
        this.$emit("input", this.value.filter(item => item !== tag));
      },
      addTag() {
        if (this.newTag.trim().length === 0 || this.value.includes(this.newTag.trim())) {
          //判断输入新标签是否为空或者是否已经存在
          alert("标签不能为空,不能重复!");
        } else {
          this.$emit("input", [...this.value, this.newTag.trim()]);
          //添加标签后清空输入框
          this.$emit("update:newTag", "");
        }
      }
    },
    render() {
      return this.$scopedSlots.default({
        "tags": this.value,
        "removeTag": this.removeTag,
        "addTag": this.addTag,
      });
    }
  });
</script>

父组件:


<template>
  <RenderlessComponentExample v-model="tags" :newTag.sync="newTag">
    <template v-slot:default="{tags, removeTag, addTag}">
      <div class="tags-input"> 
        <span class="tags-input-tag" v-for="(tag, index) in tags" v-bind:key="index"> 
          <span>{{tag}}</span> 
          <span class="tags-input-remove" @click="removeTag(tag)">x</span> 
        </span> 
        <input class="tags-input-text" @keydown.enter.prevent="addTag" v-model="newTag" placeholder="Add tag..."> 
      </div>
    </template>
  </RenderlessComponentExample>
</template>

<script>
 import RenderlessComponentExample from './NoRender'

 export default {
   data() {
     return {
       "tags": [],
       "newTag": ""
     }
   },
   components: {
     RenderlessComponentExample
   }
 }
</script>

<style scoped>
  .tags-input{
    display: inline-block;
    border: 1px solid gainsboro;
    padding: 3px;
    background-color: #FDFDFD;
    border-radius: 3px;
  }
  .tags-input-tag{
    display: inline-block;
    border-radius: 5px;
    background-color: #BBDDF9;
    padding: 0 6px;
    font-size: 14px;
    margin-right: 5px;
    line-height: 24px;
    cursor: pointer;
  }
  .tags-input-remove{
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background-color: #fff;
    margin-left: 5px;
    color: #4289C7;
    font-size: 13px;
    vertical-align: middle;
    line-height: 1;
  }
  .tags-input-remove:hover{
    color: rgb(52, 144, 224);
    background-color: azure;
  }
  .tags-input-text{
    border: 0;
    outline: 0;
  }
</style>

运行结果:
图片描述

6、总结

将一个组件分解成一个表示组件(Presentational Component)和一个无渲染组件(Renderless Component)是一种非常有用的模式,可以使组件重用变得更加容易。虽然如此,但也并不代表总是值得的。

如果你满足下面这些条件,建议你使用无渲染组件这种方式:

  • 你正在构建一个库,并且希望使用者能够更轻易地自定义组件的外观(样式效果)
  • 你项目中有多个组件,其行为或功能非常相似,但布局不同

如果一个组件看起来和它使用的地方是一样的或者只需要一个组件,那么就没有必要这样做,因为将所有东西都保存在一个组件中,会让你的事情变得更简单得多。

参考链接:
https://www.w3cplus.com/vue/r...
https://www.smashingmagazine....


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。