1. ホーム
  2. ビュー

Vue3のdefineComponentは何をするのですか?

2022-02-27 13:58:04
<パス

defineComponent 関数は、setup 関数を単純にラップして、オプションのオブジェクトを返します。

export function defineComponent(options: unknown) {
  return isFunction(options) ? { setup: options } : options
}


defineComponentの最も重要な点は、TypeScriptの下で正しいパラメータの型推論をコンポーネントに与えることである。


defineComponent オーバーロードされた関数

1: 直接設定機能

// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props, RawBindings = object>(
  setup: (
    props: Readonly<Props>,
    ctx: SetupContext
  ) => RawBindings | RenderFunction
): DefineComponent<Props, RawBindings>

// overload 2: object format with no props
// (uses user defined props interface)
// return type is for Vetur and TSX support
export function defineComponent<
  Props = {},
  RawBindings = {},
  D = {},
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = EmitsOptions,
  EE extends string = string
>(
  options: ComponentOptionsWithoutProps<Props,RawBindings,D,C,M,Mixin,Extends,E,EE>
): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>

// overload 3: object format with array props declaration
// props inferred as { [key in PropNames]? : any }
// return type is for Vetur and TSX support
export function defineComponent<
  PropNames extends string,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string
>(
  options: ComponentOptionsWithArrayProps<
    PropNames,
    RawBindings, ... >
): DefineComponent<
  Readonly<{ [key in PropNames]? : any }>,
  RawBindings,... >

// overload 4: object format with object props declaration
// see `ExtractPropTypes` in . /componentProps.ts
export function defineComponent<
  // the Readonly constraint allows TS to treat the type of { required: true }
  // as constant instead of boolean.
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>


<イグ
2: プロップスなしのオブジェクト形式

<script lang="tsx">
import { noop, trim } from 'lodash';
import {
  inject, Ref, defineComponent, getCurrentInstance, ref
} from '@vue/composition-api';
import filters from '@/filters';
import CommonDialog from '@/components/CommonDialog';
import ChildTable, { getEmptyModelRow } from '. /ChildTable.vue';
 
export interface IParentDialog {
  show: boolean;
  specFn: (component_id: HostComponent['id']) => Promise<{ data: DictSpecs }>;
}
 
