组件划分和插槽

560 阅读10分钟

1.组件类型划分

组件:是对数据和方法的简单封装,可以扩展 HTML 元素,封装可重用的代码。

如果把页面编码过程比作堆积木,组件就是一块积木,如下页面就是page,head、main、foot就是组成页面的组件

<page>
    <head></head>
    <main></main>
    <fooot></fooot>
</page>

组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:

这里以react举例,在react中组件大致分成两种

  • UI组件(简单组件)
  • 容器组件(有状态组件)

1.1 UI组件(无状态组件)

特点:

  1. 只负责UI的呈现,不带有任何业务逻辑
  2. 没有状态
  3. 所有数据都由参数 this.props提供
  4. 不使用任何Redux的API
// UI组件
 const Title = value => <h1>{value}</h1>;

function ListItems(props) {
  return <li>{props.value}</li>
}

// UI组件嵌套
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}


ReactDOM.render(
  <App />,
  document.getElementById('root')

React 非常灵活,但它也有一个严格的规则:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。 当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容

纯函数 如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。

1.2 容器组件

特点:

  1. 负责管理数据和业务逻辑,不负责UI的呈现
  2. 带有内部的状态
  3. 使用Redux的API
class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }

  tick() {
    this.setState(state => ({
      seconds: state.seconds + 1
    }));
  }

  componentDidMount() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <div>
        Seconds: {this.state.seconds}
      </div>
    );
  }
}

ReactDOM.render(
  <Timer />,
  document.getElementById('timer-example')
);
  • UI组件负责UI的呈现,容器组件负责数据和逻辑

  • 如果一个组件既有UI又有业务逻辑,分成两部分,外面是容器组件,里面是UI组件容器组件负责与外部通信,将数据传给UI组件UI组件负责渲染出视图

<Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
        <App />
    </PersistGate>
</Provider>

除了使用外部数据(通过 this.props 访问)以外,组件还可以维护其内部的状态数据(通过 this.state 访问)。当组件的状态数据改变时,组件会再次调用 render() 方法重新渲染对应的标记。

1.3 小结

在页面中中引入容器组件 如可配置 先判断是否展示, 通过props为容器组件提供必要的数据或者是方法,组件内部逻辑在容器组件中进行处理,比如需要请求请求接口、数据处理,UI组件接收容器组件传递的props进行渲染

// 父组件
 <skinWraper v-if="bgSkin" :skin="skinProps"></skinWraper>
 
 // skinWraper子组件
 <div>
    <comp-first></comp-first>
    <comp-second></comp-second>
    <comp-third></comp-third>
 </div>
 
 methods: {
    // dosomething api handleData ctrl comp show or hidden
  }

2.如何应对变化无常的业务需求

2.1 使用组件插槽前

程序员小王接到一个需求,当网络错误/接口请求失败的时候展示一个网络错误的页面,页面UI设计图如下

小王心想,这多简单啊 写一个组件 放一个刷新按钮 不就ok了

// 父组件
<netWorkError :refresh="refreshPage"></netWorkError>

// netWorkError组件
<div>
  <img src="wrongPage.png" alt="网络异常">
  <p>系统开小差了,请尝试刷新页面~</p>
  <van-button round @click="refresh">刷新</van-button>
</div>

小王兴冲冲的写完交差了,正当他准备回去听个歌庆祝一下的时候,被产品叫住了,产品说我们这边还有另外一种异常情况,跟之前的那个样子差不多,你也做一下吧,小王心想 长的差不多 拿之前改一改不就好了,就答应下来了,UI图如下

// 父组件
<netWorkError :refresh="refreshPage" :from='a'></netWorkError>

// netWorkError组件
<div>
  <img src="wrongPage.png" alt="网络异常">
  <p>{{mag}}</p>
  <van-button round @click="refresh">{{buttonName}}</van-button>
</div>
data () {
    return {
        mag: '系统开小差了,请尝试刷新页面~',
        buttonName: '刷新'
    }
}
 methods: {
   if(this.from === a) {
       this.msg = '系统开小差了,看看其他商品吧'
       this.buttonName = '去店铺看看'
   }
  }

嗯,,是的 小王又兴冲冲的写完交差了,正当他准备回去听个歌庆祝一下的时候,被产品叫住了,历史总是惊人的相似

产品说我们这边还有另外一种异常情况,跟之前的那个样子差不多,你也做一下吧,小王心想 。。。

小王又想到了之前用的from根据不同的来源显示不同的内容,可是如果下次再提一个类似的需求怎么办,就这样一直增加类型么,组件接收的参数越来越多了,到那时组件就难以维护了

小王百度了一波,发现了一片新大陆 slot-- 组件插槽 于是他准备学习一下

2.2 使用组件插槽后

// 父组件
<netWorkError :refresh="refreshPage">
  <template slot="text">
    系统开小差了,查询失败~
  </template>
  <template slot="button-name">
    <span></span> 
  </template>
</netWorkError>

//  netWorkError组件
<div>
  <img src="wrongPage.png" alt="网络异常">
  <p>
    <slot name="tip-text">系统开小差了,请尝试刷新页面~</slot>
  </p>
  <van-button round @click="refre">
    <slot name="button-name">刷新</slot>
  </van-button>
</div>

终极版错误情况汇总

3 插槽

