乐趣区

关于element-ui:elementui源码学习之仿写一个eltabs

本篇文章记录一下,仿写一个 el-tabs 组件,有助于大家更好了解,饿了么 ui 的轮子具体工作细节。本文是 elementui 源码学习仿写系列的又一篇文章,后续闲暇了会不断更新并仿写其余组件。源码在 github 上,大家能够拉下来,npm start 运行跑起来,联合正文有助于更好的了解。github 仓库地址如下:https://github.com/shuirongsh…

知识点温习

为了更好的浏览后续代码,咱们须要再来温习一下常识

vue 中的 render 函数书写 jsx 语法

vue 中咱们写组件页面,经常是构造、逻辑、款式拆散,如:

<template>
    <!-- 这里写构造 -->
</template>
<script>
    // 这里写逻辑
</script>
<style>
    // 这里写款式
</style>

如果大家相熟 react 语法,就会发现,react 中是把构造和逻辑书写在一块的(jsx)语法,其实 vue 中也是能够应用 jsx 语法的,不过要在 render 函数中去写。比方咱们要应用 render 函数写一个红色字体的 H3 标签、背景色黄绿色。代码能够如下书写:

<script>
export default {data() {return { name: "孙悟空"}; },
  render(h) {
    /**
     * 第一步,筹备一个 dom,dom 中应用单大括号作为占位符,单大括号中能够应用变量
     *        vue 中个别是双大括号中应用变量,区别不大。再一个就是中间应用小括号
     *        包裹(不便换行书写)* */
    let dom = (
      <div class="box">
        <h3 class="h3Class">{this.name}</h3>
      </div>
    );
    // 第二步,返回。如此 render 函数就会主动翻译渲染之
    return dom;
  },
};
</script>
<style>
.box {background: yellowgreen;}
.h3Class {color: red;}
</style>

效果图:

上述代码咱们只须要记住,jsx 语法中应用单大括号去示意变量应用。

再一个就是 render 函数中,如何给子组件传参数呢?也是应用 单大括号搭配点点点...,因为单大括号才示意变量。如下代码:

render(h) {
    const sendParamsData = { // 筹备参数
      props: {
        name: this.name, 
        age: this.age, 
        home: this.home, 
      },
    };
    return (
      <div class="kkk">
        {/* 传递参数 */}
        <child {...sendParamsData}></child>
      </div>
    );
  },

饿了么官网 el-tabs 组件就是应用了 jsx 语法进行编写的(因为 jsx 语法更加灵便),故:一般业务需要 template 够用了。灵便简单需要,思考应用 jsx

this.$slots.default 拿到组件标签内容中非具名插槽的局部

this.$slots.default这个 api,大家平时应用的可能不是特地多。这个 api 能够顾名思义。$slots,插槽的意思 (复数,可能有多个),default 默认的意思。那就是默认插槽的意思。即this.$slots.default 这个变量保留了所有不是命名插槽且不是作用域插槽的所有一般插槽内容。是个数组(如果有的话)。咱们来看上面的代码简略应用便明了

// 父组件
<template>
    <child>
      <h3> 孙悟空 </h3>
      <h3> 猪八戒 </h3>
      <h3> 沙和尚 </h3>
    </child>
</template>
<script>
import child from "./child.vue";
export default {components: { child}};
</script>

// 子组件
<template>
  <div> 我是子组件 </div>
</template>
<script>
export default {mounted() {console.log(this)},
};
</script>

打印的 this 组件实例图如下:

当然,这里咱们写的是一般标签 h3,这里也能够写组件标签,那么 this.$slots.default 存储的就是组件标签了,能拿到组件所有货色,就能够去进一步,加工存储,传递应用。联合官网 el-tabs 组件的应用,代码如下:

<el-tabs v-model="activeName" @tab-click="handleClick"> 
    <el-tab-pane label="用户治理" name="first"> 用户治理 </el-tab-pane> 
    <el-tab-pane label="配置管理" name="second"> 配置管理 </el-tab-pane> 
    <el-tab-pane label="角色治理" name="third"> 角色治理 </el-tab-pane> 
</el-tabs>

