vue组件---element-ul的table组件、loading组件的仿写

305 阅读8分钟

一、前言

分享一下在学习vue2时封装的最后的一个组件,table组件,在这个这次分享中例外说一下自定义指令以及jsx语法的基本写法。然后以前说过,自己封装的组件如果需要使用use去注册的话需要给这个组件写一个install的方法,这里就不再说了。

二、table组件封装

<template>
  <table class="co-table" cellspacing="0" cellpadding="0" >
    <thead>
      <!-- 渲染表头 -->
      <tr>
        <th
          v-for="col in columns"
          :key="col.label"
          :style="{ textAlign : col.align }"
        >
          {{ col.label }}
        </th>
      </tr>
    </thead>
    <tbody>
      <!-- 看表格存在几行就是看传递过来的数据的data数组的长度 -->
      <tr v-for="(row, index) in data" :key="index">
        <!-- 每一行通过获取到的属性来确定有几列 -->
        <td
          v-for="col in columns"
          :key="col.label"
          :style="{ 'text-align': col.align }"
        >
          <!-- 每一个格子的内容就是遍历data数据的每一个对象[获取的属性值] -->
          {{ row[col.prop] }}
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  // 1.对于每个自己封装的组件,name属性推荐写上
  name: 'co-table',
  // 2.在使用的时候table的数据是用绑定的data属性传递的,按照规则,
  //   需要在table组件中定义一个data的props的属性
  props: {
    data: {
      type: Array,
      require: true,
    },
  },
  computed: {
    // 3.在使用的时候,因为写在table组件里面的内容是用抽象组件进行约束的,
    //   它没有模板,所以不能使用slot插槽将它接住,以前分享过,这些内容会
    //   默认存放在this.$slots.default中,但是它也会将换行符也收集起来,
    //   所以在使用这个数据的时候需要进行筛选,将换行符给过滤掉。
    columns() {
      return this.$slots.default
        .filter(
          // 为了避免过长的引用,这里使用结构将componentOptions拿出来,
          // 如果不结构的话呢这里需要使用item.componentOptions
          ({ componentOptions }) =>
            // &&表示如果&&前面的语句为true才会执行后面的语句
            // componentOptions是判断它是否是换行符节点,
            // componentOptions.tag === 'co-table-column'
            //   是用来确定标签名是否是我需要的
            componentOptions && componentOptions.tag === 'co-table-column'
        )
        // 过滤完毕之后就获取里面的数据并返回,然后通过循环的方式就可以将
        // table表格的表头渲染出来了。
        .map(({ componentOptions }) => componentOptions.propsData);
    },
  },
};
</script>

<style lang="scss">
.co-table {
  th,
  td {
    border-bottom: 1px solid #ebeef5;
  }
}
</style>

三、tableColumn组件的封装

<script>
export default {
  // 1.对于每个自己封装的组件,name属性推荐写上
  name: 'co-table-column',
  // 3.对于之前说过的组件的分类,如果不需要模板,那就使用抽象组件,
  //   将abstract设置为true就可以了
  abstract: true,
  // 2.对于用户传递的数据组件并不知道它的顺序是怎样的,所以这里使用了
  //   tableColumn这个子组件对传递的数据进行约束,只是起到一个约束数据
  //   的作用,所以它不需要模板,其约束的方式通过绑定的属性来完成。
  props: {
    // 日期
    prop: {
      type: String,
    },
    // 内容
    label: {
      type: String,
      required: true
    },
    // 宽度
    width: {
      type: [String, Number],
    },
    // 对齐
    align: {
      type: String,
      default: 'left'
    }
  }
}
</script>

四、自定义指令

说明: 自定义指令也是一个组件,一般会将自定义指令存放在src目录下面的一个directives目录中,在这个目录里面每一个文件夹都是一个指令,当然,在写完一个指令之后,也需要为它写一个install的方法,方便它被use使用。

用途: 可以在vue中使用dom操作的地方

指令的完整格式: v-指令名:参数.修饰符 = 表达式

内容:

export default {
    // 在定义指令的时候,指令的名字不需要加上v,v会默认加上,
    // 只不过自己使用的时候需要v-指令名
    name: 'loading'
    
    // 指令初始化的时候,只会被调用一次,会在指令第一次绑定到元素时调用
    bind(el, binding) {
        // el: 指令所绑定的元素
        // binding: {
             // name: 指令名
             // value: 指令绑定的值
             // oldValue: 指令绑定的前一个值
             // expression:字符串形式的指令表达式
             // arg: 传递给指令的参数
             // modifiers:指令的修饰符对象
        // }: 指令的配置对象
        
        // 每一个声明周期函数都存在el和binding这两个参数(用的最多)
    }
    
    // 指令绑定的元素插入到父元素的时候才会被调用
    inserted() {}
    
    // 指令绑定的值发生变化的时候
    update() {}
    
    // 更新完毕时调用
    componentUpdated() {}
    
    // 解绑的时候调用
    unbind() {}
}

