el-drawer封装hooks测试

Posted by Shallow Dreameron February 9, 2025
好的,用户需要将el-drawer组件二次封装成一个通用的hook,让它在任何组件中都能使用,可以自由控制弹出方向,并且允许添加自定义组件和事件。首先,我得回忆一下Element UI的el-drawer组件的用法和属性。el-drawer有direction属性控制方向,比如rtl、ltr、ttb、btt。然后,用户希望这个hook能返回一些方法,比如打开、关闭,以及可能的状态,比如是否显示。

接下来,我需要考虑如何用Vue 3的Composition API来封装。通常,hook会导出一些响应式变量和方法。所以,可能需要一个useDrawer的函数,返回open、close、toggle方法,以及方向direction、是否显示visible等状态。另外,用户需要在drawer内部添加自定义的内容和事件,所以应该使用插槽机制。但是hook本身不能直接处理模板,所以可能需要结合provide/inject或者render函数来处理。

不过,用户可能希望在使用这个hook的时候,能够在组件中直接写入自定义内容,而不是通过配置。这时候,可能需要将hook和组件一起封装。也就是说,创建一个自定义组件,比如DrawerComponent,然后在hook中控制这个组件的状态。这样,用户可以在自己的组件中使用hook提供的方法,并在模板里放置DrawerComponent,里面可以插入自定义内容。

那具体怎么设计呢?首先,hook负责管理状态,比如visible、direction、标题等。然后,DrawerComponent接收这些状态作为props,并渲染el-drawer。用户在使用时,先调用hook得到这些状态和方法,然后在模板里使用DrawerComponent,并通过插槽添加内容。这样,用户就可以在任意组件中引入DrawerComponent,并通过hook控制它的显示和方向。

接下来需要考虑如何让用户自定义事件。比如,用户可能在drawer内部有按钮,点击后触发事件。这时候,用户应该可以在插槽内直接绑定事件,就像普通组件一样。所以,DrawerComponent需要透传插槽,这样用户的内容和事件就能正常使用。

另外,可能还需要处理一些props,比如标题、宽度、是否显示关闭按钮等。这些可以通过hook的参数传入配置,或者在调用hook时动态设置。比如,hook可以接受一个配置对象,包含direction、title等默认值,同时允许在打开drawer时覆盖这些配置。

那具体的代码结构大概是怎样的呢?首先,定义useDrawer函数,里面用ref来管理visible、direction等状态。然后提供open、close、toggle方法。当调用open时,可以传入新的配置,比如改变方向或者标题。然后,DrawerComponent组件接收这些状态作为props,并绑定到el-drawer上。用户在使用时,引入DrawerComponent,并在模板中使用,同时通过插槽添加内容。

这样,用户的使用方式可能是这样的:

在组件中:

import { useDrawer } from './useDrawer';

const { Drawer, openDrawer } = useDrawer();

然后在模板里:

<Drawer>

<div @click="自定义事件">内容</div>

</Drawer>

不过,可能需要调整hook的结构,让Drawer成为一个组件,而hook返回这个组件和控制方法。或者,hook返回的是状态和方法,而用户需要自己引入Drawer组件,并将状态绑定上去。

或者,在hook内部,使用动态组件,并通过provide/inject来传递插槽内容。但这样可能比较复杂。

另一种方式是,将Drawer作为hook的一部分返回,用户需要在模板中显式渲染它,并通过作用域插槽或普通插槽插入内容。例如:

const { drawerProps, open, close } = useDrawer();

然后在模板中使用:

<DrawerComponent v-bind="drawerProps" @close="close">

<template #default>

用户自定义内容

</template>

</DrawerComponent>

这样,DrawerComponent是一个封装的el-drawer,接收相应的props和事件,并暴露插槽。这样用户就可以在插槽中添加任何内容和事件。

可能还需要处理默认插槽和作用域插槽的问题,确保用户可以灵活地添加内容。此外,还需要考虑如何在打开时动态修改配置,比如方向、标题等。比如,在调用open方法时,可以传入一个配置对象,更新这些属性。

综上,具体的步骤可能是:

1. 创建useDrawer hook,内部管理visible、direction、title等状态。

2. 提供open、close、toggle方法,允许在调用时传递配置参数来更新状态。

3. 创建一个DrawerComponent组件,接收这些状态作为props,并渲染el-drawer,同时暴露插槽。

4. 用户在使用时,调用useDrawer获取Drawer组件和控制方法,然后在模板中放置Drawer组件,并通过插槽添加内容。

现在,编写代码的大纲:

首先,useDrawer.ts:

import { ref } from 'vue';

export function useDrawer(defaultConfig = {}) {

const visible = ref(false);

const config = ref({

direction: 'rtl',

title: '默认标题',

size: '30%',

...defaultConfig,

});

const open = (overrideConfig = {}) => {

config.value = { ...config.value, ...overrideConfig };

visible.value = true;

};

const close = () => {

visible.value = false;

};

const toggle = () => {

visible.value = !visible.value;

};

return {

visible,

config,

open,

close,

toggle,

};

}

然后,DrawerComponent.vue:

<template>

<el-drawer

v-model="visible"

:direction="config.direction"

:title="config.title"

:size="config.size"

@close="close"

>

<slot></slot>

</el-drawer>

</template>

<script setup>

defineProps({

visible: Boolean,

config: Object,

close: Function,

});

</script>

不过,这样可能还不够灵活,因为用户需要将DrawerComponent引入到自己的组件中,并且绑定visible和config。这时候,或许更好的方式是将DrawerComponent和hook结合起来,让hook返回一个组件,或者通过provide/inject来传递状态。

或者,让useDrawer返回一个渲染函数,但这可能不太符合用户的使用习惯。

另一种方式是将DrawerComponent作为hook的一部分,用户在使用时,在setup中调用hook,然后在模板中使用返回的组件。例如:

在useDrawer中:

const drawerProps = computed(() => ({ visible: visible.value, ...config.value }));

return { drawerProps, open, close, toggle };

然后用户需要在模板中:

<DrawerComponent :visible="drawerProps.visible" :direction="drawerProps.direction" @close="close">

<!-- 插槽内容 -->

</DrawerComponent>

不过,这样用户需要自己处理props的绑定,可能不够方便。

或许更好的方式是,将DrawerComponent封装到hook内部,并返回一个组件,同时允许插槽内容。但如何在hook中处理插槽呢?这可能需要使用render函数或者jsx。

不过,在Vue 3中,可以使用defineComponent和setup函数来创建一个组件,然后通过作用域插槽传递数据。但这样可能会增加复杂度。

或者,采用另一种思路,将自定义内容通过函数式的方式传入。例如,在调用open方法时,传入一个渲染函数。但这样可能不够灵活,用户可能更习惯使用模板和插槽。

这时候,可能需要重新考虑设计。或许用户希望的是在任意组件中调用hook,然后在当前组件的模板中使用某个组件,该组件由hook提供,并且可以插入内容。例如,hook返回一个Drawer组件,用户可以在自己的模板中使用这个组件,并添加插槽内容。

不过,这在Vue中可能比较困难,因为组件的模板必须在template中编写,无法动态生成。所以,可能的解决方案是:用户需要显式地在模板中放置一个Drawer组件,并通过hook来控制它的状态。

例如,用户的使用方式应该是:

在script中:

const { drawerProps, open } = useDrawer();

在template中:

<MyDrawer v-bind="drawerProps">

<button @click="handleClick">自定义按钮</button>

</MyDrawer>

而MyDrawer组件内部封装了el-drawer,并接收drawerProps,同时暴露插槽。

这样,用户可以在自己的组件中引入MyDrawer,并通过hook控制它的显示和配置,同时在插槽中添加任何内容。

因此,在封装时,需要创建这个MyDrawer组件,并且在hook中管理状态。这样,用户调用hook得到状态和方法,同时在模板中使用MyDrawer组件,并绑定状态,插槽内容由用户提供。

所以,完整的代码结构可能如下:

useDrawer.js:

import { ref } from 'vue';

