Files
Situation-Awareness-Platfor…/docs/Composition API Hooks逻辑复用实现详解.md
cfy666 c1e9a4be83 chore: sync local changes and add documentation
- Update yarn.lock
- Add project implementation docs in docs/
- Add personal internship experience notes in 实习讲解/
2026-06-29 19:47:30 +08:00

29 KiB
Raw Permalink Blame History

Composition API Hooks 逻辑复用 — 面试版

简历原话:"封装 Composition API Hooks 实现逻辑复用"

这篇文档帮你理解项目里到底封装了哪些 Hooks、怎么复用的以及面试时怎么讲。


一、先搞清楚:什么是 Hooks为什么要封装

问题场景

没有 Hooks 的时候:
  页面A需要响应式布局 → 写一堆 width 判断逻辑
  页面B也需要响应式布局 → 再写一遍 width 判断逻辑
  页面C也需要响应式布局 → 又写一遍...

  页面D需要获取仓库权限 → 调 API、存 ref、写 computed
  页面E也需要获取仓库权限 → 同样的代码再写一遍...

痛点:相同的逻辑散落在多个组件里,改了 A 忘改 B代码臃肿、维护困难。

有了 Hooks 之后

  封装一个 usePageResize() Hook
    页面A → const { widthType } = usePageResize()
    页面B → const { widthType } = usePageResize()
    页面C → const { widthType } = usePageResize()
    26 个文件复用同一套逻辑,改 Hook 一处,全局生效

一句话概括:把可复用的响应式逻辑封装成独立的 useXxx() 函数,哪个组件需要就直接调用,避免重复代码。这是 Vue3 Composition API 最核心的设计思想。

项目中 Hooks 的定位

┌────────────────────────────────────────────────────────┐
│                     Vue 组件层                          │
│  Security/index.vue  Ecosystem/index.vue  AI/index.vue │
└────────┬───────────┬───────────┬──────────────────────┘
         │           │           │  调用 useXxx()
         ▼           ▼           ▼
┌────────────────────────────────────────────────────────┐
│                  Hooks 逻辑层30+ 个)                   │
│  usePageResize  useRepoId  usePagination  useLogin ... │
└────────┬───────────┬───────────┬──────────────────────┘
         │           │           │
         ▼           ▼           ▼
┌────────────────────────────────────────────────────────┐
│            Pinia Store / API / Router                   │
│         (底层数据源和基础能力)                            │
└────────────────────────────────────────────────────────┘

二、项目里有 30+ 个 Hooks分六大类

分类 Hooks 数量 核心思想
通用交互 usePageResize、useShowMore、useModel、usePagination、useLazyImport 5+ 跨组件复用的 UI 交互逻辑
表单与认证 useFormInteraction、useLogin、useAccount、useAccModal、useNotice、useLoginCheck 6+ 登录/注册/表单校验的完整流程
数据获取 useReq/useAsync、useRepoInit、useWikiHome/create/detail/history、useUserDashboard、useRepoList 9+ 异步请求 + 状态管理的一站式封装
路由/参数 useRepoId、useOrgId、useBranchName、useTitle、useNav、useRepoPathValidate 6+ URL 参数解析、导航控制
权限控制 useUserAccessLevel、usePagePermission、useIsPrivate 3 权限判断 + computed 封装
其他辅助 useFile、usePopup、useReport、useTimeFormat、useStarFollow、useUserInfo、useDiscussGetUserInfo 等 7+ 文件图标、弹窗管理、埋点上报等

三、每个分类怎么实现的?(面试核心)

3.1 通用交互 Hooks最常用

usePageResize — 响应式断点判断26 个文件复用)

import { computed } from 'vue';
import { useWindowSize } from '@vueuse/core';

const { width } = useWindowSize();

export const usePageResize = () => {
  const widthConfig = { xxl: 1536, xl: 1280, md: 1024, lg: 768 };

  const widthType = computed(() => {
    if (width.value > 1536) return 'xxl';
    if (width.value > 1280) return 'xl';
    if (width.value > 1024) return 'md';
    if (width.value > 768)  return 'lg';
    return 'sm';
  });

  const isMobile = computed(() => width.value <= 1024);

  return { widthType, width, isMobile };
};

