从Vben-Admin里面学习hooks

在Vue 3的生态体系中,Hooks(通常指基于组合式API的自定义函数)已成为提升代码复用性与可维护性的核心工具。尽管Vue官方文档并未直接使用"Hooks"这一术语,但其倡导的组合式API设计理念,与React Hooks在逻辑复用的核心思想上高度契合。本文将深入解析Vue3 Hooks的本质、优势、使用方法及实战场景,从Vben-admin提供的例子里学习和理解Hooks。

一、Vue3 Hooks的核心概念

  1. 本质与定义

Vue3 Hooks本质上是遵循特定命名规范(以use开头)的JavaScript函数,用于封装组件中可复用的状态逻辑、副作用处理及生命周期管理。通过调用Vue的响应式API(如ref、reactive)和生命周期钩子(如onMounted、onUnmounted),Hooks能够与Vue的响应式系统无缝集成,其内部状态变化可自动触发视图更新。

  1. 与Vue2 Mixin的对比

在Vue 2时代,逻辑复用主要依赖混入(Mixin),然而,这些方案存在明显缺陷:Mixin易引发命名冲突,且逻辑来源不清晰,导致调试复杂。而Vue3 Hooks通过函数式封装实现逻辑复用,职责单一,组件可通过解构赋值明确获取所需状态和方法,有效规避了传统方案的痛点。

  1. 与公共组件的对比

Hooks和Vue公共组件功能类似,但还是有本质区别。举个简单的例子,我们现在有一个文件预览功能,用公共组件的方式实现,就是一个FilePreview组件,传入FileList,并且用一个变量控制预览弹窗的打开和关闭,这个变量一般是写在业务里,谁用谁定义,而Hooks的思路,封装的不仅仅是预览组件,还有组件的调用方式。后面的例子会详细说明。

二、Vben-Admin里面的一些Hooks

  1. useRefs
ts 复制代码
import type { Ref } from 'vue';
import { ref, onBeforeUpdate } from 'vue';

export function useRefs(): [Ref<HTMLElement[]>, (index: number) => (el: HTMLElement) => void] {
  const refs = ref([]) as Ref<HTMLElement[]>;
  onBeforeUpdate(() => {
    refs.value = [];
  });

  const setRefs = (index: number) => (el: HTMLElement) => {
    refs.value[index] = el;
  };
  return [refs, setRefs];
}

Vue3中,针对非遍历的ref,可以直接访问到,但是很难在Vue3中访问v-for动态渲染出来的ref。

ts 复制代码
const testRef = ref(null) // 直接就能在onMounted之后访问到

我们通过TS的定义,可以看到Ref是一个什么结构:

ts 复制代码
export declare type VNodeProps = {
    key?: string | number | symbol;
    ref?: VNodeRef;
    ref_for?: boolean;
    ref_key?: string;
    onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[];
    onVnodeMounted?: VNodeMountHook | VNodeMountHook[];
    onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[];
    onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[];
    onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[];
    onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[];
};

declare type VNodeRef = string | Ref | ((ref: object | null, refs: Record<string, any>) => void);

我们的useRefs可以直接在v-for的时候,只需传入索引,就能自动把我们需要的DOM元素放到refs变量里:

ts 复制代码
//......
<li
  :ref="setRefs(index)"
  v-for="(item, index) in list"
</li>
//......

import { useRefs } from '/@/hooks/core/useRefs';
const [refs, setRefs] = useRefs();

// refs 就是我们需要拿到的DOM

特别说明:Vue3.5新增:useTemplateRef 可以更方便地去操作ref。

  1. useWindowSizeFn
ts 复制代码
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
import { useDebounceFn } from '@vueuse/core';

interface WindowSizeOptions {
  once?: boolean;
  immediate?: boolean;
  listenerOptions?: AddEventListenerOptions | boolean;
}

export function useWindowSizeFn<T>(fn: Fn<T>, wait = 150, options?: WindowSizeOptions) {
  let handler = () => {
    fn();
  };
  const handleSize = useDebounceFn(handler, wait);
  handler = handleSize;

  const start = () => {
    if (options && options.immediate) {
      handler();
    }
    window.addEventListener('resize', handler);
  };

  const stop = () => {
    window.removeEventListener('resize', handler);
  };

  tryOnMounted(() => {
    start();
  });

  tryOnUnmounted(() => {
    stop();
  });
  return [start, stop];
}

正常在一个Vue页面里监听屏幕尺寸变化,我们是怎么写的?

ts 复制代码
import { debounce } from 'lodash-es';
import { onMounted, onUnmounted } from 'vue'

let handler = () => {
   myChart && myChart.resize() // 举个例子,屏幕尺寸变化,我们让echarts适应屏幕
};

const handleSize = debounce(handler, 300);
onMounted(() => {
  window.addEventListener('resize', handleSize);
});

onUnmounted(() => {
  window.removeEventListener('resize', handleSize);
});

用hooks之后:

ts 复制代码
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
let handler = () => {
   myChart && myChart.resize() // 举个例子,屏幕尺寸变化,我们让echarts适应屏幕
};
useWindowSizeFn(handler, 300, { immediate: false });
// 如果需要手动start或stop
// const [start, stop] = useWindowSizeFn(handler, 300, { immediate: false });

我们注意到,hook 把监听、卸载、防抖逻辑全部封装了,使用起来十分方便,且不容易遗漏逻辑。