export function useDrawer(defaultConfig = {}) {

const visible = ref(false);

const config = ref({

direction: 'rtl',

title: '默认标题',

size: '30%',

...defaultConfig,

});

const open = (overrideConfig = {}) => {

config.value = { ...config.value, ...overrideConfig };

visible.value = true;

};

const close = () => {

visible.value = false;

};

const toggle = () => {

visible.value = !visible.value;

};

return {

Drawer: {

visible,

config,

open,

close,

toggle,

},

// 或者返回各个属性和方法

drawerProps: {

visible,

...config.value,

},

open,

close,

toggle,

};

}

然后,DrawerComponent.vue:

<script setup>

import { ElDrawer } from 'element-plus';

const props = defineProps({

visible: Boolean,

direction: {

type: String,

default: 'rtl',

},

title: String,

size: String,

});

const emit = defineEmits(['update:visible', 'close']);

const drawerVisible = computed({

get: () => props.visible,

set: (val) => emit('update:visible', val),

});

const handleClose = () => {

emit('close');

};

</script>

<template>

<ElDrawer

v-model="drawerVisible"

:direction="direction"

:title="title"

:size="size"

@close="handleClose"

>

<slot></slot>

</ElDrawer>

</template>

但这样的话,用户需要在他们的组件中引入DrawerComponent,并通过hook传递状态。例如:

用户组件:

<template>

<button @click="open({ direction: 'btt' })">打开抽屉</button>

<DrawerComponent

:visible="visible"

:direction="config.direction"

:title="config.title"

:size="config.size"

@close="close"

>

<!-- 自定义内容 -->

<button @click="submit">提交</button>

</DrawerComponent>

</template>

<script setup>

import { useDrawer } from './useDrawer';

import DrawerComponent from './DrawerComponent.vue';

const { visible, config, open, close } = useDrawer();

</script>

不过,这样用户需要手动绑定很多props,可能比较繁琐。所以,是否可以将DrawerComponent与hook更紧密地结合,让hook返回一个已经绑定好props的组件?

或者,在useDrawer中返回一个组件,用户可以直接在模板中使用,而不需要手动绑定props。例如:

const { Drawer, open, close } = useDrawer();

然后在模板中:

<Drawer>

内容

</Drawer>

但这样需要动态创建组件,可能比较复杂。在Vue中,可以使用createVNode和render函数,但可能会影响响应式。

另一种思路是,hook返回一个对象,其中包含需要绑定的props和事件,用户使用v-bind将其绑定到组件上。例如:

const { drawerProps, open, close } = useDrawer();

然后在模板中:

<DrawerComponent v-bind="drawerProps" @close="close">

...

</DrawerComponent>

这样,drawerProps是一个包含visible、direction等属性的响应式对象,用户只需要用v-bind绑定即可,而不需要逐个绑定。

这样,useDrawer可以返回drawerProps,其中包含visible和配置项,以及open、close等方法。DrawerComponent组件接收这些props,并绑定到el-drawer上。

这样,用户在组件中使用时,只需:

import { useDrawer } from './useDrawer';

import DrawerComponent from './DrawerComponent.vue';

const { drawerProps, open, close } = useDrawer();

然后在模板:

<DrawerComponent v-bind="drawerProps" @close="close">

<!-- 插槽内容 -->

</DrawerComponent>

这样,用户可以在任何组件中引入DrawerComponent,并通过hook控制它。同时,可以自定义插槽内容,添加事件等。

现在,调整useDrawer的实现:

在useDrawer函数中,管理visible和配置状态,并提供方法:

import { reactive, ref } from 'vue';

export function useDrawer(defaultConfig = {}) {

const visible = ref(false);

const config = reactive({

direction: 'rtl',

title: '默认标题',

size: '30%',

...defaultConfig,

});

const open = (overrideConfig = {}) => {

Object.assign(config, overrideConfig);

visible.value = true;

};

const close = () => {

visible.value = false;

};

const toggle = () => {

visible.value = !visible.value;

};

return {

drawerProps: {

visible: visible.value,

...config,

},

open,

close,

toggle,

};

}