怎么用

<script setup>
const { widthType, isMobile } = usePageResize();
// 根据不同断点渲染不同布局
</script>

复用在15 个布局文件 + 4 个态势感知图表组件 + 导航栏、底部栏等,共 26 个文件。

面试话术

"封装了 usePageResize基于 vueuse 的 useWindowSize 响应式监听窗口宽度,定义了 5 个断点xxl/xl/md/lg/sm返回 computed 类型的 widthType 和 isMobile。26 个布局和组件文件都在用,页面布局根据断点自动切换,不需要每个文件自己写一遍判断。"


useModel — v-model 双向绑定封装18 个文件复用)

// 把 v-model 的 prop + emit 封装成统一的写法
export function useModel(props, emits) {
  const vModels = computed({
    get() { return props.modelValue; },
    set(val) { emits('update:modelValue', val); }
  });
  return { vModels };
}

解决什么Vue3 自定义组件里使用 v-model 需要写 props.modelValue + emits('update:modelValue'),每个弹窗/输入组件都写一遍很繁琐。这个 Hook 一行代码搞定。

怎么用

<script setup>
const props = defineProps({ modelValue: Boolean });
const emits = defineEmits(['update:modelValue']);
const { vModels } = useModel(props, emits);
// vModels.value 就是双向绑定的值
</script>

复用在18 个弹窗/表单组件(登录弹窗、标签选择、分支选择等)。


usePagination — 分页器封装

export const usePagination = ({ storageKey, clientType }) => {
  const pager = reactive({
    page: 1, pageSize: 10, total: 0,
    loading: false, isFirstLoad: true,
    showLoading: false, showEmpty: false,
  });

  // 自动判断显示骨架屏还是 loading
  watch(() => pager.loading, (value) => {
    if (value) {
      if (pager.isFirstLoad || pager.total === 0) {
        pager.showLoading = false;  // 首次加载用骨架屏
      } else {
        pager.showLoading = true;   // 切换页用 loading
      }
    } else {
      pager.showEmpty = pager.total === 0;
    }
  });

  return { pager, pageOptions };
};

亮点:自动区分"首次加载"和"翻页加载",首次加载显示骨架屏(结构预览),翻页显示 loading 动画,体验更好。


useShowMore — 内容溢出检测

export const useShowMore = (eleRef) => {
  const isOver = ref(false);    // 内容是否超出容器

  const checkRange = () => {
    isOver.value = eleRef.value.scrollHeight > eleRef.value.clientHeight;
  };

  useResizeObserver(eleRef, checkRange);  // 利用 ResizeObserver 自动检测

  return { isOver, showMore, onShowMore };
};

解决什么:很多卡片展示简介时只能显示 3 行,超出部分要显示"查看更多"。这个 Hook 自动检测内容是否溢出。


useLazyImport — 异步组件懒加载封装

export function useLazyImport(loader, options = {}) {
  return defineAsyncComponent({
    loader,
    loadingComponent: showLoading ? LoadingComponents : '',
    timeout: 2000,    // 2s 超时
    delay: 100,        // 100ms 后才显示 loading避免闪烁
    onError: (_, retry, fail) => {
      Message.error('加载失败');
      fail();
    }
  });
}

解决什么:大组件异步加载时,统一处理 loading 状态、超时、错误重试。项目中有至少 6 个组件通过它注册为异步组件。


3.2 表单与认证 Hooks

useFormInteraction — 表单校验完整流程最复杂332 行)

这是项目中最大的 Hook封装了整个表单交互逻辑