export default defineComponent<IParentDialog>({
  // Custom components in tsx still have to be registered
  components: {
    ChildTable
  },
  props: {
    show: {
      type: Boolean,
      default: false
    },
    specFn: {
      type: Function,
      default: noop
    }
  },
 
  // note: setup must use the arrow function
  setup: (props, context) => {
 
    // Fix a problem in tsx where the 'h' function could not be injected automatically
    // eslint-disable-next-line no-unused-vars
    const h = getCurrentInstance()! $createElement;
 
    const { emit } = context;
    const { specFn, show } = props;
 
    // usage of filter
    const { withColon } = filters;
 
    // usage of inject
    const pageType = inject<CompSpecType>('pageType', 'foo');
    const dictComponents = inject<Ref<DictComp[]>>('dictComponents', ref([]));
    
    // type constraints for ref
    const dictSpecs = ref<DictSpecs>([]);
    const loading = ref(false);
 
    const _lookupSpecs = async (component_id: HostComponent['id']) => {
      loading.value = true;
      try {
        const json = await specFn(component_id);
        dictSpecs.value = json.data;
      } finally {
        loading.value = false;
      }
    };
 
    const formdata = ref<Spec>({
      component_id: '',
      specs_id: '',
      model: [getEmptyModelRow()]
    });
    const err1 = ref('');
    const err2 = ref('');
    
    const _doCheck = () => {
      err1.value = '';
      err2.value = '';
      
      const { component_id, specs_id, model } = formdata.value;
      if (!component_id) {
        err1.value = 'Please select a component';
        return false;
      }
      for (let i = 0; i < model.length; i++) {
        const { brand_id, data } = model[i];
        if (!brand_id) {
          err2.value = 'Please select a brand';
          return false;
        }
        if (
          formdata.value.model.some(
            (m, midx) => midx ! == i && String(m.brand_id) === String(brand_id)
          )
        ) {
          err2.value = 'Brand duplicate';
          return false;
        }
      }
      return true;
    };
 
    const onClose = () => {
      emit('update:show', false);
    };
    const onSubmit = async () => {
      const bool = _doCheck();
      if (!bool) return;
      const params = formdata.value;
      emit('submit', params);
      onClose();
    };
 
    // note: in tsx, globally registered components such as element-ui still have to use the kebab-case form ???? 
    return () => (
      <CommonDialog
        class="comp"
        title="New"
        width="1000px"
        labelCancel="Cancel"
        labelSubmit="OK"
        vLoading={loading.value}
        show={show}
        onClose={onClose}
        onSubmit={onSubmit}
      >
        <el-form labelWidth="140px" class="create-page">
         <el-form-item label={withColon('PartType')} required={true} error={err1.value}>
            <el-select
              class="full-width"
              model={{
                value: formdata.value.component_id,
                callback: (v: string) => {
                  formdata.value.component_id = v;
                  _lookup

<イグ
3: 配列プロップス宣言のあるオブジェクト形式

// overload 3: object format with array props declaration
// props inferred as { [key in PropNames]? : any }
// return type is for Vetur and TSX support
export function defineComponent<
  PropNames extends string,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string
>(
  options: ComponentOptionsWithArrayProps<
    PropNames,
    RawBindings, ... >
): DefineComponent<
  Readonly<{ [key in PropNames]? : any }>,
  RawBindings,... >


<イグ
4: object props 宣言のある object format

// overload 4: object format with object props declaration
// see `ExtractPropTypes` in . /componentProps.ts
export function defineComponent<
  // the Readonly constraint allows TS to treat the type of { required: true }
  // as constant instead of boolean.
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>


<イグ

開発実践

ユニットテストでの基本的な使い方に加え、以下のParentDialogコンポーネントは、実用的な開発において、以下のような留意すべき点があります。

カスタムコンポーネントとグローバルコンポーネントの書き方

inject、refなどの型制約。

書き込み設定と対応するhインジェクションの問題

tsxでのv-modelとscopedSlotsの書き方

ParentDialog.vue

<script lang="tsx">
import { noop, trim } from 'lodash';
import {
  inject, Ref, defineComponent, getCurrentInstance, ref
} from '@vue/composition-api';
import filters from '@/filters';
import CommonDialog from '@/components/CommonDialog';
import ChildTable, { getEmptyModelRow } from '. /ChildTable.vue';
 
export interface IParentDialog {
  show: boolean;
  specFn: (component_id: HostComponent['id']) => Promise<{ data: DictSpecs }>;
}
 
export default defineComponent<IParentDialog>({
  // Custom components in tsx still have to be registered
  components: {
    ChildTable
  },
  props: {
    show: {
      type: Boolean,
      default: false
    },
    specFn: {
      type: Function,
      default: noop
    }
  },
 
  // note: setup must use the arrow function
  setup: (props, context) => {
 
    // Fix a problem in tsx where the 'h' function could not be injected automatically
    // eslint-disable-next-line no-unused-vars
    const h = getCurrentInstance()! $createElement;
 
    const { emit } = context;
    const { specFn, show } = props;
 
    // usage of filter
    const { withColon } = filters;
 
    // usage of inject
    const pageType = inject<CompSpecType>('pageType', 'foo');
    const dictComponents = inject<Ref<DictComp[]>>('dictComponents', ref([]));
    
    // type constraints for ref
    const dictSpecs = ref<DictSpecs>([]);
    const loading = ref(false);
 
    const _lookupSpecs = async (component_id: HostComponent['id']) => {
      loading.value = true;
      try {
        const json = await specFn(component_id);
        dictSpecs.value = json.data;
      } finally {
        loading.value = false;
      }
    };
 
    const formdata = ref<Spec>({
      component_id: '',
      specs_id: '',
      model: [getEmptyModelRow()]
    });
    const err1 = ref('');
    const err2 = ref('');
    
    const _doCheck = () => {
      err1.value = '';
      err2.value = '';
      
      const { component_id, specs_id, model } = formdata.value;
      if (!component_id) {
        err1.value = 'Please select a component';
        return false;
      }
      for (let i = 0; i < model.length; i++) {
        const { brand_id, data } = model[i];
        if (!brand_id) {
          err2.value = 'Please select a brand';
          return false;
        }
        if (
          formdata.value.model.some(
            (m, midx) => midx ! == i && String(m.brand_id) === String(brand_id)
          )
        ) {
          err2.value = 'Brand duplicate';
          return false;
        }
      }
      return true;
    };
 
    const onClose = () => {
      emit('update:show', false);
    };
    const onSubmit = async () => {
      const bool = _doCheck();
      if (!bool) return;
      const params = formdata.value;
      emit('submit', params);
      onClose();
    };
 
    // note: in tsx, globally registered components such as element-ui still have to use the kebab-case form ???? 
    return () => (
      <CommonDialog
        class="comp"
        title="New"
        width="1000px"
        labelCancel="Cancel"
        labelSubmit="OK"
        vLoading={loading.value}
        show={show}
        onClose={onClose}
        onSubmit={onSubmit}
      >
        <el-form labelWidth="140px" class="create-page">
         <el-form-item label={withColon('PartType')} required={true} error={err1.value}>
            <el-select
              class="full-width"
              model={{
                value: formdata.value.component_id,
                callback: (v: string) => {
                  formdata.value.component_id = v;
                  _lookup

全文要約

defineComponent()を導入し、setup()コンポーネントのパラメータタイプを正しく推測できるようにした。
defineComponentは、propsなし、配列propsなどのフォームに正しく適応します。
defineComponentは、明示的なカスタムプロップインターフェース、またはプロパティ検証オブジェクトから自動的に推論されることを受け入れることができます。
tsxでは、element-uiのようなグローバルに登録されたコンポーネントは、まだkebab-case形式を使用しなければなりません。
tsxでは、v-modelはmodel={でモデル化することになっています。 <未定義 { value, callback }} と記述します。
tsxでは、スコープ付きスロットはscopedSlots={で記述します。 <未定義 { foo: (スコープ) => () }} 書き込み
defineComponent は機能的なコンポーネントには適用されないので、RenderContext を使用して解決する必要があります。