不过,这里drawerProps.visible是visible.value的当前值,但visible是ref,所以应该用computed来保持响应式。否则,当visible变化时,drawerProps不会自动更新。

所以,更好的方式是将drawerProps包装成一个计算属性:

import { computed, reactive, ref } from 'vue';

export function useDrawer(defaultConfig = {}) {

const visible = ref(false);

const config = reactive({

direction: 'rtl',

title: '默认标题',

size: '30%',

...defaultConfig,

});

const drawerProps = computed(() => ({

visible: visible.value,

direction: config.direction,

title: config.title,

size: config.size,

}));

const open = (overrideConfig = {}) => {

Object.assign(config, overrideConfig);

visible.value = true;

};

// ...其他方法

return {

drawerProps,

open,

close,

toggle,

};

}

这样,drawerProps就是一个计算属性,当visible或config变化时,会自动更新。

然后,在DrawerComponent中,接收这些props:

<script setup>

defineProps({

visible: Boolean,

direction: String,

title: String,

size: String,

});

const emit = defineEmits(['update:visible', 'close']);

// 处理visible的双向绑定

const drawerVisible = computed({

get() {

return this.visible;

},

set(val) {

emit('update:visible', val);

},

});

const handleClose = () => {

emit('close');

};

</script>

<template>

<el-drawer

v-model="drawerVisible"

:direction="direction"

:title="title"

:size="size"

@close="handleClose"

>

<slot></slot>

</el-drawer>

</template>

用户在使用时,组件中的模板部分:

<template>

<button @click="open({ direction: 'ttb', title: '自定义标题' })">打开</button>

<DrawerComponent

v-bind="drawerProps"

@update:visible="(val) => (drawerProps.visible = val)"

@close="close"

>

<!-- 自定义内容 -->

<button @click="submit">提交</button>

</DrawerComponent>

</template>

不过,这里可能需要处理visible的双向绑定。因为drawerProps.visible是来自hook中的visible ref,所以当DrawerComponent内部修改visible时,应该更新hook中的visible值。可以通过在DrawerComponent中emit 'update:visible',然后在父组件中监听这个事件,并更新visible.value。

但这样用户需要在模板中手动绑定@update:visible,可能不够方便。或许更好的方式是在hook的drawerProps中包含一个onUpdateVisible方法,或者在DrawerComponent内部直接修改hook中的visible ref。但这样可能破坏封装性。

或者,在useDrawer中返回的drawerProps包含visible的ref,并且DrawerComponent使用v-model:visible绑定。例如:

用户组件中的使用:

<DrawerComponent v-model:visible="drawerProps.visible" :direction="drawerProps.direction" ... >

但这样用户需要手动绑定每个属性,而无法简单地使用v-bind="drawerProps"。因为v-model需要单独的绑定。

可能这里存在一个矛盾:使用v-bind="drawerProps"会将visible作为prop传递,但更新visible需要emit事件。所以,在DrawerComponent中,visible应该是一个prop,并且通过v-model来同步。

但此时,drawerProps是一个计算属性,包含visible的值,而visible的更新需要在hook中处理。所以,当DrawerComponent内部emit update:visible事件时,用户需要监听这个事件并更新hook中的visible ref。

例如,在用户组件的模板中:

<DrawerComponent

v-bind="drawerProps"

@update:visible="val => visible.value = val"

@close="close"

>

但这样用户必须显式地处理这个事件,可能不够方便。或许可以在hook中提供一个方法,自动处理这个事件。例如,在hook返回的drawerProps中也包括事件处理器:

但这样可能比较复杂。或者,考虑在DrawerComponent内部直接修改visible的值,但这样会破坏单向数据流的原则,除非使用v-model。

可能更简单的方式是让用户手动处理visible的双向绑定。例如:

用户组件的模板:

<DrawerComponent

:visible="drawerProps.visible"

:direction="drawerProps.direction"

:title="drawerProps.title"

:size="drawerProps.size"

@update:visible="val => visible.value = val"

@close="close"

>

...

</DrawerComponent>

这样,当DrawerComponent内部的el-drawer的visible变化时,会触发update:visible事件,用户监听到后更新hook中的visible ref,从而触发重新渲染。