export function useFormInteraction(currentForm, flag, extraStatus) {
  // 状态管理
  const formErrors = reactive({});       // 表单校验错误
  const disabled = ref(true);            // 提交按钮禁用
  const loading = ref(false);            // 提交 loading
  const FormRef = shallowRef(null);      // 表单组件引用
  const status = ref(flag);              // 协议勾选状态

  // 表单字段 change实时校验单个字段
  const handleFormChange = ({ key, errors }) => { ... };

  // 表单 input判断是否禁用提交
  const handleFormInput = (val) => { disabled.value = val; };

  // 表单 submit校验全部 → 通过则回调
  const handleSubmit = async (callback) => {
    const formData = await FormRef.value.ValidateForm();
    if (formData.type === 'success') {
      await callback(formData.forms);  // 执行提交逻辑
    } else {
      catchFormErrors(formData);       // 展示错误
    }
  };

  // 倒计时:发送验证码后 59 秒倒计时
  const handleCountDown = async (conf, callback) => { ... };

  // 第三方登录OAuth 跳转
  const handleAuthLogin = (type) => { ... };

  return {
    FormRef, formErrors, disabled, loading,
    handleFormChange, handleFormInput, handleSubmit,
    handleCountDown, handleAuthLogin, ...
  };
}

封装了什么:表单校验、清错、提交、协议勾选、倒计时、第三方 OAuth 登录——所有登录/注册页的通用逻辑,全部封装在一个 Hook 里。

怎么用

<script setup>
const { FormRef, disabled, loading, handleSubmit, handleFormChange } =
  useFormInteraction(formList);
</script>
<template>
  <Form ref="FormRef" @change="handleFormChange">
    <!-- 表单内容 -->
  </Form>
  <Button :disabled="disabled" :loading="loading" @click="handleSubmit(onLogin)">
    登录
  </Button>
</template>

useLogin / useAccModal / useNotice — 弹窗类 Hooks

这三个 Hook 都基于 usePopup 封装,负责不同类型的弹窗:

// useLogin — 登录弹窗
export function useLogin() {
  const { mount, unMount, isMounted } = usePopup();
  const login = (options) => {
    if (isMounted()) return;  // 防止重复弹出
    mount(LoginModal, {
      onLogin: (userInfo) => { unMount(); RecordInfo(userInfo); },
      onClose: () => { unMount(); }
    });
  };
  return { login };
}

// useAccModal — 快捷登录弹窗(支持 CSDN/Gitee/GitHub
export function useAccModal() {
  const { mount, unMount } = usePopup('g-acc-modal');
  const openModal = () => {
    if (isMounted()) return;
    mount(AccModal, { onConfirm: (type) => { /* OAuth 跳转 */ } });
  };
  return { openModal };
}

// useNotice — 邮箱修改提醒弹窗
export function useNotification() {
  const { mount, unMount } = usePopup('global-notification', document.body);
  const notice = () => {
    mount(NoticeModal, { onConfirm: () => { router.push('/setting/email'); } });
  };
  return { notice };
}

设计模式:三个弹窗 Hook 基于同一个 usePopup 底层能力,各自封装不同的弹窗组件和业务逻辑。调用方只需要一行:

const { login } = useLogin();
login({ type: 'login' });  // 弹出登录窗口

useAccount — 用户信息增删改查

export function useAccount() {
  const userInfo = useAccountStore();
  const { accountInfo } = storeToRefs(userInfo);

  // 记录用户信息(登录时)
  const RecordInfo = (source) => {
    localStorage.setItem('op_access_token', source.op_access_token);
    localStorage.setItem('opUserInfo', JSON.stringify(userInfo));
    saveStatus(true);
    saveAccountInfo(userInfo);
  };

  // 清除用户信息(退出时)
  const RemoveInfo = () => {
    localStorage.removeItem('op_access_token');
    // ... 清除 10+ 个 localStorage key
    saveStatus(false);
    saveAccountInfo();
  };

  return { RecordInfo, RemoveInfo, accountInfo };
}

封装的逻辑Token 存 localStorage + Store 同步 + 登录状态更新。一次调用 RecordInfo 完成三件事13 个文件复用。


3.3 数据获取 Hooks

useReq/useAsync — 异步请求封装(通用能力)

export const useAsync = (fn, params, precondition, map) => {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  watchEffect(() => {
    if (!precondition()) return;      // 前提条件不满足,不发请求
    loading.value = true;
    fn(params).then(res => data.value = map(res))
      .catch(e => error.value = e)
      .finally(() => loading.value = false);
  });

  return { data, error, loading, mutate };  // mutate 手动触发重新请求
};

解决什么:每个页面都要写 data/loading/error 三件套 + try/catch。这个 Hook 一行调用搞定,自动处理 loading 状态和错误捕获。

面试话术