注册: 全局注册使用Vue.directive(指令名,指令配置项)

举例element-ul的loading组件的实现(仿写)

// 书写loading指令主体逻辑的index.js文件

// 引入一个圈圈的svg图片
import loadingSvg from './loading.svg';
// 引入loading指令实现的效果的css样式
import './loading.scss';

// 这个函数用来书写创造节点的流程
function createLoadingMask() {
  // 最外层的蒙版
  const coLoadingMask = document.createElement('div');
  // 添加类名为了好去书写样式
  coLoadingMask.className = 'co-loading-mask';
  // 装图片的盒子
  const coLodingSpinner = document.createElement('div');
  coLodingSpinner.className = 'co-loading-spinner';
  // 创建图片标签
  const coLoadingCircular = document.createElement('img');
  // 给空的图片标签进行赋值
  coLoadingCircular.src = loadingSvg;
  coLoadingCircular.className = 'co-loading-circular';
  // 将创建的节点合并为一个节点
  coLodingSpinner.appendChild(coLoadingCircular);
  coLoadingMask.appendChild(coLodingSpinner);
  // 将合并后的节点返回出来
  return coLoadingMask;
}

// 用变量接住上面函数执行所返回的节点的嵌套结果
const coLoadingMask = createLoadingMask();
const loading = {
  name: 'loading',
  bind(el, binding) {
    // 通过绑定的值来确定是否插入这个生成的节点
    if (binding.value) {
      // 给el节点添加定位,让loading的圈圈覆盖在需要的位置上面,而不是body
      el.style.position = 'relative';
      // 将合并生成的节点插入到指令的el中
      el.appendChild(coLoadingMask);
    }
  },
  // 当绑定的值变化的时候,需要根据这个值来确认是否插入
  update(el, binding) {
    if (binding.value) {
      el.style.position = 'relative';
      el.appendChild(coLoadingMask);
    } else {
      el.removeChild(coLoadingMask);
    }
  },
};

export default {
  install(Vue) {
    Vue.directive(loading.name, loading);
  },
};
// 圈圈的svg图片的loading.svg文件

<svg t="1681267906984" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2668" width="200" height="200">
<path d="M515.698303 969.127499c-97.187972 0-191.279691-31.134554-270.406182-90.479422-96.67193-72.245926-159.45708-178.206619-176.658492-297.928439s13.245087-238.9276 85.663027-335.59953C304.120947 45.239711 588.288258 4.644381 787.99664 154.124643c83.770872 62.78515 143.459768 153.092558 168.2298 254.580884 4.300353 17.373425-6.364522 34.918864-23.737947 39.047203-17.373425 4.128339-34.918864-6.364522-39.047203-23.737947-21.157736-86.867126-72.417941-164.44549-144.147825-218.285906C578.139425 77.750378 334.395431 112.669242 206.244919 283.823282c-62.097094 82.910801-88.243239 185.087183-73.450025 287.607593s68.461616 193.34386 151.372417 255.440954c171.326054 128.322526 414.898035 93.403662 543.220561-77.922392 33.542752-44.895683 56.592642-95.123803 68.289602-149.308248 3.78431-17.373425 21.157736-28.554342 38.359147-24.770032 17.373425 3.78431 28.554342 20.985721 24.770032 38.359147-13.761129 63.473207-40.59533 122.130018-79.814547 174.422308-72.417941 96.67193-178.378633 159.45708-298.100454 176.658492C559.217873 967.579372 537.372081 969.127499 515.698303 969.127499z" fill="#575B66" p-id="2669"></path></svg>
// loading指令实现的效果的css样式的loading.scss文件

.co-loading {
  @keyframes loading-rotate {
      from {
          transform: rotate(0);
      }
      to {
          transform: rotate(360deg);
      }
  }
  @at-root &-mask {
    position: absolute;
    z-index: 2000;
    background-color: hsla(0,0%,100%,.9);
    margin: 0;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    transition: opacity .3s;
  }
  @at-root &-spinner {
    top: 50%;
    margin-top: -21px;
    width: 100%;
    text-align: center;
    position: absolute;
  }
  @at-root &-circular {
      width: 42px;
      height: 42px;
      animation: loading-rotate 2s infinite;
  }
}

五、svg图片

如果需要解析:

// 1.先下载一个url-loader
npm i url-loader
// 2.在webpack.config.js文件中配置一下解析的规则
module.exports = {
    module: {
        reles: [
            {
                test: /\.svg$/,
                use: ['url-loader']
            }
        ]
    }
}

如果需要自定义svg图标:

// 1.先安装一个loader,这个loader没有被vue官方所收集。
npm i vue-svg-loader -D
// 2.在vue.config.js中书写文件
const path = require("path")
module.exports = {
    chainWebpack: (config) => {
        // 先锁定处理svg文件
        const svgRule = config.module.rule("svg");
        // vue-svg-loader只处理指定的文件目录
        // (include是指定文件夹,exclude是除了这个文件夹)
        svgRule.include.add(path.resolve(__dirname, "目录名称"))
        // 清空处理svg图片的规则
        svgRule.uses.clear();
        // 然后自己添加规则去解析
        svgRule.use("vue-svg-loader").loader("vue-svg-loader");
    }
}
// 3.然后导入svg格式的文件并注册(举例)
import loadingSvg from './loading.svg';

export default {
    components: {
        loadingSvg
    }
}

六、jsx语法

jsx语法: 在vue中书写template的时候,它会根据模板先生成render函数,由函数的执行去生成虚拟节点树,最后再转换为真实的dom树,但是render函数的书写并不是很容易,所以便出现了jsx语法,这个语法它保留了template的简单的写法,也继承了render函数的灵活性。

render函数与template的对比: template语法理解起来较为容易,语法比较简单,对js要求很低,也就导致了它使用起来不够灵活,不能够发挥js的全部能力,但是前者却恰恰相反。

注意: render函数返回什么就会渲染什么,并且它只能返回虚拟dom节点

render函数:

// 1.要返回虚拟的节点,那么就需要生成虚拟节点,可以使用createElement()函数
//   将真实的节点转换为虚拟节点,以此提供给render函数来渲染,由于这个函数比
//   较长,所以将它俗称为h函数,render函数的形参自己会携带这个h。
render(h) {
    return h(App) // 使用
}

render(createElement) {
    // vue在初始化的过程中,会主动把createElement这个函数传递给render函数
    // 作为参数,所以这里也可以使用createElement,
    return createElement(App) 
}

// 2.render函数中标签的嵌套

// 格式:标签名、标签所带的属性(用对象表示)、子标签的内容(用数组表示)
// 注意:如果子标签还存在子标签,嵌套下去就可以了
// 举例:
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
    </div>
    <router-view />
  </div>
</template>

// 转换
<script>
render(h) {  
    return h("div", { attrs: { id"app" } }, [  
      h("div", { attrs: { id"nav" } }, [  
        h("router-link", { attrs: { to"/" } }, "Home"),  
        h("router-link", { attrs: { to"/about" } }, "About"),  
      ]),  
      h("router-view"),
      // 事件绑定的写法
      h("button", { on: { click: this.fn } }, "事件的绑定" ) 
    ]);  
  },
</script>

render函数与jsx语法:

  // jsx语法写法:一切跟动态绑定有关的东西都使用{}就可以了
  render() {   
    return (  
      <div  
        class="home"  
        id={this.idName}  
        title="home"  
        style={{ color: "#fff", fontSize: "40px", lineHeight: 2 }}  
        onMousemove={function () {  
          console.log(123);  
        }}  
      >  
        <co-button type="danger" onClick={() => (this.loading = !this.loading)}>  
          {![]()this.name}  
        </co-button>  
        <h1>  
          <span>span</span>  
        </h1>  
        <h1>  
          <span>span</span>  
        </h1>  
        <ul>
          {this.list.map((item) => (  
            <li key={item}>{item}</li>  
          ))}  
        </ul>
        {this.visible ? <p>测试v-if</p> : null}  
        <p style={{ display: this.show ? "block: "none" }}>测试v-show</p>  
      </div>  
    );  
  }, 
  
  // render函数改写:  
  render(h) {  
    return h(  
      "div",  
      {  
        class: { hometrue },  
        style: { color"#fff"fontSize"40px"lineHeight2 },  
        attrs: { idthis.idNametitle"home" }, // v-bind:id="idName"  
        on: {  
          // v-on  
          mousemovefunction () {  
            console.log(123);  
          },  
        },  
        directives: [  
          {  
            name"loading",  
            valuethis.loading,  
          },  
        ],  
      },  
      [  
        h(  
          "co-button",  
          {  
            props: {  
              type"danger",  
            },  
            on: {  
              click() => {  
                this.loading = !this.loading;  
              },  
            },  
          },  
          ![]()this.name // => {{name}}  
        ),  
        h("h1", {  
          domProps: {  
            innerHTML"<span>span</span>"// v-html  
          },  
        }),  
        h("h1", [  
          h("span", {  
            domProps: {  
              textContent"span"// => v-text  
            },  
          }),  
        ]),  
        h(  
          "ul",  
          // v-for :key  
          this.list.map((item) => h("li", { key: item }, item))  
        ),  
        this.visible ? h("p""测试v-if") : null,  
        h(  
          "p",  
          { attrs: { style: { displaythis.show ? "block" : "none" } } },  
          "测试v-show"  
        ),  
      ]  
    );  
  },  
  
  // 上面做注释的地方就是自己实现指令系统的地方,因为在render函数和jsx语法中,
  // vue的指令系统不能够使用了,需要自己去使用自己学过的方法去实现它,上面简单
  // 实现了一下。