虽然这样需要用户手动绑定事件,但可能更符合Vue的数据流模式。或者,可以提供一个更简化的方式。

另一种思路是,在useDrawer中,返回的drawerProps包含visible的ref,而DrawerComponent内部使用v-model绑定visible。例如:

DrawerComponent的代码:

<el-drawer v-model="visible" ... >

但这样需要visible是一个ref,而用户传递的visible是来自hook的ref。这样,在DrawerComponent中,直接使用v-model绑定到props.visible可能可行,但需要确保props.visible是响应式的。

例如,用户在使用时,将visible作为ref传递给DrawerComponent的v-model:

<DrawerComponent v-model="visible" ... >

但这样用户需要从hook中获取visible的ref,而不是通过drawerProps。这可能与之前的封装方式不同。

综上,可能最直接的方式是让用户在模板中显式绑定visible,并使用v-model:

用户组件:

const { visible, direction, title, size, open, close } = useDrawer();

然后在模板中:

<DrawerComponent

v-model:visible="visible"

:direction="direction"

:title="title"

:size="size"

@close="close"

>

...

</DrawerComponent>

这样,用户需要从hook中获取各个属性,而不是通过drawerProps对象。这可能更灵活,但需要用户写更多的绑定代码。

或者,在hook中返回一个对象包含所有状态,用户可以使用展开运算符:

const drawer = useDrawer();

然后在模板中:

<DrawerComponent v-model:visible="drawer.visible" :direction="drawer.direction" ... >

这样,用户可以根据需要选择绑定的属性。

不过,回到用户的需求,他们希望可以方便地在任意组件中使用,并且可以自由控制方向,添加自定义组件和事件。所以,可能最佳的封装方式是:hook返回状态和方法,而用户负责将这些状态绑定到DrawerComponent组件,该组件封装了el-drawer,并暴露插槽。

这样,用户的使用步骤是:

1. 引入useDrawer和DrawerComponent。

2. 调用useDrawer获取状态和方法。

3. 在模板中使用DrawerComponent,绑定状态,并在插槽中添加内容和事件。

这样,用户可以在插槽中自由添加任何内容,包括带有自定义事件的组件。

最终,代码可能如下:

useDrawer.ts:

import { ref, reactive } from 'vue';

export function useDrawer(initialConfig = {}) {

const visible = ref(false);

const config = reactive({

direction: 'rtl',

title: '默认标题',

size: '30%',

...initialConfig,

});

const open = (overrideConfig = {}) => {

Object.assign(config, overrideConfig);

visible.value = true;

};

const close = () => {

visible.value = false;

};

const toggle = () => {

visible.value = !visible.value;

};

return {

visible,

config,

open,

close,

toggle,

};

}

DrawerComponent.vue:

<script setup>

import { ElDrawer } from 'element-plus';

const props = defineProps({

visible: {

type: Boolean,

required: true,

},

direction: {

type: String,

default: 'rtl',

},

title: String,

size: String,

});

const emit = defineEmits(['update:visible', 'close']);

const drawerVisible = computed({

get: () => props.visible,

set: (val) => emit('update:visible', val),

});

const handleClose = () => {

emit('close');

};

</script>

<template>

<ElDrawer

v-model="drawerVisible"

:direction="direction"

:title="title"

:size="size"

@close="handleClose"

>

<slot />

</ElDrawer>

</template>

用户的使用示例:

在某个组件中:

<template>

<button @click="open({ direction: 'btt' })">打开抽屉</button>

<DrawerComponent

v-model:visible="visible"

:direction="config.direction"

:title="config.title"

:size="config.size"

@close="close"

>

<div>

<h3>自定义内容</h3>

<button @click="handleSubmit">提交</button>

</div>

</DrawerComponent>

</template>

<script setup>

import { useDrawer } from './useDrawer';

import DrawerComponent from './DrawerComponent.vue';

const { visible, config, open, close } = useDrawer();

const handleSubmit = () => {

// 处理提交逻辑

close();

};

</script>

这样,用户可以通过调用open方法并传递配置来改变弹出方向和其他属性,同时在插槽中添加任意内容和事件。