"封装了 useAsync 和 useReq 两个通用请求 Hook。useAsync 接收请求函数、参数、前置条件和结果映射函数,自动管理 data/loading/error 三种状态。前置条件不满足时不会发请求(比如用户未登录时不发),通过 watchEffect 自动追踪依赖变化。还提供了 mutate 方法手动触发重新请求。"


useRepoInit — 仓库首页全部数据一站式初始化243 行)

export const useRepoInit = () => {
  const { repoId } = useRepoId();
  const repoInfo = reactive({...});          // 仓库信息
  const loadingStatus = reactive({           // 5 个模块的加载状态
    profileLoading: true,
    readmeLoading: true,
    eventsLoading: true,
    contributorLoading: true,
    releasesLoading: true,
  });

  const initRepoHeader = async () => {       // 顶部初始化(快)
    initRepoData(); await initNotice();
  };

  const initRepoDashboard = () => {          // 首页初始化(完整)
    initRepoData(); initEvents(); initReadme();
    initContributors(); initRelease();
  };

  return {
    repoInfo, loadingStatus, readmeText, timeData,
    contributorList, releasesList,
    initRepoHeader, initRepoDashboard, ...
  };
};

为什么这样设计仓库首页需要加载仓库信息、README、动态、贡献者、Release 等 5+ 个模块的数据。每个模块都有独立的 loading 状态。Hook 封装了它们的初始化时机header 先加载dashboard 完整加载),页面只管调用 initRepoHeader()initRepoDashboard()


useWikiHome / useWikiCreate / useWikiDetail / useWikiHistory — Wiki 全流程 Hooks

四个 Hook 覆盖 Wiki 的完整生命周期(首页 → 创建 → 详情 → 历史版本),每个 Hook 封装对应的数据获取 + 状态管理:

useWikiHome()    →  首页:获取 Wiki 列表 + Home.md 内容
useWikiCreate()  →  创建/编辑页:表单数据 + 提交 + 删除
useWikiDetail()  →  详情页:内容 + 侧边栏 + 页脚
useWikiHistory() →  历史版本:版本列表 + 分页

设计思想:每个页面一个 Hook页面组件只负责渲染数据逻辑全部在 Hook 里。


useUserDashboard — 用户首页一站式数据初始化272 行)

export const useUserDashboard = () => {
  // 6 个模块的数据
  const userRepoList = ref([]);        // 我的仓库
  const userEventsList = ref({});      // 最近动态
  const recommandRepoList = ref([]);   // 推荐仓库
  const recommandOrgList = ref([]);    // 推荐组织
  const userActivityList = ref([]);    // 关注动态
  const operationData = ref({});       // 运营信息

  // 6 个模块的加载状态
  const loadingStatus = reactive({
    boardLoading: true, myRepoLoading: true,
    eventLoading: true, repoRecmLoading: true,
    orgRecmLoading: true, activityLoading: true,
  });

  const pageInit = async () => {       // 一键初始化
    loadingStatus.boardLoading = true;
    await getUserInfo();
    await getUserRepos();
    loadingStatus.boardLoading = false;
    getRecentEvents();      // 不阻塞首屏
    getOperationData();     // 不阻塞首屏
    recommandRepos();
    recommandOrgs();
    getUserActivity();
  };

  return { userInfo, userRepoList, loadingStatus, pageInit, ... };
};

面试话术

"比如 useUserDashboard 这个 Hook封装了用户首页 6 个模块的数据获取——仓库列表、最近动态、推荐仓库、推荐组织、关注动态、运营信息,每个模块有独立的 loading 状态。pageInit 方法分层加载:先加载用户信息和仓库列表(阻塞首屏),然后并行加载其他 5 个模块(不阻塞首屏)。页面上只需要 const { pageInit } = useUserDashboard(); onMounted(pageInit); 两行代码。"


useRepoList — 用户仓库列表(含 Star 操作)

