2021-01-17 Vue组件化思想及实践

本文探讨Vue组件化思想,阐述如何通过组件提高开发效率和应用可维护性。介绍了组件通信的多种方式,如props、自定义事件、Vuex等,并通过组件化实战案例,展示了通用表单组件和弹窗组件的实现,包括KForm、KFormItem、KInput、通知组件Notice以及表格组件KTable的构建过程。
摘要由CSDN通过智能技术生成

2021-01-17 Vue组件化思想及实践

组件化

vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率方面重复使用简化调试步骤提升项目可维护性便于多人协同开发

组件化
组件通信常用方式

  • props
  • $emit/$on
  • event bus
  • vuex

边界情况

  • $parent
  • $children
  • $root
  • $refs
  • provide/inject
  • 非props特性
    - $attrs
    - $listeners

组件通信
props
父给子传值

// child
props:{msg: String}

//parent
<HelloWorld msg="Hello World!" />

自定义事件
子给父传值

// child
this.$emit('add', good)

//parent
<Cart @add="cartAdd($event)" />

事件总线
任意两个组件之间传值常用事件总线或vuex的方式

// Bus:事件派发、监听和回调管理
class Bus{
	constructor(){
		this.callbacks = [];
	}
	$on(name, fn){
		this.callbacks[name] = this.callbacks[name] || [];
		this.callbacks[name].push(fn)
	}
	$emit(name, args){
		if(this.callbacks[name]){
			this.callbacks[name].forEach(cb=>cb(args))
		}
	}
}

// main.js
Vue.prototype.$bus = new Bus()

// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')

实践中通常用Vue代替Bus,因为Vue已经实现了$on$emit

vuex
创建唯一的全局数据管理者store,通过它管理数据并通知组件状态更新

$attrs/$listeners

包含了父作用域中不作为prop被识别的(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过v-bind="$attrs"传入内部组件——在创建高级别的组件时非常有用

// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo" />

// 给Grandson隔代传值
<Child2 msg="foo" @some-event="onSomeEvent" />

// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners" />

// Grandson使用
<div @click="$emit('some-evnet', 'msg from grandson')">
	{{msg}}
</div>

provide/inject
能够实现祖先和后代之间传值

// ancestor
provide(){
	return {
		yc: 'yichan'
	}
}

// descendant
inject: ['yc']

插槽

插槽语法是Vue实现内容分发API,用于复合组件开发。该技术在通用组件库开发中有大量应用

匿名插槽

// comp1
<div>
	<slot></slot>
</div>

// parent
<comp>hello</comp>

具名插槽
将内容分发到自组件指定位置

// comp2
<div>
	<slot></slot>
	<slot name="content"></slot>
</div>

//parent
<Comp2>
	<!-- 默认插槽⽤default做参数 -->
	 <template v-slot:default>具名插槽</template>
	 <!-- 具名插槽⽤插槽名做参数 -->
	 <template v-slot:content>内容...</template>
 </Comp2>

作用域插槽
分发内容要用到子组件中的数据

// comp3
<div>
 <slot :foo="foo"></slot>
</div>
// parent
<Comp3>
 <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
 <template v-slot:default="slotProps">
 来⾃⼦组件数据:{{slotProps.foo}}
 </template>
</Comp3>

组件化实战

通用表单组件
收集数据、校验数据并提交

需求:

  • 表单KForm
    - 载体,输入数据model,校验规则rules
    - 校验validate
  • 表单项KFormItem
    - label标签添加
    - 载体,输入项包起来
    - 校验执行者,显示错误
  • 输入框KInput
    - 双绑
    - 图标、反馈

KInput.vue

<template>
  <div>
    <!-- 1.双绑 -->
    <input :type="type" :value="value" @input="onInput" v-bind="$attrs" />
  </div>
</template>

<script>
export default {
  // 特性继承关掉,div上就不会出现$attrs
  inheritAttrs: false,
  props: {
    value: {
      type: String,
      default: "",
    },
    type: {
      type: String,
      default: "text",
    },
  },
  methods: {
    onInput(e) {
      this.$emit("input", e.target.value);

      // 触发校验
      // 源码中实现了一个this.dispatch('k-form-item', 'validate')
      this.$parent.$emit("validate");
    },
  },
};
</script>

<style scoped>
</style>

KFormItem.vue


<template>
  <div>
    <!-- 1.labelasyncValidator -->
    <label v-if="label">{{ label }}</label>
    <!-- 2.内部使用 -->
    <slot></slot>
    <!-- 3.校验结果 -->
    <p v-if="error">{{ error }}</p>
    <!-- <p>{{ form.rules[this.prop] }}</p> -->
  </div>
</template>

<script>
import Validator from "async-validator";

export default {
  inject: ["form"],
  props: {
    label: {
      type: String,
      default: "",
    },
    prop: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      error: "",
    };
  },
  mounted() {
    // 老爹挂载之后,孩子一定已挂载,KInput已挂载,可以监听校验事件
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      // 获取校验规则
      const rules = this.form.rules[this.prop];
      // 当前值
      const value = this.form.model[this.prop];
      const validator = new Validator({ [this.prop]: rules });

      // 执行校验,返回一个promise
      return validator.validate({ [this.prop]: value }, (errors) => {
        if (errors) {
          // 显示错误信息
          this.error = errors[0].message;
        } else {
          // 校验通过清除错误
          this.error = "";
        }
      });
    },
  },
};
</script>

