# 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 个文件复用) ```typescript 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 }; }; ``` **怎么用**: ```vue ``` **复用在**:15 个布局文件 + 4 个态势感知图表组件 + 导航栏、底部栏等,共 26 个文件。 **面试话术**: > "封装了 usePageResize,基于 vueuse 的 useWindowSize 响应式监听窗口宽度,定义了 5 个断点(xxl/xl/md/lg/sm),返回 computed 类型的 widthType 和 isMobile。26 个布局和组件文件都在用,页面布局根据断点自动切换,不需要每个文件自己写一遍判断。" --- #### useModel — v-model 双向绑定封装(18 个文件复用) ```typescript // 把 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 一行代码搞定。 **怎么用**: ```vue ``` **复用在**:18 个弹窗/表单组件(登录弹窗、标签选择、分支选择等)。 --- #### usePagination — 分页器封装 ```typescript 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 — 内容溢出检测 ```typescript 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 — 异步组件懒加载封装 ```typescript 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,封装了整个表单交互逻辑: ```typescript 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 里。 **怎么用**: ```vue ``` --- #### useLogin / useAccModal / useNotice — 弹窗类 Hooks 这三个 Hook 都基于 `usePopup` 封装,负责不同类型的弹窗: ```typescript // 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` 底层能力,各自封装不同的弹窗组件和业务逻辑。调用方只需要一行: ```typescript const { login } = useLogin(); login({ type: 'login' }); // 弹出登录窗口 ``` --- #### useAccount — 用户信息增删改查 ```typescript 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 — 异步请求封装(通用能力) ```typescript 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 行) ```typescript 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 行) ```typescript 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 操作) ```typescript 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 提取仓库 ID(30 个文件复用,使用最多) ```typescript 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/repoName(如 `gitcode/MyProject`)。30 个文件都在用这个 Hook,不需要每个文件自己解析 URL。 **类似的还有**:`useOrgId`(提取组织 ID)、`useBranchName`(提取分支名+编码)。 --- #### useTitle — 页面标题设置 ```typescript export const usePageTitle = (title, name) => { if (title && name) { pageTitle.value = `${title}`; } else { pageTitle.value = title || (name ? `${name} - GitCode` : 'Tcode'); } }; ``` 路由守卫中每个页面切换时调用,自动更新浏览器标签页标题。 --- ### 3.5 权限控制 Hooks #### useUserAccessLevel — 权限查询 + 角色判断 ```typescript 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 — 页面级权限控制 ```typescript export function usePagePermission(access_level, isPrivate) { // 根据权限等级和是否私有,返回哪些菜单/操作可用 } ``` --- ### 3.6 其他辅助 Hooks #### useFile — 文件类型识别(图标 + 语言高亮) ```typescript 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 — 关注/粉丝操作 ```typescript 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 — 埋点上报 ```typescript export const useReport = (eventID, eventParams, headers) => { // 统一埋点上报逻辑 }; ``` #### usePopup — 弹窗管理器(底层能力) 被 useLogin、useAccModal、useNotice 三个 Hook 依赖的底层弹窗管理: ```typescript 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 → 基于 useWindowSize(vueuse) ``` **面试话术**: > "我设计的 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 把响应式断点判断封装成 Hook,26 个布局文件复用;useRepoId 从 URL 提取仓库 ID,30 个文件复用;useFormInteraction 封装了完整的表单校验流程(332 行),8 个登录/注册页共享同一套校验逻辑;useUserDashboard 封装了用户首页 6 个模块的数据初始化,页面只需两行代码完成全部数据加载。Hooks 之间还有层次关系——底层 Hooks(usePopup、useAsync)提供通用能力,中间层组合底层能力,业务层直接给组件用。 ### Q2:Hooks 和之前写的工具函数有什么区别? > 工具函数是无状态的(纯函数,输入→输出),Hooks 是有状态的(包含 ref、computed、watch)。比如 useFormInteraction 内部管理了 formErrors、disabled、loading 等 6 个状态,还监听了表单 change 事件自动校验。如果写成工具函数,这些状态管理就要在每个页面里重复写。Hooks 把"响应式状态 + 操作逻辑"封装在一起,是 Vue3 Composition API 的核心优势。 ### Q3:Hooks 怎么保证类型安全? > 所有 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 封装了内容溢出检测。既用了社区的最佳实践,又做了业务定制。 ### Q5:30 多个 Hooks 怎么组织的?会不会管理混乱? > 按职责分目录:通用的放在 `src/utils/hooks/`(30 个),特定业务域放在对应的 views 目录下(如 `src/views/User/hooks/` 放用户相关的 7 个 Hook),布局相关的放在 `src/layouts/hooks/`。调用时能直观看出 Hook 的作用域。 ### Q6:Hooks 和 Pinia Store 怎么分工? > Hooks 和 Store 的分工很明确。Store 存跨组件共享的持久状态(用户信息、仓库信息),Hooks 封装的是"行为逻辑"——怎么获取数据、怎么校验表单、怎么处理交互。Hooks 经常调用 Store 来读写状态,但它们不替代 Store。比如 useAccount Hook 既调用 Store 的 saveAccountInfo,又操作 localStorage,把"用户信息存储到 Store + 持久化到 localStorage + 更新登录状态"三步封装成一步 RecordInfo。 --- ## 七、关键数字(面试时用) | 数据 | 数字 | |------|------| | Hooks 总数 | 30+ 个 | | 使用最多的 Hook | useRepoId(30 个文件、61 处引用) | | 第二常用 | usePageResize(26 个文件、51 处引用) | | 第三常用 | useModel(18 个文件、35 处引用) | | 最大的 Hook | useFormInteraction(332 行) | | 业务最全的 Hook | useUserDashboard(6 个模块数据初始化) | | 弹窗类 Hooks | 3 个(useLogin/useAccModal/useNotice,共用 usePopup) | | Wiki 全家桶 | 4 个(Home/Create/Detail/History) | | 权限类 Hooks | 2 个(useUserAccessLevel/usePagePermission) | | 基于 @vueuse/core 封装的 | 3 个(useWindowSize/useResizeObserver/useTitle) | --- ## 八、涉及的源码文件(需要看的时候查) | 做什么 | 文件在哪 | |--------|----------| | 通用交互 Hooks(5 个) | `src/utils/hooks/usePageResize.ts`、`useShowMore.ts`、`useModel.ts`、`usePagination.ts`、`useLazy.ts` | | 表单与认证 Hooks(6 个) | `src/utils/hooks/useForm.ts`、`useLogin.ts`、`useAccount.ts`、`useAccModal.ts`、`useNotice.ts`、`useLoginCheck.ts` | | 数据获取 Hooks(9 个) | `src/utils/hooks/useReq.ts`、`useRepoInit.ts`、`useWikiInit.ts`、`useIssueTemplate.ts`、`src/api/discussion/hook.ts` | | 用户域 Hooks(7 个) | `src/views/User/hooks/useUserDashboard.ts`、`useRepoList.ts`、`useStarFollow.ts`、`useContributes.ts`、`useTimelineActivities.ts`、`useIsPrivate.ts`、`useUserLang.ts` | | 路由/参数 Hooks(6 个) | `src/utils/hooks/useRepoId.ts`、`useOrgId.ts`、`usebranchName.ts`、`useTitle.ts`、`useNav.ts`、`useRepoPathValidate.ts` | | 权限控制 Hooks(3 个) | `src/utils/hooks/useUserAccessLevel.ts`、`src/layouts/hooks/usePagePermission.ts`、`src/views/User/hooks/useIsPrivate.ts` | | 其他辅助 Hooks(7 个) | `src/utils/hooks/useFile.ts`、`usePopup.ts`、`useReport.ts`、`useTimeFormat.ts`、`useUserInfo.ts`、`useRepoHeaderInit.ts`、`useBranchOptions.ts` | | 组织域 Hooks | `src/views/Org/hooks/orgInfo.ts` | | 弹窗底层能力 | `src/utils/hooks/usePopup.ts`(被 useLogin/useAccModal/useNotice 依赖) |