总结,这种封装方式将状态管理与组件分离,用户通过hook获取状态和控制方法,并通过组件绑定状态,利用插槽添加内容,满足了灵活性和可重用性的需求。

下面是一个将 Element UI 的 el-drawer 封装为通用 Hook 的完整解决方案,包含可直接复用的代码:

<!-- DrawerComponent.vue -->
<template>
  <el-drawer
    v-model="drawerVisible"
    v-bind="$attrs"
    :direction="direction"
    @close="handleClose"
  >
    <slot />
  </el-drawer>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps({
  visible: {
    type: Boolean,
    required: true
  },
  direction: {
    type: String,
    default: 'rtl',
    validator: (val: string) => ['rtl', 'ltr', 'ttb', 'btt'].includes(val)
  }
})

const emit = defineEmits(['update:visible', 'close'])

const drawerVisible = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
})

const handleClose = () => {
  emit('close')
}
</script>
// useDrawer.ts
import { ref, reactive } from 'vue'

type DrawerDirection = 'rtl' | 'ltr' | 'ttb' | 'btt'

interface DrawerConfig {
  direction?: DrawerDirection
  title?: string
  size?: string | number
  withHeader?: boolean
  // 其他 el-drawer 支持的属性
}

export function useDrawer(initialConfig: DrawerConfig = {}) {
  const visible = ref(false)
  const config = reactive<DrawerConfig>({
    direction: 'rtl',
    title: '默认标题',
    size: '30%',
    withHeader: true,
    ...initialConfig
  })

  const open = (overrideConfig: DrawerConfig = {}) => {
    Object.assign(config, overrideConfig)
    visible.value = true
  }

  const close = () => {
    visible.value = false
  }

  const toggle = () => {
    visible.value = !visible.value
  }

  return {
    visible,
    config,
    open,
    close,
    toggle
  }
}

使用示例

<script setup lang="ts">
import { useDrawer } from './useDrawer'
import DrawerComponent from './DrawerComponent.vue'

const { visible, config, open, close } = useDrawer({
  direction: 'btt',
  title: '设置面板'
})

const handleSave = () => {
  console.log('保存操作')
  close()
}
</script>

<template>
  <el-button @click="open({ title: '编辑配置' })">打开抽屉</el-button>

  <DrawerComponent
    v-model:visible="visible"
    :direction="config.direction"
    :title="config.title"
    :size="config.size"
    @close="close"
  >
    <!-- 自定义内容 -->
    <div class="custom-content">
      <h3>个性化设置</h3>
      <el-form>
        <el-form-item label="主题颜色">
          <el-color-picker />
        </el-form-item>
        <el-form-item label="字体大小">
          <el-input-number />
        </el-form-item>
      </el-form>
      <el-button type="primary" @click="handleSave">保存设置</el-button>
    </div>
  </DrawerComponent>
</template>

<style scoped>
.custom-content {
  padding: 20px;
}
</style>

功能特性

  1. 灵活控制:通过 Hook 返回的 open/close/toggle 方法控制抽屉状态
  2. 方向控制:支持 4 种弹出方向(rtl/ltr/ttb/btt)
  3. 动态配置:open 方法支持传入临时配置覆盖默认设置
  4. 完全可定制
    • 通过插槽可添加任意内容
    • 支持所有 el-drawer 原生属性和事件
    • 可通过 config 配置标题、尺寸等参数
  5. 类型安全:使用 TypeScript 编写,提供完善的类型提示

高级用法

// 动态修改配置示例
const { open } = useDrawer()

// 打开时指定不同方向
const openFromTop = () => {
  open({
    direction: 'ttb',
    size: '50%',
    title: '从顶部弹出'
  })
}

// 自定义无标题抽屉
const openNoHeader = () => {
  open({
    withHeader: false,
    direction: 'btt',
    size: 200
  })
}

这个方案通过组合式 API 实现了抽屉组件的逻辑复用,同时保持视图层的灵活性。使用时可以自由选择:直接使用默认配置、通过初始化参数配置、或在调用 open 方法时动态覆盖配置。