<style scoped>
</style>

KForm.vue

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    model: {
      type: Object,
      required: true,
    },
    rules: Object,
  },
  methods: {
    // 全局校验
    validate(cb) {
      // 1.执行全部Item校验
      // 结果[Promise...]
      const results = this.$children
        .filter((item) => item.prop)
        .map((item) => item.validate());
      // 2.判断校验结果
      Promise.all(results)
        .then(() => cb(true))
        .catch(() => cb(false));
    },
  },
};
</script>

<style scoped>
</style>

组合上面的表单组件
index.vue

<template>
  <div>
    <!-- <ElementTest></ElementTest> -->
    <!-- KInput -->
    <k-form :model="model" :rules="rules" ref="loginForm">
      <k-form-item label="用户名" prop="username">
        <k-input v-model="model.username" placeholder="用户名" />
        <!-- 等效于 -->
        <!-- <k-input :value="model.username" @input="model.username=$event" /> -->
      </k-form-item>
      <k-form-item label="密码" prop="password">
        <k-input v-model="model.password" placeholder="密码" />
      </k-form-item>
      <k-form-item>
        <button @click="onLogin">登录</button>
      </k-form-item>
    </k-form>
  </div>
</template>

<script>
import KForm from "./KForm.vue";
import KFormItem from "./KFormItem.vue";
// import ElementTest from "@/components/form/ElementTest.vue";
import KInput from "./KInput.vue";

export default {
  provide() {
    // 隔代传参给KFormItem
    return {
      form: this,
    };
  },
  components: {
    // ElementTest
    KInput,
    KFormItem,
    KForm,
  },
  data() {
    return {
      model: {
        username: "tom",
        password: "",
      },
      rules: {
        username: [{ required: true, message: "请输⼊⽤户名" }],
        password: [{ required: true, message: "请输⼊密码" }],
      },
    };
  },
  methods: {
    onLogin() {
      // 全局校验
      this.$refs.loginForm.validate((isValid) => {
        if (isValid) {
          console.log("submit login");
        } else {
          alert("校验失败");
        }
      });
    },
  },
};
</script>

<style scoped>
</style>

实现弹窗组件
弹窗类组件的特点是它们在当前Vue实例之外独立存在,通常挂载于body;它们是通过JS动态创建的,不需要在任何组件中声明。
通知组件:Notice.vue

<template>
  <div class="box" v-if="isShow">
    <h3>{{ title }}</h3>
    <p class="box-content">{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: "",
    },
    message: {
      type: String,
      default: "",
    },
    duration: {
      type: Number,
      default: 3000,
    },
  },
  data() {
    return {
      isShow: false,
    };
  },
  methods: {
    show() {
      this.isShow = true;
      setTimeout(this.hide, this.duration);
    },
    hide() {
      this.isShow = false;
      this.remove();
    },
  },
};
</script>

<style scoped>
.box {
  position: fixed;
  width: 100%;
  top: 16px;
  left: 0;
  text-align: center;
  pointer-events: none;
  background-color: #fff;
  border: grey 3px solid;
  box-sizing: border-box;
}

.box-content {
  width: 200px;
  margin: 10px auto;
  font-size: 14px;
  padding: 8px 16px;
  background: #fff;
  border-radius: 3px;
  margin-bottom: 8px;
}
</style>

动态使用工具方法:utils/create.js

import Vue from "vue";

// 1.create(Comp):动态创建Comp组件实例
// 2.执行挂载获取其dom元素
// 3.追加到body中

export default function create(Component, props) {
  // 创建组件实例
  // 方案1:extends
  /* const Ctor = Vue.extends(Component);
  new Ctor({ propsData: props }); */
  // 方案2:借鸡生蛋
  const vm = new Vue({
    render: h => h(Component, { props })
  }).$mount(); // 只挂载,不指定宿主,依然一颗得到dom

  // 手动追加
  document.body.appendChild(vm.$el);

  // 获取组件实例
  //   vm.$root // 根实例
  const comp = vm.$children[0]; // 根组件实例

  // 给一个淘汰方法
  comp.remove = () => {
    document.body.removeChild(vm.$el);
    vm.$destroy();
  };

  // 返回一个组件实例
  return comp;
}