export const useRepoList = (params) => {
  const repoList = ref([]);
  const allRepoList = ref([]);
  const createdRepoList = ref([]);
  const starredRepoList = ref([]);
  const loading = ref(false);

  // 四类仓库列表 + Star/Unstar 操作
  const toggleRepoStar = ({ id, isStar }) => {
    if (!userInfo.username) {
      emitEvent('login', { triggerType: 'Star' });  // 未登录 → 弹出登录框
      return;
    }
    isStar ? unstarRepo({ repoId: id }) : starRepo({ repoId: id });
    init();  // 操作后重新拉取列表
  };

  return { allRepoList, createdRepoList, starredRepoList, toggleRepoStar, init };
};

封装的逻辑:四种仓库列表 + Star/Unstar 操作 + 未登录时自动弹出登录框。8 个文件复用。


3.4 路由/参数 Hooks

useRepoId — 从 URL 提取仓库 ID30 个文件复用,使用最多)

export const useRepoId = (connector = '%2F', namespace = '') => {
  const route = useRoute();
  const repoId = computed(() => {
    const [, ns, repo, ...rest] = route.path.split('/');
    if (namespace) {
      return [ns, repo, ...rest].join(connector);  // 拼接完整路径
    }
    return [ns, repo].join(connector);               // namespace/repo
  });
  return { repoId };
};

解决什么:项目中大量页面需要从 URL 中提取仓库的 namespace/repoNamegitcode/MyProject。30 个文件都在用这个 Hook不需要每个文件自己解析 URL。