三、自己写一个Hooks

Vben-admin里的附件预览组件如下:

html 复制代码
<template>
  <BasicModal
    width="800px"
    :title="t('component.upload.preview')"
    wrapClassName="upload-preview-modal"
    v-bind="$attrs"
    @register="register"
    :showOkBtn="false"
  >
    <FileList :dataSource="fileListRef" :columns="columns" :actionColumn="actionColumn" />
  </BasicModal>
</template>
<script lang="ts">
  import { defineComponent, watch, ref } from 'vue';
  //   import { BasicTable, useTable } from '/@/components/Table';
  import FileList from './FileList.vue';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import { previewProps } from './props';
  import { PreviewFileItem } from './typing';
  import { downloadByUrl } from '/@/utils/file/download';
  import { createPreviewColumns, createPreviewActionColumn } from './data';
  import { useI18n } from '/@/hooks/web/useI18n';
  import { isArray } from '/@/utils/is';

  export default defineComponent({
    components: { BasicModal, FileList },
    props: previewProps,
    emits: ['list-change', 'register', 'delete'],
    setup(props, { emit }) {
      const [register, { closeModal }] = useModalInner();
      const { t } = useI18n();

      const fileListRef = ref<PreviewFileItem[]>([]);
      watch(
        () => props.value,
        (value) => {
          if (!isArray(value)) value = [];
          fileListRef.value = value
            .filter((item) => !!item)
            .map((item) => {
              return {
                url: item,
                type: item.split('.').pop() || '',
                name: item.split('/').pop() || '',
              };
            });
        },
        { immediate: true },
      );

      // 删除
      function handleRemove(record: PreviewFileItem) {
        const index = fileListRef.value.findIndex((item) => item.url === record.url);
        if (index !== -1) {
          const removed = fileListRef.value.splice(index, 1);
          emit('delete', removed[0].url);
          emit(
            'list-change',
            fileListRef.value.map((item) => item.url),
          );
        }
      }

      // 下载
      function handleDownload(record: PreviewFileItem) {
        const { url = '' } = record;
        downloadByUrl({ url });
      }

      return {
        t,
        register,
        closeModal,
        fileListRef,
        columns: createPreviewColumns() as any[],
        actionColumn: createPreviewActionColumn({ handleRemove, handleDownload }) as any,
      };
    },
  });
</script>

针对这个组件,我们是这样使用的:

ts 复制代码
import { ref, shallowRef } from 'vue';
import { useModal } from '/@/components/Modal';
import UploadPreviewModal from '/@/components/Upload/src/UploadPreviewModal.vue';

const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
const curFileList = shallowRef([]);

const openFileModal = list => {
  curFileList.value = list
  openPreviewModal(true, {})
}

改写成Hooks:

js 复制代码
import { ref, shallowRef } from 'vue';
import UploadPreviewModal from '/@/components/Upload/src/UploadPreviewModal.vue';
import { useModal } from '/@/components/Modal';

const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
export function useFilePreview() {
  const curFileList = shallowRef([]);
  const openFileModal = list => {
    curFileList.value = list
    openPreviewModal(true, {})
  }
  return { openFileModal, UploadPreviewModal, registerPreviewModal, openPreviewModal };
}

使用:

ts 复制代码
import { useFilePreview } from '/@/hooks/component/useFilePreview';
const { openFileModal, registerPreviewModal, UploadPreviewModal } = useFilePreview(); // 附件预览

openFileModal([
 // 附件信息
])

封装后,我们只需要在template里直接定义:

html 复制代码
<UploadPreviewModal
  :value="fileList"
  @register="registerPreviewModal"
/>

然后用openFileModal方法直接打开弹窗。

我们清楚地看到,公共组件和hooks对于同一个功能的使用情况的差别。实际上,这个hooks也不是随随便便写的,而是因为有大量的按公共组件的方式使用的场景,我们发现每次都需要在业务组件里定义变量和引用组件,就会想到把curFileList和UploadPreviewModal写到hooks里面,从而进一步封装成hooks。

四、总结

Vue3 Hooks作为组合式API的核心实践,为组件逻辑的复用与组织提供了现代化的解决方案。通过将复杂逻辑拆分为职责单一的函数模块,开发者能够构建出结构清晰、易于维护的代码体系。

相关推荐
Mintopia1 小时前
MSW Mock Feature-First 方案
前端·架构
sin6031 小时前
Talk is cheap 之后:AI Agent 时代,程序员真正要交付什么?
前端
Ticnix1 小时前
手把手教你在 Next.js 中接入本地大模型,实现 ChatGPT 同款流式对话
前端·next.js
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_18:(HTML 表格进阶特性与无障碍——从标题结构到屏幕阅读器适配)
前端·笔记·ui·html·音视频
沐 修1 小时前
前端调试 - 获取下拉框元素 F12 延时断点操作记录 - 秒杀其他所谓的F8和手速快操作
前端
天蓝色的鱼鱼1 小时前
当AI开始替我写代码,我还要纠结选Vue还是React吗?
vue.js·react.js·ai编程
恋猫de小郭1 小时前
AI 时代开源协议将消亡,malus 讽刺性展示了这一点
前端·人工智能·ai编程
Mike_jia2 小时前
MeterSphere:开源持续测试平台,让测试管理变得如此简单
前端
Csvn2 小时前
Vue 3 响应式原理深度解析
前端