工作日记 之 从v-model进阶成自定义 Hooks

54 阅读5分钟

Hooks 使用与管理全攻略

概述:

本文系统整理了 Vue 项目中使用组合式函数(hooks/composables)的最佳实践,涵盖结构、收集、复用、依赖管理等方面。


🎯 一句话总结

将逻辑从组件中解耦出来,变得更清晰、更可复用、更易测试和维护。


✅ 1. 为什么使用自定义 hooks?

❌ 问题背景(传统写法带来的痛点)

如果你在组件中直接写:

<script setup>
import { ref } from 'vue';

const username = ref('');
function resetUsername() {
  username.value = '';
}
</script>

问题在于:

问题自定义 hooks 的解决方式
表单字段逻辑重复useFormInput()useFormCheckbox() 提取复用逻辑
组件代码臃肿不清晰把状态逻辑抽出去,组件更专注展示
多页面字段逻辑一致hooks 复用一致逻辑,维护一处生效
不易测试和扩展hooks 可独立单测、添加校验、监听等能力
✅ Hooks 的作用和优势
  1. 逻辑复用

    你只要写一次 useFormInput,可以在多个组件中复用这个“输入+重置”的逻辑。

    const username = useFormInput("张三");
    const phone = useFormInput("123456");
    

    而不用每次都手动写:

    const xxx = ref("");
    function resetXxx() {
      xxx.value = "";
    }
    
  2. 关注点分离

    把「表单状态 + 初始化 + 重置」的逻辑独立出去,组件里就只关心业务,不用管这些底层细节。组件变得更专注、更干净。

  3. 更易扩展和维护

    比如我们要添加“输入值监听”或“校验”:

    export function useFormInput(initialValue = "") {
      const value = ref(initialValue);
    
      // 新增监听逻辑
      watch(value, (val) => {
        console.log("输入值变化为", val);
      });
    
      function reset() {
        value.value = initialValue;
      }
    
      return { value, reset };
    }
    

    你只改了 useFormInput 这一处,所有使用它的组件都立刻受益。

  4. 可单独测试(更容易写单元测试)

    import { useFormInput } from "./useFormInput";
    
    test("should reset to initial value", () => {
      const { value, reset } = useFormInput("abc");
      value.value = "xyz";
      reset();
      expect(value.value).toBe("abc");
    });
    

    组件逻辑越“干净”,越能通过组合函数单独测试。


✅ 2. 推荐目录结构

✅ 命名 composables 更通用,适合团队协作,或者也可以叫 hooks/


src/
├── composables/  👈 推荐命名:组合式函数(推荐)
│ ├── form/
│ │ ├── useFormInput.js
│ │ ├── useFormCheckbox.js
│ │ ├── useFormDate.js
│ │ ├── useFormGroup.js
│ │ └── index.js  👈 统一导出

使用示例:

// composables/form/index.js
export * from "./useFormInput";
export * from "./useFormCheckbox";
export * from "./useFormRadio";
import { useFormInput, useFormCheckbox } from "@/composables/form";

✅ 3. 如何统一收集多个 hooks 的 value?

使用 useFormGroup 工具封装:

当字段太多时,推荐写一个 useFormGroup 来自动统一管理字段。

📦 useFormGroup 封装示例:

// useFormGroup.js
export function useFormGroup(fields) {
  function getValues() {
    const result = {};
    for (const key in fields) {
      result[key] = fields[key].value;
    }
    return result;
  }

  function resetAll() {
    Object.values(fields).forEach((field) => field.reset?.());
  }

  function validateAll() {
    let valid = true;
    Object.values(fields).forEach((field) => {
      if (typeof field.validate === "function") {
        valid = field.validate() && valid;
      }
    });
    return valid;
  }

  return {
    getValues,
    resetAll,
    validateAll,
    fields,
  };
}

使用方式(配合多个 useFormXXX):

import { useFormInput } from "@/composables/useFormInput";
import { useFormCheckbox } from "@/composables/useFormCheckbox";
import { useFormGroup } from "@/composables/useFormGroup";

const username = useFormInput("", { required: true });
const password = useFormInput("", { required: true });
const hobbies = useFormCheckbox([]);

const { getValues, validateAll, resetAll } = useFormGroup({
  username,
  password,
  hobbies,
});

async function onSubmit() {
  if (!validateAll()) {
    console.warn("校验不通过");
    return;
  }

  const formData = getValues(); // 🚀 自动收集所有 value
  await api.submitForm(formData);
}

✅ 4. 如何组合多个 hooks?

随着 hooks 越来越多、功能越来越复杂,如何管理它们之间的依赖关系?