插槽 <slot> 元素作为组件模板之中的内容分发插槽,传入内容后 元素自身将被替换。

举例:上面所说的网络错误页面,大致结构是相同的,变化的主要是提示信息文案和按钮文字,依靠子组件不能实现,通过设置专门的方法比较麻烦,此时可以通过使用插槽来很好的解决上述问题

在 2.6.0 版本中,Vue 为具名插槽和作用域插槽引入了一个新的统一的语法 (即<v-slot> 指令)。它取代了 slotslot-scope 这两个目前已被废弃、尚未移除,仍在文档中的特性。

插槽分为三类: 匿名插槽、具名插槽以及作用域插槽

3.1 匿名插槽

匿名插槽又称之为默认插槽,匿名插槽名字为default

 // 子组件
  <template>
    <div>
      <p>
        <!--匿名插槽  当父组件不添加任何插槽内容时,显示默认内容-->
        <slot>系统开小差了,请尝试刷新页面~</slot>
      </p>
    </div>
  </template>
// 父组件
<netWorkError :refresh="refreshPage">
    系统开小差了,查询失败~
</netWorkError>

渲染效果

<!--甚至是其他组件-->
<netWorkError :refresh="refreshPage">
    <child></child>
</netWorkError>

如果 组件内没有包含一个 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

3.2 具名插槽

为 元素添加 name 属性,用来区分不同的插槽,当不填写 name 时,默认为 default 匿名插槽。

//  子组件
<template>
    <div>
      <img src="wrongPage.png" alt="网络异常">
      <p>
        <slot name="tip-text">系统开小差了,请尝试刷新页面~</slot>
      </p>
      <van-button round @click="refresh">
        <slot name="button-name">刷新</slot>
      </van-button>
    </div>
</template>
// 父组件
<netWorkError :refresh="refreshPage">
  <template v-slot:"text">
    系统开小差了,看看其他商品~
  </template>
  <!--具名插槽的缩写#-->
  <template #button-name>
    <span>去店铺</span> 
  </template>
  <!--重复定义 只会加载最后一个定义的插槽内容-->
  <template #button-name>
    <span>去店铺看看</span> 
  </template>
</netWorkError>

渲染效果

现在 <template>元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容。

如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:

//  子组件
<template>
    <div>
      <img src="wrongPage.png" alt="网络异常">
      <p>
        <slot name="tip-text">系统开小差了,请尝试刷新页面~</slot>
      </p>
      <slot>我是一个没有感情的coding机器</slot>
      <van-button round @click="refresh">
        <slot name="button-name">刷新</slot>
      </van-button>
    </div>
</template>
// 父组件
<netWorkError :refresh="refreshPage">
  <template v-slot:"text">
    系统开小差了,看看其他商品~
  </template>
  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>
  <!--具名插槽的缩写#-->
  <template #button-name>
    <span>去店铺</span> 
  </template>
</netWorkError>

3.3 插槽作用域

在父组件中访问子组件内部的一些可用数据

// 子组件
<div>
    <slot>{{user.name}}</slot>
</div>

data () {
    return {
        user: {
            name: '小王',
            job: 'coding'
       }
    }
}

// 父组件这样写是不行的 因为当前代码的作用域环境是父组件,所以不能访问child内部的数据
 <child>
      {{ user.name }}
</child>

// 下面改写一下子组件
<div>
    <slot :user='user'>
        {{user.name}}
    </slot>
</div>

// 父组件
 <child>
    <!--slotProp 代表的是slot props名字 也可以换成其他的名字-->
     <template slot-scope="slotProp">
        {{ slotProp.user.name }}
    </template>
</child>
// slot-scope 在vue 2.6中已废弃,我们用v-slot写一下
<!--匿名插槽-->
 <child>
     <template v-solt:default="slotProp">
        {{ slotProp.user.name }}
    </template>
</child>
<!--具名插槽-->
 <child>
     <template v-solt:text="slotProp">
        {{ slotProp.user.name }}
    </template>
</child>

3.4 解构插槽props

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里,这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。可以使用 ES2015 解构来传入具体的插槽 prop,如下:

function (slotProps) {
  // 插槽内容
}
<!--slot是一个参数集合 可以通过结构获取-->
<child v-slot="{ user }">
  {{ user.firstName }}
</child>
<!--也可以换个别名-->
<child v-slot="{ user: person }">
  {{ person.firstName }}
</child>
<!--甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形-->
<child v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</child>

3.5 动态插槽名称

2.6.0 + 新增 动态指令参数 也适用于 v-slot ,允许我们定义动态插槽名称:

<child>
  <template v-slot:[SlotName]>
    ...
  </template>
</child>

3.6 其他

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。

例如,我们要实现一个 组件,它是一个列表且包含布局和过滤逻辑:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    <!--
    我们为每个 todo 准备了一个插槽,
    将 `todo` 对象作为一个插槽的 prop 传入。
    -->
    <slot name="todo" v-bind:todo="todo">
      <!-- 后备内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

现在当我们使用<todo-list> 组件的时候,我们可以选择为todo 定义一个不一样的<template> 作为替代方案,并且可以从子组件获取数据:

<todo-list :todos="todos">
  <template v-slot:todo="{ todo }">
    <span v-if="todo.isComplete">✓</span>
    {{ todo.text }}
  </template>
</todo-list>