Published on

设计一个兼容外部传参的Vue组件

Authors
  • avatar
    Name
    Deng Hua
    Twitter

目录

假设有一个手风琴组件,组建内部有自己的 open 状态,该组件将在单击时显示和隐藏其内容。某些情况下,你希望能够覆盖该内部状态并能从父组件控制它,该如何设计?

组件内部的Props

从最简单的部分开始:

<template>
  <div>
    <div
      class="title"
      @click="toggleHidden"
    >
      {{ title }}
    </div>
    <div v-if="!hidden" class="content">
      <slot />
    </div>
  </div>
</template>
import { ref } from "vue";

const props = defineProps({
  title: String,
});

const hidden = ref(true);

const toggleHidden = () => {
  hidden.value = !hidden.value;
};

如果您使用 Options API,它会如下所示:


export default {
  name: 'Toggle',
  props: {
    title: {
      type: String,
    },
  },
  data() {
    return {
      hidden: true,
    };
  },
  methods: {
    toggleHidden() {
      this.hidden = !this.hidden;
    },
  },
};

只需要为组件提供content便可使用:

<template>
  <Toggle title="Toggled Content">
    This content can be toggled on and off.
  </Toggle>
</template>

接受外部传参控制

<div class="content">/*...*/</div>的这部分通常是隐藏的,直到用户单击标题切换显示。

但现在需求发生了变化,我们希望父组件也能够触发内容切换。

换句话说,我们不再希望由组件内部来管理状态。我们希望由父级来管理和切换状态。

这很简单:

import { ref } from "vue";

const props = defineProps({
  title: String,
  hidden: Boolean,
});

const emit = defineEmits(['click']);
const toggleHidden = () => emit('click');

Options API版本:

export default {
  name: 'Toggle',
  emits: ['click'],
  props: {
    title: {
      type: String,
      required: true,
    },
    hidden: {
      type: Boolean,
      default: true,
    },
  },

  methods: {
    toggleHidden() {
      this.$emit('click');
    },
  },
};

现在可以从父组件控制它了:

<template>
  <Toggle
    title="Toggled Content"
    :hidden="hidden"
    @click="hidden = !hidden"
  >
    This content can be toggled on and off.
  </Toggle>
</template>

两者兼得

我们现在已经看到了编写该组件的两种不同方法。

第一种方法是最容易使用的,组件内部会管理其自身的状态。第二种方式给了我们更多的控制权,但父级必须管理维护状态。

但我们能两者兼得吗?

有没有一种方法可以让一个组件可以以两种方式使用?

我们需要一个 hidden props以及一个 hidden的state。

为了使用不同的名称,我们将内部状态称为 _hidden ,这样props仍然可以使用hidden

<template>
  <div>
    <div
      class="title"
      @click="toggleHidden"
    >
      {{ title }}
    </div>
    <div
      class="content"
      v-if="!hidden"
    >
      <slot />
    </div>
  </div>
</template>
import { ref } from "vue";

const props = defineProps({
  title: String,
  hidden: Boolean,
});

const emit = defineEmits(['click']);

const _hidden = ref(true);

const toggleHidden = () => emit('click');

Options API版本:

export default {
  name: 'Toggle',
  emits: ['click'],

  props: {
    title: {
      type: String,
    },
    hidden: {
      type: Boolean,
    }
  },

  data() {
    return {
      _hidden: true,
    };
  },

  methods: {
    toggleHidden() {
      this.$emit('click');
    },
  },
};

请注意,我们没有为 hidden props提供默认值。 因为我们将使用计算属性将 props 和内部state“组合”在一起:

const hidden = computed(() =>
  props.hidden !== undefined
    ? props.hidden
    : _hidden.value
);

Options API版本:

computed: {
  $hidden() {
    return this.hidden !== undefined
      ? this.hidden
      : this._hidden;
  },
},

即如果设置了props,我们将使用props来源的hidden。否则我们使用组件内部的_hidden

<template>
  <div>
    <div
      class="title"
      @click="toggleHidden"
    >
      {{ title }}
    </div>
    <div
      class="content"
      v-if="!hidden"
    >
      <slot />
    </div>
  </div>
</template>

import { ref, computed } from "vue";

const props = defineProps({
  title: String,
  hidden: Boolean,
});

const _hidden = ref(false);

const emit = defineEmits(['click']);
const toggleHidden = () => emit('click');

const hidden = computed(() =>
  props.hidden !== undefined ? props.hidden : _hidden.value
);

Options API 版本如下:

<template>
  <div>
    <div
      class="title"
      @click="toggleHidden"
    >
      {{ title }}
    </div>
    <div
      class="content"
      v-if="!$hidden"
    >
      <slot />
    </div>
  </div>
</template>
export default {
  name: 'Toggle',
  emits: ['click'],
  props: {
    title: {
      type: String,
    },
    hidden: {
      type: Boolean,
    }
  },
  data() {
    return {
      _hidden: true,
    };
  },
  methods: {
    toggleHidden() {
      this.$emit('click');
    },
  },
  computed: {
    $hidden() {
      return this.hidden !== undefined
        ? this.hidden
        : this._hidden;
    },
  },
};

但是toggleHidden函数还不完善,需要判断是否存在hiddenprops,以及对应的分支逻辑:

/*
toggleHidden() {
  this.$emit('click');
}
改写为:
*/

const toggleHidden = () => {
  if (props.hidden !== undefined) {
    emit('click');
  } else {
    _hidden.value = !_hidden.value;
  };
};

Options API:

toggleHidden() {
  if (this.hidden !== undefined) {
    this.$emit('click');
  } else {
    this._hidden = !this._hidden;
  }
},

完整示例:

<template>
  <div>
    <div
      class="title"
      @click="toggleHidden"
    >
      {{ title }}
    </div>
    <div v-if="!hidden" class="content">
      <slot />
    </div>
  </div>
</template>

<script setup>
  import { ref, computed } from "vue";

  const props = defineProps({
    title: String,
    hidden: Boolean,
  });
  const emit = defineEmits(["click"]);

  const _hidden = ref(true);

  const toggleHidden = () => {
    if (props.hidden !== undefined) {
      emit("click");
    } else {
      _hidden.value = !_hidden.value;
    }
  };

  const hidden = computed(() =>
    props.hidden !== undefined ? props.hidden : _hidden.value
  );
</script>

简而言之,如果父组件外部提供了props,则子组件使用外部提供的props,并且状态更改时也是更改外部的props。

否则使用组件内部维护的状态。

End.