咱们发现,如同很类似。是的,官网就是通过 this.$slots.default 这个数组,拿到每一个 el-tab-pane 组件上的 labelname 以及其余的信息,而后传递到 tab-nav 组件上,于是乎,tab-nav组件,就会显示出一个又一个的选项卡信息了(供用户点击)

父组件 v -model 传参,子组件 props 中 key 为 ’value’ 接管,’input’ 事件更改

咱们看官网应用案例中,也发现 v-model 绑定在组件 el-tabs 组件上。平时咱们应用 v-model 个别都是绑定在输入框、下拉框、切换之类的表单控件组件上。只须要写一个 v-model 就能够了,不必做别的操作。不过 v-model 若绑定在一般自定义组件上,用于父子组件传递参数(双向数据绑定),就须要咱们多写点代码了。咱们来看一下案例:

// 父组件
<template>
  <child v-model="age"></child>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
  data() {
    return {age: 500,};
  },
};
</script>

// 子组件
<template>
  <div>
    <h3> 孙悟空年龄是:{{ageValue}}</h3>
    <button @click="add"> 加一 </button>
  </div>
</template>

<script>
export default {
  props: {value: null, // 申明接管},
  data() {
    return {ageValue: this.value, // 取到值用于显示};
  },
  methods: {add() {this.$emit("input", this.ageValue); // 父组件更新值的时候,子组件也要更新
      this.ageValue = this.ageValue + 1; // 子组件手动更新(另,应用 watch 监听 value 值变动自动更新也行)},
  },
};
</script>

效果图如下:

起因:

// 父组件 v -model=age 相当于
<child v-model="age"></child>

<child :value="age" @input="age = $event"></child>

所以,子组件须要在 props 中应用 value 接管,同时应用 this.$emit("input", xxx) 触发

开始仿写

首先要搭建一个 tabs 构造

咱们晓得 tabs 就是选项卡切换的意思,整体能够分为三局部:选项卡局部 内容区局部 整个选项卡盒子局部,所以这里咱们新建三个文件,去实现这个 tabs 组件。

  1. tabs.vue文件(整个选项卡盒子局部),用来作为整个选项卡切换的大容器的文件,在这个文件中去解决 选项卡局部 逻辑,以及 内容区局部 逻辑
  2. tabNav.vue文件(选项卡局部)
  3. tabContent.vue文件(内容区局部)

图示如下:

当然这里笔者曾经封装好了,先来看看如何应用,以及效果图,而后再看看封装的代码呗

应用封装好的组件

<template>
  <div>
    <my-tabs v-model="activeName" @tabClick="tabClick">
      <my-tab-content label="孙悟空" name="sunwukong"
        > 孙悟空内容 </my-tab-content
      >
      <my-tab-content label="猪八戒" name="zhubajie"> 猪八戒内容 </my-tab-content>
      <my-tab-content label="沙和尚" name="shaheshang"
        > 沙和尚内容 </my-tab-content
      >
      <my-tab-content label="唐僧" name="tangseng"> 唐僧内容 </my-tab-content>
      <my-tab-content label="白龙马" name="bailongma"
        > 白龙马内容 </my-tab-content
      >
    </my-tabs>
    <br />
    <hr />
    <my-tabs v-model="activeName2" :before-leave="beforeLeave">
      <my-tab-content label="武松" name="wusong"> 武松内容 </my-tab-content>
      <my-tab-content label="宋江" name="songjiang"> 松江内容 </my-tab-content>
      <my-tab-content label="林冲" name="linchong"> 林冲内容 </my-tab-content>
      <my-tab-content disabled label="吴用" name="wuyong"
        > 吴用内容 </my-tab-content
      >
    </my-tabs>
  </div>
</template>

<script>
export default {data() {
    return {
      activeName: "sunwukong",
      activeName2: "wusong",
    };
  },
  methods: {tabClick(tabItem) {console.log("外层点击", tabItem);
    },
    beforeLeave(newTabName, oldTabName) {console.log("外层", newTabName, oldTabName);
      /**
       * 形式一:*    return true; // 示意容许切换 tab
       *    return false; // 示意不容许切换 tab
       * */
      /**
       * 形式二:*    应用 Promise 解决异步相干切换 tab 操作,比方问询操作
       * */
      var p = new Promise((resolve, reject) => {this.$confirm(` 确认由 ${oldTabName}切换到 ${newTabName}`, "tab change", {
          confirmButtonText: "确认切换",
          cancelButtonText: "勾销",
          type: "warning",
        })
          .then(() => {resolve(true); // 容许放行通过切换 tab
          })
          .catch((err) => {reject(false); // 不容许切换 tab
          });
      }).catch((reason) => {// 留神此处须用 Promise 的 catch 办法捕捉谬误,否则控制台报错 Uncaught (in promise)
        console.log("reason", reason);
      });
      // 最初返回 Promise 的后果
      return p;
    },
  },
};
</script>

效果图

tabs.vue 组件(相当于数据中转站)

<script>
import tabNav from "./tabNav.vue"; // 引入 tab 导航页组件
export default {
  name: "myTabs",
  components: {tabNav}, // 注册之
  props: {
    // 父组件用 v -model 传参,子组件须用 value 接参,方可接到 v -model="activeName" 绑定的 activeName 的值
    value: null, // 接管到的值即为以后高亮的是哪一项
    // 传递一个函数,作为 tab 切换的钩子函数
    beforeLeave: {
      // 切换标签之前的钩子,若返回 false 或者返回 Promise 且被 reject,则阻止切换
      type: Function,
      default: () => {return true; // 默认为 true,始终容许切换 tab},
    },
  },
  data() {
    return {tabItemArr: [], // 用于传递给 tabNav 组件信息数据的数组
      activeName: this.value, // 高亮的是哪个 tab 标签页选项卡
    };
  },
  mounted() {
    /**
     * 计算收集 tab 页内容信息,将须要用到的信息存在 tabItemArr 数组中
     * 并传递给 tabNav 组件,tabNav 组件依据 tabItemArr 信息去 v -for 渲染有哪些
     * */
    this.calcTabItemInstances();},
  methods: {calcTabItemInstances() {
      // 重点办法
      // 获取应用的中央的 my-tab 标签两头的内容
      if (this.$slots.default) {
        // 收集 my-tab 标签两头的插槽内容数组
        let slotTabItemArr = this.$slots.default; // console.log("slotTabItemArr", slotTabItemArr);
        // 而后把这些数据交给 tab-nav 动静渲染
        this.tabItemArr = slotTabItemArr.map((item) => {return item.componentInstance; // 只保留 componentInstance 组件实例即可,能够了解为组件的 this});
        // consoloe.log('this.tabItemArr',this.tabItemArr)
      } else {this.tabItemArr = []; // 没传递就置为空,当然须要标准应用组件,标准传递相干参数
      }
    },
    handleTabClick(tabItem) {this.$emit("tabClick", tabItem); // 告诉父元素点击的是谁,是哪个 tab-nav
      let newTabName = tabItem.name; // 获取传进去的最新的 name 名字
      this.setCurrentName(newTabName); // 执行更新办法
    },
    // 思考到可能有异步操作,所以加上 async await(比方在切换 tab 标签页之前,做一个问询)async setCurrentName(newTabName) {
      let oldTabName = this.activeName; // 要更新了,所以当下的就变成旧的了
      let res = await this.beforeLeave(newTabName, oldTabName);
      if (res) {this.$emit("input", newTabName); // 更新父组件的 v -model 绑定的值
        this.activeName = newTabName; // 本身也更新一下
      }
    },
  },
  render(h) {
    // 筹备参数,以便把参数传递给 tab-nav 组件
    const navData = {
      props: {
        tabItemArr: this.tabItemArr, // 内容区相干信息数组
        activeName: this.activeName, // 以后高亮的是哪一项
        onTabClick: this.handleTabClick, // 点击某一 tab 项的回调
      },
    };
    return (
      <div class="tab-Box">
        <tab-nav {...navData}></tab-nav>
        <div class="my-tab-content-item-box">{this.$slots.default}</div>
      </div>
    );
    /**
     * 留神:<div class="my-tab-content-item-box">{this.$slots.default}</div> 写法,失常会把所有的都渲染进去
     * 所以咱们在 myTabContent 组件中再加一个判断(v-show="isActiveToShowContent"),看看以后高亮的名字是否和组件的名字统一,* 统一才渲染. 这样的话,同一时刻,依据 myTabContent 组件的 name 属性,只会对应渲染一个
     * */
  },
};
</script>

tabNav.vue 组件(接管 tabs.vue 的数据进行 v -for 动静渲染)

<template>
  <div class="my-tab-nav-item-box">
    <div
      :class="['my-tab-nav-item',
        tabItem.name === activeName ? 'highLight' : '',
        tabItem.disabled ? 'isForbiddenItem' : '',
      ]"v-for="(tabItem, index) in tabItemArr":key="index"@click="changeActiveName(tabItem)"
    >
      {{tabItem.label}}
    </div>
  </div>
</template>
<script>
export default {
  name: "myTabNav",
  props: {
    // 源自于内容区的数组数据,十分重要
    tabItemArr: {
      type: Array,
      default: [],},
    // 以后激活的名字
    activeName: {
      type: String,
      default: "",
    },
    // 接管点击选项卡函数,在点击 tab 选项卡的时候,通过此函数传递进来
    onTabClick: {type: Function,},
  },
  methods: {changeActiveName(tabItem) {
      // 本人点本人就不让执行了
      if (tabItem.name === this.activeName) {return;}
      // 如果蕴含禁用项 disabled 属性(即处于禁用状态),也不让执行(搭配.isForbiddenItem 类名)if (tabItem.disabled) {return;}
      this.onTabClick(tabItem);
    },
  },
};
</script>
<style lang="less" scoped>
.my-tab-nav-item-box {
  width: 100%;
  border-bottom: 1px solid #e9e9e9;
  .my-tab-nav-item {
    // 转换成行内盒子,每一项都程度排列
    display: inline-block;
    // 垂直居中
    height: 40px;
    line-height: 40px;
    // 字体款式地位设置一下
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    margin: 0 12px;
    cursor: pointer;
  }
  // 非禁用时鼠标悬浮款式,留神这里 not 的应用
  .my-tab-nav-item:not(.isForbiddenItem):hover {color: #409eff;}
  // 高亮项款式
  .highLight {
    color: #409eff;
    border-bottom: 1px solid #409eff;
  }
  // 禁用项款式
  .isForbiddenItem {
    cursor: not-allowed;
    color: #aaa;
  }
}
</style>

myTabContent.vue 组件(搭配 v -show 条件比照只渲染一个)

<template>
  <div class="my-tab-content-item" v-show="isActiveToShowContent">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "myTabContent",
  props: {
    label: String, // 标签名
    name: String, // 每个下方内容区都有本人的 name 名字
    disabled: {
      // 是否禁用这一项
      type: Boolean,
      default: false, // 默认不禁用
    },
  },
  computed: {
    // 管制依据高亮的 tab 显示对应标签页内容
    isActiveToShowContent() {
      let activeName = this.$parent.value; // 比方以后高亮的是 sunwukong
      let currentName = this.name; // this.name 的值有很多个,有:sunwukong、zhubajie、shaheshang...
      // 谁等于,就显示谁
      return activeName === currentName;
    },
  },
};
</script>

<style>
.my-tab-content-item {padding: 12px;}
</style>

总结

对于 tabs 切换成果,能够本人写,比方应用动静组件形式去写(笔者也写过动静组件的文章链接:https://juejin.cn/post/695769…),或者本人封装一个 tabs 组件。

不过本文只是仿照饿了么官网封装的一个简略的组件(某些中央的写法也可能和官网不太一样,不过思路是相通的)。再一个官网封装组件的时候,须要思考到十分多的状况(可能某些状况很少用到),本文只是仿写并实现罕用的成果。理论开发中,组件的封装的水平,须要依据我的项目的需要状况,进行考量。不可适度封装,也不能不封装。毕竟组件复用确实可能晋升开发效率。

如果本篇文章可能帮忙您更好的了解 el-tabs 的流程原理、数据传递形式,万分感激给咱的 github 仓库 star 一下哦。毕竟elementui 源码学习之仿写 xxx 是一个系列文章,您的 start 是咱创作的能源哦^_^

退出移动版