本文是 ahooks 源码系列的第二篇,往期文章:
本文主要解读 useEventTarget、useExternal、useTitle、useFavicon、useFullscreen、useHover 源码实现
常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,支持自定义值转换和重置功能。
官方文档
export interface Options {
initialValue?: T; // 初始值
transformer?: (value: U) => T; // 自定义回调值的转化
}
复制代码
import React from 'react';
import { useEventTarget } from 'ahooks';
export default () => {
const [value, { reset, onChange }] = useEventTarget({ initialValue: 'this is initial value' });
return (
);
};
复制代码
适用于较为简单的表单受控控件(如 input 输入框)管理
这个实现比较简单,这里结尾代码有个as const,它表示强制 TypeScript 将变量或表达式的类型视为不可变的
具体可以看下这篇文章: 杀手级的 TypeScript 功能:const 断言
function useEventTarget(options?: Options) {
const { initialValue, transformer } = options || {};
const [value, setValue] = useState(initialValue);
const transformerRef = useLatest(transformer);
const reset = useCallback(() => setValue(initialValue), []);
const onChange = useCallback((e: EventTarget) => {
const _value = e.target.value;
if (isFunction(transformerRef.current)) {
return setValue(transformerRef.current(_value));
}
// no transformer => U and T should be the same
return setValue(_value as unknown as T);
}, []);
return [
value,
{
onChange,
reset,
},
] as const; // 将数组变为只读元组,可以确保其内容不会在其声明和函数调用之间发生变化
}
复制代码
完整源码
动态注入 JS 或 CSS 资源,useExternal 可以保证资源全局唯一。
官方文档
import React from 'react';
import { useExternal } from 'ahooks';
export default () => {
const status = useExternal('/useExternal/test-external-script.js', {
js: {
async: true,
},
});
return (
<>
Status: {status}
Response: {status === 'ready' ? window.TEST_SCRIPT?.start() : '-'}
>
);
};
复制代码
原理:通过 script 标签加载 JS 资源 / 创建 link 标签加载 CSS 资源,再通过创建标签返回的 Element 元素监听 load 和 error 事件 获取加载状态
主体实现结构:
export interface Options {
type?: 'js' | 'css';
js?: Partial;
css?: Partial;
}
const useExternal = (path?: string, options?: Options) => {
const [status, setStatus] = useState(path ? 'loading' : 'unset');
const ref = useRef();
useEffect(() => {
if (!path) {
setStatus('unset');
return;
}
const pathname = path.replace(/[|#].*$/, '');
if (options?.type === 'css' || (!options?.type && /(^css!|.css$)/.test(pathname))) {
const result = loadCss(path, options?.css);
} else if (options?.type === 'js' || (!options?.type && /(^js!|.js$)/.test(pathname))) {
const result = loadScript(path, options?.js);
} else {
}
if (!ref.current) {
return;
}
const handler = (event: Event) => {};
ref.current.addEventListener('load', handler);
ref.current.addEventListener('error', handler);
return () => {
// 移除监听 & 清除操作
};
}, [path]);
return status;
};
复制代码
主函数中判断加载 CSS 还是 JS 资源:
const pathname = path.replace(/[|#].*$/, '');
if (options?.type === 'css' || (!options?.type && /(^css!|.css$)/.test(pathname))) {
const result = loadCss(path, options?.css); // 加载 css 资源并返回结果
ref.current = result.ref; // 返回创建 link 标签返回的 Element 元素,用于后续绑定监听 load 和 error事件
setStatus(result.status); // 设置加载状态
} else if (options?.type === 'js' || (!options?.type && /(^js!|.js$)/.test(pathname))) {
const result = loadScript(path, options?.js);
ref.current = result.ref;
setStatus(result.status);
} else {
// do nothing
console.error(
"Cannot infer the type of external resource, and please provide a type ('js' | 'css'). " +
'Refer to the https://ahooks.js.org/hooks/dom/use-external/#options',
);
}
复制代码
loadCss 方法:
往 HTML 标签上添加任意以 "data-" 为前缀来设置我们需要的自定义属性,可以进行一些数据的存放
const loadCss = (path: string, props = {}): loadResult => {
const css = document.querySelector(`link[href="${path}"]`);
// 不存在则创建
if (!css) {
const newCss = document.createElement('link');
newCss.rel = 'stylesheet';
newCss.href = path;
// 设置 link 标签支持的属性
Object.keys(props).forEach((key) => {
newCss[key] = props[key];
});
// IE9+
const isLegacyIECss = 'hideFocus' in newCss;
// use preload in IE Edge (to detect load errors)
if (isLegacyIECss && newCss.relList) {
newCss.rel = 'preload';
newCss.as = 'style';
}
// 设置自定义属性[data-status]为loading状态
newCss.setAttribute('data-status', 'loading');
// 添加到 head 标签
document.head.appendChild(newCss);
// 标签路径匹配存在则直接返回现有结果,保证全局资源全局唯一
return {
ref: newCss,
status: 'loading',
};
}
// 如果标签存在则直接返回,并取 data-status 中的值
return {
ref: css,
status: (css.getAttribute('data-status') as Status) || 'ready',
};
}
复制代码
loadScript 方法的实现也类似:
const loadScript = (path: string, props = {}): loadResult => {
const script = document.querySelector(`script[src="${path}"]`);
if (!script) {
const newScript = document.createElement('script');
newScript.src = path;
// 设置 script 标签支持的属性
Object.keys(props).forEach((key) => {
newScript[key] = props[key];
});
newScript.setAttribute('data-status', 'loading');
// 添加到 body 标签
document.body.appendChild(newScript);
return {
ref: newScript,
status: 'loading',
};
}
return {
ref: script,
status: (script.getAttribute('data-status') as Status) || 'ready',
};
};
复制代码
前面获取到 Element 元素后,监听 Element 的 load 和 error 事件,判断其加载状态并更新状态
const handler = (event: Event) => {
const targetStatus = event.type === 'load' ? 'ready' : 'error';
ref.current?.setAttribute('data-status', targetStatus);
setStatus(targetStatus);
};
ref.current.addEventListener('load', handler);
ref.current.addEventListener('error', handler);
复制代码
完整源码
用于设置页面标题。
官方文档
import React from 'react';
import { useTitle } from 'ahooks';
export default () => {
useTitle('Page Title');
return (
Set title of the page.
);
};
复制代码
当进入某页面需要改浏览器 Tab 中展示的标题时
这个实现比较简单
const DEFAULT_OPTIONS: Options = {
restoreOnUnmount: false, // 组件卸载时,是否恢复上一个页面标题
};
function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
const titleRef = useRef(isBrowser ? document.title : '');
useEffect(() => {
document.title = title;
}, [title]);
useUnmount(() => {
if (options.restoreOnUnmount) {
// 组件卸载时,恢复上一个页面标题
document.title = titleRef.current;
}
});
}
复制代码
如果项目中我们自己实现的话,有个需要注意的地方,不要把document.title = title;写在外层,要写在 useEffect 里面,具体见该文:检测意外的副作用
完整源码
设置页面的 favicon。
官方文档
favicon 指显示在浏览器收藏夹、地址栏和标签标题前面的个性化图标
import React, { useState } from 'react';
import { useFavicon } from 'ahooks';
export const DEFAULT_FAVICON_URL = 'https://ahooks.js.org/simple-logo.svg';
export const GOOGLE_FAVICON_URL = 'https://www.google.com/favicon.ico';
export default () => {
const [url, setUrl] = useState(DEFAULT_FAVICON_URL);
useFavicon(url);
return (
<>
Current Favicon: {url}
>
);
};
复制代码
当需要改浏览器 Tab 中展示的图标 icon 时
原理:通过 link 标签设置 favicon
更多 favicon 知识可见: 详细介绍 HTML favicon 尺寸 格式 制作等相关知识
源代码仅支持图标四种类型:
const ImgTypeMap = {
SVG: 'image/svg+xml',
ICO: 'image/x-icon',
GIF: 'image/gif',
PNG: 'image/png',
};
type ImgTypes = keyof typeof ImgTypeMap;
复制代码
const useFavicon = (href: string) => {
useEffect(() => {
if (!href) return;
const cutUrl = href.split('.');
// 取出文件后缀
const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes;
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = ImgTypeMap[imgSuffix];
// 指定被链接资源的地址
link.href = href;
// rel 属性用于指定当前文档与被链接文档的关系,直接使用 rel=icon 就可以,源码下方的 `shortcut icon` 是一种过时的用法
link.rel = 'shortcut icon';
document.getElementsByTagName('head')[0].appendChild(link);
}, [href]);
};
复制代码
完整源码
管理 DOM 全屏的 Hook。
官方文档
import React, { useRef } from 'react';
import { useFullscreen } from 'ahooks';
export default () => {
const ref = useRef(null);
const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] = useFullscreen(ref);
return (
{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}
);
};
复制代码
useFullscreen 内部主要是依赖 screenfull 这个库进行实现的。
screenfull 对各种浏览器全屏的 API 进行封装,兼容性好。
下面是该库的 API:
看看 useFullscreen 的导出值:
return [
state,
{
enterFullscreen: useMemoizedFn(enterFullscreen),
exitFullscreen: useMemoizedFn(exitFullscreen),
toggleFullscreen: useMemoizedFn(toggleFullscreen),
isEnabled: screenfull.isEnabled,
},
] as const;
复制代码
那么实现的方向就比较简单了:
三个方法的实现:
// 进入全屏方法
const enterFullscreen = () => {
const el = getTargetElement(target);
if (!el) {
return;
}
if (screenfull.isEnabled) {
try {
screenfull.request(el);
screenfull.on('change', onChange);
} catch (error) {
console.error(error);
}
}
};
// 退出全屏方法
const exitFullscreen = () => {
const el = getTargetElement(target);
if (screenfull.isEnabled && screenfull.element === el) {
screenfull.exit();
}
};
const toggleFullscreen = () => {
if (state) {
exitFullscreen();
} else {
enterFullscreen();
}
};
复制代码
onChange 方法
const onChange = () => {
if (screenfull.isEnabled) {
const el = getTargetElement(target);
// screenfull.element:当前元素以全屏模式显示
if (!screenfull.element) {
// 退出全屏
onExitRef.current?.();
setState(false);
screenfull.off('change', onChange); // 卸载 change 事件
} else {
// 全屏模式展示
const isFullscreen = screenfull.element === el; // 判断当前全屏元素是否为目标元素
if (isFullscreen) {
onEnterRef.current?.();
} else {
onExitRef.current?.();
}
setState(isFullscreen);
}
}
};
复制代码
上方onChange以及exitFullscreen执行退出全屏前有行需要判断的代码注意下,具体原因可以看下修复 useFullScreen 当全屏后,子元素重复全屏和退出全屏操作后父元素也会退出全屏
// 判断当前全屏元素是否为目标元素,支持对多个元素同时全屏
const isFullscreen = screenfull.element === el;
复制代码
screenfull.element 的实现:
element: {
enumerable: true,
get: () => document[nativeAPI.fullscreenElement] ?? undefined,
},
复制代码
完整源码
监听 DOM 元素是否有鼠标悬停。
官方文档
import React, { useRef } from 'react';
import { useHover } from 'ahooks';
export default () => {
const ref = useRef(null);
const isHovering = useHover(ref);
return {isHovering ? 'hover' : 'leaveHover'};
};
复制代码
扩展下几个鼠标事件的区别:
原理是监听 mouseenter 触发 onEnter 回调,切换状态为 true;监听 mouseleave 触发 onLeave回调,切换状态为 false。
完整实现:
export interface Options {
onEnter?: () => void;
onLeave?: () => void;
onChange?: (isHovering: boolean) => void;
}
export default (target: BasicTarget, options?: Options): boolean => {
const { onEnter, onLeave, onChange } = options || {};
// useBoolean:优雅的管理 boolean 状态的 Hook
const [state, { setTrue, setFalse }] = useBoolean(false);
// 监听 mouseenter 判断有鼠标进入目标元素
useEventListener(
'mouseenter',
() => {
onEnter?.();
setTrue();
onChange?.(true);
},
{
target,
},
);
// 监听 mouseleave 判断有鼠标是否移出目标元素
useEventListener(
'mouseleave',
() => {
onLeave?.();
setFalse();
onChange?.(false);
},
{
target,
},
);
return state;
};
复制代码
完整源码
页面更新:2024-02-25
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号