漫谈vue组件设计

886 阅读3分钟

本来标题准备起 vue 组件设计,不过觉得不太好太过于广泛了,下面就以一个实际的例子来谈谈项目中用到的设计思想。

bg1

这个是一个很传统的级联选择器,在项目中使用大概有三个方面使用

  • 第一个起到搜索作用,比如一个列表的筛选需要选定一定的条件
  • 在创建或者编辑的时候使用,例如新建一个列表项
  • 单独在某一个场景使用,比如只使用省这一级别

基于以上两点,直接硬怼肯定不行了,要保证这个组件的足够低耦合同时也要方便自己使用,比如使用的时候肯定期待,只需要放置组件,一些组件的变动交给内部处理。

为了方便讲解所有数据会固定,但是实际中可能数据是通过接口获取的

初步设计想法

bg2

向上抽离一个组件起到监听的作用,当根组件或者前置组件发生变化的时候,通知到watch提醒watch通知相关依赖组件,这里就是

为了代码的精简,这里只展示两级

回想一下组件的通信,对于这种父子组件可以使用emit的方式 不过在这里不行,因为上面举例子说了可能用作新建中。

也就是说可能是被这样包裹的

<a-form>
  <watch>
    <a-form-item>
      <>< />
    </a-form-item>
    <a-form-item>
      <>< />
    </a-form-item>
  </watch>
</a-form>

所以使用父子组件通信不太可行。 不过还是有两种方式的

  • 使用$parent$children

指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent 访问父实例,子实例被推入父实例的 $children 数组中。

  • provide / inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

组件实现

第一种方式的实现就是通过递归来实现,这个实现过程可以通过匹配组件相对应的name实现,今天重点用provide / inject的方式来实现。

代码本身不是特别多,直接就贴了

child.vue

<template>
  <span class="cs-child">
    <select v-bind="$attrs" :value="value" @change="change">
      <option v-for="(item, k) of province" :key="k" :value="item"
        >{{ item }}</option
      >
    </select>
  </span>
</template>

<script>
  export default {
    props: ["value"],
    model: {
      prop: "value",
      event: "change",
    },
    name: "cs-child",
    inject: {
      theme: {
        default: () => undefined,
      },
    },
    data() {
      const province = [
        "河北省",
        "山西省",
        "辽宁省",
        "吉林省",
        "黑龙江省",
        "江苏省",
        "浙江省",
        "安徽省",
        "福建省",
        "江西省",
        "山东省",
        "河南省",
        "湖北省",
        "湖南省",
        "广东省",
        "海南省",
        "四川省",
        "贵州省",
        "云南省",
        "陕西省",
        "甘肃省",
        "青海省",
        "台湾省",
      ];
      return {
        province,
      };
    },
    methods: {
      change(e) {
        const value = e.target.value;
        this.$emit("change", value);
        this.trigger(value);
      },
      trigger(value = this.value) {
        if (!this.theme) {
          return;
        }
        this.theme.obj = {
          ...this.theme.obj,
          child: value,
        };
        this.theme.$emit("change-item", value);
      },
    },
    // 注意生命周期时机,在依赖组件我们是在created绑定的时间,需要隔离这个事件来触发事件
    mounted() {
      this.trigger();
    },
  };
</script>

relyonChild.vue

<template>
  <span class="cs-relyonChild">
    <select v-bind="$attrs" v-model="value">
      <option v-for="(item, k) of list" :key="k" :value="item.value"
        >{{ item.name }}</option
      >
    </select>
  </span>
</template>

<script>
  export default {
    name: "cs-relyonChild",
    inject: ["theme"],
    data() {
      return {
        list: [{ name: 1, value: 1 }],
        value: undefined,
      };
    },
    created() {
      this.theme.$on("change-item", this.change);
    },
    watch: {
      value: {
        handler(value) {
          this.theme.obj = {
            ...this.theme.obj,
            relyonChild: value,
          };
        },
        immediate: true,
      },
    },
    methods: {
      change(value) {
        this.list = new Array(100).fill().map((_item, key) => {
          return {
            name: `${value || ""}-${key}`,
            value: key,
          };
        });
        this.value = 0;
      },
    },
  };
</script>

watch.vue

<template>
  <span class="cs-watch">
    <slot></slot>
  </span>
</template>

<script>
  export default {
    name: "cs-watch",
    props: ["value"],
    model: {
      prop: "value",
      event: "change",
    },
    data() {
      return {
        obj: {
          child: undefined,
          relyonChild: undefined,
        },
      };
    },
    watch: {
      obj: {
        handler(value) {
          this.$emit("change", value);
        },
        immediate: true,
      },
    },
    provide() {
      return {
        theme: this,
      };
    },
  };
</script>

OK,整体的代码就放到上面了,需要特别注意两点

  1. inject
  inject: {
    theme: {
      default: () => undefined
    }
  },

这里之所以使用default是为了让child.vue可以单独使用,不然控制台会报一个错误。

  1. $on$emit绑定时机

relyonChild.vue组件依赖child.vue组件,在一个完整的生命周期,我们能拿到 this 的情况最早也是在created中,所以relyonChild.vuecreated中绑定了需要监听的事件,而child.vue初始化的时候也需要传递一次事件,而这个事件只能晚于created,否则会导致relyonChild.vue监听不到。

使用方法

  • index.js
export { default as child } from "./child.vue";
export { default as watch } from "./watch.vue";
export { default as relyonChild } from "./relyonChild.vue";
  • App.vue
<template>
  <div id="app">
    <watch v-model="obj">
      <label>
        <span>省级区域:</span>
        <child v-model="item"></child>
      </label>
      <label>
        <span>子项区域:</span>
        <relyonChild></relyonChild>
      </label>
      <button @click="click">查询</button>
    </watch>
  </div>
</template>

<script>
  import { watch, child, relyonChild } from "./components/demo";

  export default {
    name: "App",
    components: {
      watch,
      child,
      relyonChild,
    },
    data() {
      return {
        item: "河南省",
        obj: undefined,
      };
    },
  };
</script>

最后

贴一个预览地址,有什么疑问可以多看几遍,子组件通过绑定$on$emit来实现监听与触发

codesandbox.io/s/modest-wr…