封装成一个插件 plugins/notice.js

import Notice from "@/components/Notice.vue";
import create from "@/utils/create";

export default {
  // 把Notice和create封装为一个插件
  install(Vue) {
    Vue.prototype.$notice = props => {
      const comp = create(Notice, props);
      comp.show();
      return comp;
    };
  }
};

在main.js中使用Vue.use()

// 导入封装插件
import notice from "@/plugins/notice";

Vue.use(notice);

测试代码 index.vue

// 用封装插件形式
        this.$notice({
          title: "测试",
          message: isValid ? "过" : "不过",
        });

实现Table组件
一个简单的表格组件KTable,有基本实现、插槽以及排序功能

模板代码:index.vue

<template>
  <div>
    <!-- KTable -->
    <k-table :data="tableData">
      <k-table-column sortable prop="date" label="日期"></k-table-column>
      <k-table-column sortable prop="name" label="姓名"></k-table-column>
      <k-table-column prop="address" label="地址"></k-table-column>
      <k-table-column label="操作">
        <template v-slot:default="scope">
          <button @click="handleEdit(scope.$index, scope.row)">编辑</button>
          <button @click="handleDelete(scope.$index, scope.row)">删除</button>
        </template>
      </k-table-column>
    </k-table>
  </div>
</template>

<script>
import KTable from "./KTable.vue";
import KTableColumn from "./KTableColumn.vue";
export default {
  components: { KTable, KTableColumn },
  data() {
    return {
      tableData: [
        {
          date: "2016-05-02",
          name: "小徐",
          address: "上海市",
        },
        {
          date: "2017-05-03",
          name: "小虎",
          address: "上海市",
        },
        {
          date: "2018-05-20",
          name: "小刘",
          address: "上海市",
        },
        {
          date: "2021-01-02",
          name: "小李",
          address: "上海市",
        },
      ],
    };
  },
};
</script>

<style scoped>
</style>

表格组件:KTable.vue


<script>
export default {
  props: {
    data: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      orderFiled: "",
      orderBy: "desc",
    };
  },
  computed: {
    columns() {
      // 由于不一定有prop属性,内部如果出现了默认作用域插槽,则就按照它来执行渲染
      return this.$slots.default
        .filter((vnode) => vnode.tag)
        .map(({ data: { attrs, scopedSlots } }) => {
          const column = { ...attrs };
          if (scopedSlots) {
            // 自定义模板
            column.renderCell = (row, i) => (
              <div>{scopedSlots.default({ row, $index: i })}</div>
            );
          } else {
            // 设置prop的情况
            column.renderCell = (row) => <div>{row[column.prop]}</div>;
          }
          return column;
        });
    },
  },
  created() {
    this.columns.forEach((column) => {
      // 如果存在sortable列,则头一个作为默认排序字段
      if (column.hasOwnProperty("sortable")) {
        if (column.prop && !this.orderFiled) {
          this.sort(column.prop, this.orderBy);
        }
      }
    });
  },
  methods: {
    sort(filed, by) {
      this.orderFiled = filed;
      this.orderBy = by;

      this.data.sort((a, b) => {
        const v1 = a[this.orderFiled];
        const v2 = b[this.orderFiled];

        if (typeof v1 === "number") {
          return this.orderBy === "desc" ? v2 - v1 : v1 - v2;
        } else {
          return this.orderBy === "desc"
            ? v2.localeCompare(v1)
            : v1.localeCompare(v2);
        }
      });
    },
    toggleSort(field) {
      const by = this.orderBy === "desc" ? "asc" : "desc";
      this.sort(field, by);
    },
  },
  // 实现一个渲染函数JSX
  render() {
    return (
      <table>
        <thead>
          <tr>
            {this.columns.map((column) => {
              if (column.hasOwnProperty("sortable") && column.prop) {
                let orderArrow = "↑↓";
                if (this.orderFiled === column.prop) {
                  orderArrow = this.orderBy === "desc" ? "↓" : "↑";
                }
                return (
                  <th
                    key={column.label}
                    onClick={() => this.toggleSort(column.prop)}
                  >
                    {column.label} <span>{orderArrow}</span>
                  </th>
                );
              } else {
                return <th key={column.label}>{column.label}</th>;
              }
            })}
          </tr>
        </thead>
        <tbody>
          {this.data.map((row, rowIndex) => (
            <tr key={rowIndex}>
              {this.columns.map((column, columnIndex) => (
                <td key={columnIndex}>{column.renderCell(row, rowIndex)}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    );
  },
};
</script>

<style scoped>
table {
  width: 50%;
  margin: 0 auto;
}
</style>

插槽组件:KTableColumn.vue

<template>
  <div></div>
</template>

<script>
export default {};
</script>

<style scoped>
</style>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值