类似的还有useOrgId(提取组织 IDuseBranchName(提取分支名+编码)。


useTitle — 页面标题设置

export const usePageTitle = (title, name) => {
  if (title && name) {
    pageTitle.value = `${title}`;
  } else {
    pageTitle.value = title || (name ? `${name} - GitCode` : 'Tcode');
  }
};

路由守卫中每个页面切换时调用,自动更新浏览器标签页标题。


3.5 权限控制 Hooks

useUserAccessLevel — 权限查询 + 角色判断

export function useUserAccessLevel({ type = 'org', targetId = '' } = {}) {
  const access_level = ref(0);

  async function getPermission(id) {
    if (type === 'org') {
      const res = await getOrgPermission({ group_id: id });
      access_level.value = res.data.data.my_role?.access_level || 0;
    } else {
      const res = await getRepoPermission({ repo_id: id });
      access_level.value = res.data.data.access_level || 0;
    }
  }

  // 三种角色自动 computed
  const isAdmin = computed(() => access_level.value >= admin);       // 50
  const isDeveloper = computed(() => access_level.value >= developer); // 30
  const isVisitor = computed(() => access_level.value >= visitor);     // 10

  return { access_level, getPermission, isAdmin, isDeveloper, isVisitor };
}

解决什么:查询用户在当前组织/仓库的权限后,自动算出三个布尔角色值。配合 Store 里的权限 computed页面只需 if (isAdmin) 判断是否显示管理按钮。

usePagePermission — 页面级权限控制

export function usePagePermission(access_level, isPrivate) {
  // 根据权限等级和是否私有,返回哪些菜单/操作可用
}

3.6 其他辅助 Hooks

useFile — 文件类型识别(图标 + 语言高亮)

export function useFile() {
  // 根据文件名后缀返回对应图标名
  const getFileIcon = (filename) => {
    const format = filename?.split('.').pop();
    if (/^(zip|rar|7z)$/i.test(format))  return 'gt-file-zip-c';
    if (/^(png|jpg|jpeg|gif)$/i.test(format)) return 'gt-picture-c';
    if (/^(js|ts|vue|py)$/i.test(format)) return 'gt-file-code-c';
    return 'gt-file-c';
  };

  // 根据文件名返回代码高亮语言
  const getFileLanguage = (filename) => {
    const map = { js: 'javascript', ts: 'typescript', vue: 'html', py: 'python' };
    return map[format] || format;
  };

  return { getIcon, getFileLanguage, getFileFormat };
}

useStarFollow — 关注/粉丝操作

export const useStarFollow = (params, auto) => {
  const toggleStar = (username, followedUsername, follow) => {
    if (!username) {
      emitEvent('login', { triggerType: '关注用户' });  // 未登录→弹出登录
      return;
    }
    follow ? followUser(...) : unfollowUser(...);
    microApp.setData('user-center', { type: 'user_hasFollowed_update', ... });
  };
  return { fanCount, followCount, hasFollowed, toggleStar };
};

封装了什么:关注/取消关注的 API 调用 + 状态更新 + 微前端数据同步 + 未登录自动弹登录框。


useReport — 埋点上报

export const useReport = (eventID, eventParams, headers) => {
  // 统一埋点上报逻辑
};

usePopup — 弹窗管理器(底层能力)

被 useLogin、useAccModal、useNotice 三个 Hook 依赖的底层弹窗管理:

export function usePopup(className?, rootElement?) {
  const mount = (component, props) => { /* 挂载弹窗到 DOM */ };
  const unMount = () => { /* 卸载弹窗 */ };
  const isMounted = () => { /* 是否已挂载 */ };
  return { mount, unMount, isMounted };
}

四、Hooks 之间的层次关系

底层能力(被其他 Hook 依赖)
├── usePopup       → 弹窗挂载/卸载
├── useReq/useAsync → 通用异步请求封装
├── useRepoId      → URL 参数解析
└── useModel       → v-model 封装

中间能力(组合底层能力)
├── useLogin       → 基于 usePopup
├── useAccModal    → 基于 usePopup
├── useNotice      → 基于 usePopup
├── useFormInteraction → 基于 useAccount + useRouter
├── useUserAccessLevel → 基于 useUserInfo

业务能力(组合中间能力)
├── useRepoInit    → 基于 useRepoId
├── useWikiHome    → 基于 useRepoId + useRouter
├── useRepoList    → 基于 API 调用
├── useUserDashboard → 基于 6 个 API 调用
├── useStarFollow  → 基于 API + eventBus + microApp
└── usePageResize  → 基于 useWindowSizevueuse

面试话术

"我设计的 Hooks 是有层次的。底层 Hooks 提供通用能力(如 usePopup 管理弹窗生命周期、useAsync 封装异步请求);中间层 Hooks 基于底层组合出业务场景(如 useLogin 基于 usePopup 封装登录弹窗流程);最上层是业务 Hooks组件里一行代码调用就能获得完整的数据和操作方法。"


五、整体架构总结

30+ 个 Hooks 在项目中的定位:

  可复用逻辑提取为 Hooks
          ↓
  ┌───────────────┐
  │ 通用交互 (5+)  │  usePageResize(26文件) useModel(18文件) usePagination useShowMore useLazyImport
  ├───────────────┤
  │ 表单认证 (6+)  │  useFormInteraction(8文件) useLogin useAccount(13文件) useAccModal useNotice
  ├───────────────┤
  │ 数据获取 (9+)  │  useAsync/useReq useRepoInit(7文件) useWikiHome/create/detail/history
  │               │  useUserDashboard useRepoList(8文件) useDiscussGetUserInfo
  ├───────────────┤
  │ 路由参数 (6+)  │  useRepoId(30文件) useOrgId useBranchName useTitle useNav useRepoPathValidate
  ├───────────────┤
  │ 权限控制 (3)   │  useUserAccessLevel usePagePermission useIsPrivate
  ├───────────────┤
  │ 其他辅助 (7+)  │  useFile usePopup useReport useTimeFormat useStarFollow(4文件) useUserInfo
  └───────────────┘
          ↓
    Vue 组件层40+ 组件调用)

六、面试问答准备

Q1"封装 Composition API Hooks 实现逻辑复用"具体做了什么?

项目里封装了 30 多个 Hooks分为六大类。最核心的几个usePageResize 把响应式断点判断封装成 Hook26 个布局文件复用useRepoId 从 URL 提取仓库 ID30 个文件复用useFormInteraction 封装了完整的表单校验流程332 行8 个登录/注册页共享同一套校验逻辑useUserDashboard 封装了用户首页 6 个模块的数据初始化页面只需两行代码完成全部数据加载。Hooks 之间还有层次关系——底层 HooksusePopup、useAsync提供通用能力中间层组合底层能力业务层直接给组件用。

Q2Hooks 和之前写的工具函数有什么区别?

