- Update yarn.lock - Add project implementation docs in docs/ - Add personal internship experience notes in 实习讲解/
29 KiB
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 提取仓库 ID(30 个文件复用,使用最多)
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 — 页面标题设置
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 → 基于 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 依赖) |