短信预约-IT技能 免费直播动态提醒
在vue3中如何编写一个标准的hooks?
hooks的定义
其实,事实上官方并未管这种方式叫做hooks,而似乎更应该叫做compositions更加确切些,更加符合vue3的设计初衷。由于react的hooks设计理念在前,而vue3的组合式使用也像一个hook钩子挂载vue框架的生命周期中,对此习惯性地称作hooks。
对于onMounted、onUnMounted等响应式API都必须在setup阶段进行同步调用。
图片
要理解 Vue 3 中的 Hooks,需要明白它的本质是一个函数,这个函数可以包含与组件相关的状态和副作用操作。
- 状态是应用中存储的数据,这些数据可以影响组件的外观和行为。在 Vue 3 中,可以使用 ref 和 reactive 来创建状态。
- 副作用操作是指在应用执行过程中会产生外部可观察效果的操作,比如数据获取、订阅事件、定时器等。这些操作可能会影响应用的状态或与外部系统进行交互。
记住:hooks就是特殊的函数,可以在vue组件外部使用,可以访问vue的响应式系统。
vue3中hooks和react的区别
vue3的compositions和react的hooks还是有所区别的,对此官方还特别写了两者的比较,原文如下:
图片
大抵意思如下,Vue Composition API 与 React Hooks 都具有逻辑组合能力,但存在一些重要差异。
React Hooks 的问题:
- 每次组件更新都会重复调用,存在诸多注意事项,可能使经验丰富的开发者也感到困惑,并导致性能优化问题。
- 对调用顺序敏感且不能有条件调用。
- 变量可能因依赖数组不正确而“过时”,开发者需依赖 ESLint 规则确保正确依赖,但规则不够智能,可能过度补偿正确性,遇到边界情况会很麻烦。
- 昂贵的计算需使用 useMemo,且要手动传入正确依赖数组。
- 传递给子组件的事件处理程序默认会导致不必要的子组件更新,需要显式使用 useCallback 和正确的依赖数组,否则可能导致性能问题。陈旧闭包问题结合并发特性,使理解钩子代码何时运行变得困难,处理跨渲染的可变状态也很麻烦。
Vue Composition API 的优势:
hooks工具库vueuse和vue-hooks-plus
对于常用的hooks方法可以单独抽取进行发包成hooks工具。在业务开发中常用的vue hooks方法库有:vueuse和vue-hooks-plus。那么,咱们看看这两个库对于useCounter的封装是什么样的。
vueuse:
// eslint-disable-next-line no-restricted-imports
import { ref, unref } from 'vue-demi'
import type { MaybeRef } from 'vue-demi'
export interface UseCounterOptions {
min?: number
max?: number
}
export function useCounter(initialValue: MaybeRef = 0, options: UseCounterOptions = {}) {
let _initialValue = unref(initialValue)
const count = ref(initialValue)
const {
max = Number.POSITIVE_INFINITY,
min = Number.NEGATIVE_INFINITY,
} = options
const inc = (delta = 1) => count.value = Math.min(max, count.value + delta)
const dec = (delta = 1) => count.value = Math.max(min, count.value - delta)
const get = () => count.value
const set = (val: number) => (count.value = Math.max(min, Math.min(max, val)))
const reset = (val = _initialValue) => {
_initialValue = val
return set(val)
}
return { count, inc, dec, get, set, reset }
}
vue-hooks-plus:
import { Ref, readonly, ref } from 'vue'
import { isNumber } from '../utils' // export const isNumber = (value: unknown): value is number => typeof value === 'number'
export interface UseCounterOptions {
min?: number
max?: number
}
export interface UseCounterActions {
inc: (delta?: number) => void
dec: (delta?: number) => void
set: (value: number | ((c: number) => number)) => void
reset: () => void
}
export type ValueParam = number | ((c: number) => number)
function getTargetValue(val: number, options: UseCounterOptions = {}) {
const { min, max } = options
let target = val
if (isNumber(max)) {
target = Math.min(max, target)
}
if (isNumber(min)) {
target = Math.max(min, target)
}
return target
}
function useCounter(
initialValue = 0,
options: UseCounterOptions = {},
): [Ref, UseCounterActions] {
const { min, max } = options
const current = ref(
getTargetValue(initialValue, {
min,
max,
}),
)
const setValue = (value: ValueParam) => {
const target = isNumber(value) ? value : value(current.value)
current.value = getTargetValue(target, {
max,
min,
})
return current.value
}
const inc = (delta = 1) => {
setValue(c => c + delta)
}
const dec = (delta = 1) => {
setValue(c => c - delta)
}
const set = (value: ValueParam) => {
setValue(value)
}
const reset = () => {
setValue(initialValue)
}
return [
readonly(current),
{
inc,
dec,
set,
reset,
},
]
}
export default useCounter
两段代码都在代码实现上都遵守了上面的hook军规,实现了相似的功能,即创建一个可复用的计数器模块,具有增加、减少、设置特定值和重置等操作,并且都可以配置最小和最大计数范围。
差异点
- 代码细节:
- 第一段代码使用了unref函数来获取初始值的实际数值,第二段代码没有使用这个函数,而是直接在初始化响应式变量时进行处理。
- 第二段代码引入了一个辅助函数isNumber和getTargetValue来确保设置的值在合法范围内,第一段代码在设置值的时候直接进行范围判断,没有单独的辅助函数。
- 返回值处理:
- 第二段代码返回的响应式变量是只读的,这可以提高代码的安全性,防止在组件中意外修改计数器的值;第一段代码没有对返回的响应式变量进行只读处理。
那么什么场景下需要抽取hooks呢?
在以下几种情况下,通常需要抽取 Hooks 方法:
1.逻辑复用当多个组件中存在相同或相似的逻辑时,抽取为 Hooks 可以提高代码的复用性。例如,在多个不同的页面组件中都需要进行数据获取和状态管理,如从服务器获取用户信息并显示加载状态、错误状态等。可以将这些逻辑抽取为一个useFetchUser的 Hooks 方法,这样不同的组件都可以调用这个方法来获取用户信息,避免了重复编写相同的代码。
2.复杂逻辑的封装如果某个组件中有比较复杂的业务逻辑,将其抽取为 Hooks 可以使组件的代码更加清晰和易于维护。比如,一个表单组件中包含了表单验证、数据提交、错误处理等复杂逻辑。可以将这些逻辑分别抽取为useFormValidation、useSubmitForm、useFormErrorHandling等 Hooks 方法,然后在表单组件中组合使用这些 Hooks,使得表单组件的主要逻辑更加专注于用户界面的呈现,而复杂的业务逻辑被封装在 Hooks 中。
3.与特定功能相关的逻辑当有一些特定的功能需要在多个组件中使用时,可以抽取为 Hooks。例如,实现一个主题切换功能,需要管理当前主题状态、切换主题的方法以及保存主题设置到本地存储等逻辑。可以将这些逻辑抽取为useTheme Hooks 方法,方便在不同的组件中切换主题和获取当前主题状态。
4.提高测试性如果某些逻辑在组件中难以进行单元测试,可以将其抽取为 Hooks 以提高测试性。比如,一个组件中的定时器逻辑可能与组件的生命周期紧密耦合,难以单独测试。将定时器相关的逻辑抽取为useTimer Hooks 方法后,可以更容易地对定时器的行为进行单元测试,而不依赖于组件的其他部分。
总之,抽取 Hooks 方法可以提高代码的复用性、可维护性和测试性,当遇到上述情况时,考虑抽取 Hooks 是一个很好的实践。
案例:vue-vben-admin中的usePermission
我们看看关于在业务开发中如何进行hooks抽取封装的案例,vue-vben-admin(https://github.com/vbenjs/vue-vben-admin)是个优秀的中后台管理项目,在项目中设计很复杂也很全面,很多地方都充分体现了vue3的设计思想,也能窥见作者对于vue3源码的深入。
import type { RouteRecordRaw } from 'vue-router';
import { useAppStore } from '/@/store/modules/app';
import { usePermissionStore } from '/@/store/modules/permission';
import { useUserStore } from '/@/store/modules/user';
import { useTabs } from './useTabs';
import { router, resetRouter } from '/@/router';
// import { RootRoute } from '/@/router/routes';
import projectSetting from '/@/settings/projectSetting';
import { PermissionModeEnum } from '/@/enums/appEnum';
import { RoleEnum } from '/@/enums/roleEnum';
import { intersection } from 'lodash-es';
import { isArray } from '/@/utils/is';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
// User permissions related operations
export function usePermission() {
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const { closeAll } = useTabs(router);
async function togglePermissionMode() {
appStore.setProjectConfig({
permissionMode:
appStore.projectConfig?.permissionMode === PermissionModeEnum.BACK
? PermissionModeEnum.ROUTE_MAPPING
: PermissionModeEnum.BACK,
});
location.reload();
}
async function resume() {
const tabStore = useMultipleTabStore();
tabStore.clearCacheTabs();
resetRouter();
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
permissionStore.setLastBuildMenuTime();
closeAll();
}
function hasPermission(value?: RoleEnum | RoleEnum[] | string | string[], def = true): boolean {
// Visible by default
if (!value) {
return def;
}
const permMode = projectSetting.permissionMode;
if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) {
if (!isArray(value)) {
return userStore.getRoleList?.includes(value as RoleEnum);
}
return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0;
}
if (PermissionModeEnum.BACK === permMode) {
const allCodeList = permissionStore.getPermCodeList as string[];
if (!isArray(value)) {
return allCodeList.includes(value);
}
return (intersection(value, allCodeList) as string[]).length > 0;
}
return true;
}
async function changeRole(roles: RoleEnum | RoleEnum[]): Promise {
if (projectSetting.permissionMode !== PermissionModeEnum.ROUTE_MAPPING) {
throw new Error(
'Please switch PermissionModeEnum to ROUTE_MAPPING mode in the configuration to operate!',
);
}
if (!isArray(roles)) {
roles = [roles];
}
userStore.setRoleList(roles);
await resume();
}
async function refreshMenu() {
resume();
}
return { changeRole, hasPermission, togglePermissionMode, refreshMenu };
}
这段代码实现了一个与权限管理相关的模块,主要用于在 Vue 应用中处理用户权限、切换权限模式、重新获取权限资源信息以及刷新菜单等操作。
主要结构和组成部分
- 引入依赖:
- 引入了RouteRecordRaw类型,用于表示路由记录。
- 从特定路径引入了应用的store模块,包括useAppStore、usePermissionStore和useUserStore,用于管理应用状态。
- 引入了自定义的useTabs函数,用于处理标签页相关操作。
- 引入了router和resetRouter,用于操作路由。
- 引入了一些项目设置和工具函数,如projectSetting、PermissionModeEnum、RoleEnum、intersection和isArray。
- 定义**usePermission**函数:
- 该函数内部获取了用户存储、应用存储和权限存储的实例,并调用了useTabs函数获取标签页操作方法。
- togglePermissionMode方法:用于切换权限模式,通过更新应用存储中的项目配置,然后重新加载页面。
- resume方法:用于重置和重新获取权限资源信息。它先清除多标签页存储中的缓存标签,重置路由,重新构建路由并添加到路由实例中,设置最后构建菜单的时间,并关闭所有标签页。
- hasPermission方法:用于判断用户是否具有特定的权限。根据不同的权限模式,检查用户的角色列表或权限代码列表是否包含给定的值。
- changeRole方法:用于切换用户角色。如果当前权限模式不是ROUTE_MAPPING,则抛出错误。如果角色不是数组,则转换为数组,然后更新用户存储中的角色列表,并调用resume方法重新获取权限资源信息。
- refreshMenu方法:用于刷新菜单数据,实际上是调用了resume方法。
- 返回值:
- usePermission函数最后返回一个包含changeRole、hasPermission、togglePermissionMode和refreshMenu方法的对象。
总结
本文主要介绍了 Vue 3 中的组合式 API 及 Hooks 相关内容。首先说明了 Vue 3 组合式 API 中 Hooks 的概念、作用及与 React Hooks 的区别,指出 Vue Composition API 的优势。接着详细阐述了编写自定义 Hooks 时应避免的错误和陷阱,如状态共享、副作用处理、过度依赖外部状态等问题,并给出了自定义 Hooks 函数的示例及单元测试方法。然后对比了两个库(vueuse 和 vue-hooks-plus)对 useCounter 的封装差异。还探讨了抽取 Hooks 的场景,如逻辑复用、复杂逻辑封装等,并以 vue-vben-admin 项目中的权限管理模块为例进行分析。
参考素材:
- https://router.vuejs.org/
- https://inhiblabcore.github.io/docs/hooks/
- https://vueuse.org/
- https://juejin.cn/post/7083401842733875208
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341