工具函数是无状态的纯函数输入→输出Hooks 是有状态的(包含 ref、computed、watch。比如 useFormInteraction 内部管理了 formErrors、disabled、loading 等 6 个状态,还监听了表单 change 事件自动校验。如果写成工具函数这些状态管理就要在每个页面里重复写。Hooks 把"响应式状态 + 操作逻辑"封装在一起,是 Vue3 Composition API 的核心优势。

Q3Hooks 怎么保证类型安全?

所有 Hooks 都用 TypeScript 写,返回值和参数都有类型定义。比如 usePagination 接收 {storageKey, clientType} 接口类型useRepoList 接收包含 profile/all/created/starred 四种子参数的类型。组件调用时 VSCode 有完整的智能提示。

Q4有没有用过 VueUse 的 Hooks

用过useWindowSize、useResizeObserver、useTitle 等都是从 @vueuse/core 引入的。但项目不只是用第三方 Hooks更关键的是基于 VueUse 做二次封装——比如 usePageResize 基于 useWindowSize 封装了断点判断逻辑useShowMore 基于 useResizeObserver 封装了内容溢出检测。既用了社区的最佳实践,又做了业务定制。

Q530 多个 Hooks 怎么组织的?会不会管理混乱?

按职责分目录:通用的放在 src/utils/hooks/30 个),特定业务域放在对应的 views 目录下(如 src/views/User/hooks/ 放用户相关的 7 个 Hook布局相关的放在 src/layouts/hooks/。调用时能直观看出 Hook 的作用域。

Q6Hooks 和 Pinia Store 怎么分工?

Hooks 和 Store 的分工很明确。Store 存跨组件共享的持久状态用户信息、仓库信息Hooks 封装的是"行为逻辑"——怎么获取数据、怎么校验表单、怎么处理交互。Hooks 经常调用 Store 来读写状态,但它们不替代 Store。比如 useAccount Hook 既调用 Store 的 saveAccountInfo又操作 localStorage把"用户信息存储到 Store + 持久化到 localStorage + 更新登录状态"三步封装成一步 RecordInfo。


七、关键数字(面试时用)

数据 数字
Hooks 总数 30+ 个
使用最多的 Hook useRepoId30 个文件、61 处引用)
第二常用 usePageResize26 个文件、51 处引用)
第三常用 useModel18 个文件、35 处引用)
最大的 Hook useFormInteraction332 行)
业务最全的 Hook useUserDashboard6 个模块数据初始化)
弹窗类 Hooks 3 个useLogin/useAccModal/useNotice共用 usePopup
Wiki 全家桶 4 个Home/Create/Detail/History
权限类 Hooks 2 个useUserAccessLevel/usePagePermission
基于 @vueuse/core 封装的 3 个useWindowSize/useResizeObserver/useTitle

八、涉及的源码文件(需要看的时候查)

做什么 文件在哪
通用交互 Hooks5 个) src/utils/hooks/usePageResize.tsuseShowMore.tsuseModel.tsusePagination.tsuseLazy.ts
表单与认证 Hooks6 个) src/utils/hooks/useForm.tsuseLogin.tsuseAccount.tsuseAccModal.tsuseNotice.tsuseLoginCheck.ts
数据获取 Hooks9 个) src/utils/hooks/useReq.tsuseRepoInit.tsuseWikiInit.tsuseIssueTemplate.tssrc/api/discussion/hook.ts
用户域 Hooks7 个) src/views/User/hooks/useUserDashboard.tsuseRepoList.tsuseStarFollow.tsuseContributes.tsuseTimelineActivities.tsuseIsPrivate.tsuseUserLang.ts
路由/参数 Hooks6 个) src/utils/hooks/useRepoId.tsuseOrgId.tsusebranchName.tsuseTitle.tsuseNav.tsuseRepoPathValidate.ts
权限控制 Hooks3 个) src/utils/hooks/useUserAccessLevel.tssrc/layouts/hooks/usePagePermission.tssrc/views/User/hooks/useIsPrivate.ts
其他辅助 Hooks7 个) src/utils/hooks/useFile.tsusePopup.tsuseReport.tsuseTimeFormat.tsuseUserInfo.tsuseRepoHeaderInit.tsuseBranchOptions.ts
组织域 Hooks src/views/Org/hooks/orgInfo.ts
弹窗底层能力 src/utils/hooks/usePopup.ts(被 useLogin/useAccModal/useNotice 依赖)