关于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是咱创作的能源哦^_^

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理