问题本质

  • 数据依赖:A hook 的行为依赖于 B 的结果或状态
  • 调用顺序依赖:必须先初始化某些 hooks,再用其他 hooks
  • 功能组合依赖:多个 hooks 一起组成一个“功能模块”(如一个表单字段)

管理多个 hooks 依赖关系的 5 个常见方式:

1️⃣ 创建组合器函数:

写一个“组合器”,用来统一初始化并组合多个 hooks —— 类似 useFormGroup

export function useUserFormGroup() {
  const username = useFormInput("", { required: true });
  const gender = useFormSelect("", ["男", "女"]);
  const hobbies = useFormCheckbox([]);

  return {
    fields: { username, gender, hobbies },
    getValues() {
      return {
        username: username.value,
        gender: gender.value,
        hobbies: hobbies.value,
      };
    },
  };
}

✅ 所有依赖集中管理 ✅ 表单相关逻辑聚合在一个地方 ✅ 非侵入,保持 hooks 独立

2️⃣ 创建组合器函数:

如果一个 hook 依赖另一个 hook,可以通过参数传入,避免隐式耦合。

export function usePasswordConfirm(passwordField) {
  const confirmPassword = useFormInput("");

  const error = ref("");
  watch(confirmPassword.value, (val) => {
    if (val !== passwordField.value) {
      error.value = "两次输入不一致";
    } else {
      error.value = "";
    }
  });

  return { confirmPassword, error };
}

✅ 明确声明依赖 ✅ 更易读,更易测试

3️⃣ 组合基础能力层(useFormField)继承封装

所有字段 hook 继承同一个低层基础结构,内部可以有一致的 validate/reset 接口。

function useFormField(initialValue, config = {}) {
  const value = ref(initialValue);
  const error = ref("");
  function reset() {
    value.value = initialValue;
    error.value = "";
  }
  function validate() {
    /_..._/;
  }

  return { value, error, reset, validate };
}

然后其他 hooks 复用它:

function useFormInput(...args) {
  return useFormField(...args);
}

✅ 保持一致的结构 ✅ 可以在更高层组合 validateAll/resetAll

4️⃣ 用工厂函数统一注册 hooks(适用于 schema 场景)
function createField(type, config) {
  switch (type) {
    case "input":
      return useFormInput(config.default || "", config);
    case "select":
      return useFormSelect(config.default || "", config.options || []);
  }
}
const schema = [
  { type: "input", name: "username" },
  { type: "select", name: "gender", options: ["男", "女"] },
];

const formFields = {};
schema.forEach((item) => {
  formFields[item.name] = createField(item.type, item);
});
5️⃣ 使用 hooks 容器或注册器(进阶)

如果项目很复杂,你甚至可以实现一个“hook 容器”:

class FormHookRegistry {
  constructor() {
    this.registry = {};
  }

  register(name, hook) {
    this.registry[name] = hook;
  }

  get(name) {
    return this.registry[name];
  }

  getAllValues() {
    return Object.fromEntries(Object.entries(this.registry).map(([key, hook]) => [key, hook.value]));
  }
}

✅ 5. hooks 之间有依赖,如何解耦管理

💡 场景 1:字段依赖字段(如密码确认)

推荐:用参数注入依赖(不要在 hook 内部 import 另一个 hook)

export function usePasswordConfirm(passwordField) {
  const confirmPassword = useFormInput("");
  watch(confirmPassword.value, (val) => {
    if (val !== passwordField.value) {
      error.value = "不一致";
    }
  });
}

💡 场景 2:动态字段(如 schema)

推荐:字段注册 + 工厂方法

function createField(type, config) {
  switch (type) {
    case "input":
      return useFormInput(config.default || "");
    case "select":
      return useFormSelect(config.default || "", config.options);
  }
}

然后:

const formFields = {};
schema.forEach((field) => {
  formFields[field.name] = createField(field.type, field);
});

💡 场景 3:字段共享上下文或 validate/reset

推荐封装 useFormField 基础字段结构,所有字段继承它:

function useFormField(value, config) {
  return {
    value: ref(value),
    error: ref(''),
    validate() { ... },
    reset() { ... }
  };
}

✅ 推荐组合与结构总结

功能Hook 用法
输入框字段useFormInput('', { required: true })
多选字段useFormCheckbox([], { required: true })
字段组合useFormGroup({ username, password })
字段依赖传参usePasswordConfirm(username)
字段类型工厂createField('input', config)
schema 驱动schema.forEach(field => createField(...))

✅ 可扩展方向建议

  • ✅ 异步校验支持(如手机号唯一性)
  • ✅ schema 配置驱动渲染表单
  • ✅ 字段状态管理(disabled / readonly)
  • ✅ 和 pinia 状态缓存联动
  • ✅ 自动生成 validateAll / resetAll / getValues 工具函数