前言
本文是 ahooks 源码系列的第三篇,往期文章:
- 【解读 ahooks 源码系列】(开篇)如何获取和监听 DOM 元素
- 【解读 ahooks 源码系列】DOM篇(一)
本文次要解读 useEventTarget
、useExternal
、useTitle
、useFavicon
、useFullscreen
、useHover
源码实现
useEventTarget
常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,反对自定义值转换和重置性能。
官网文档
export interface Options<T, U> { 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 ( <div> <input value={value} onChange={onChange} style={{ width: 200, marginRight: 20 }} /> <button type="button" onClick={reset}> reset </button> </div> );};
应用场景
实用于较为简单的表单受控控件(如 input 输入框)治理
实现思路
- 监听表单的 onChange 事件,拿到值后更新 value 值
- 反对自定义回调值的转化,对外裸露 value 值、onChange 和 reset 办法
外围实现
这个实现比较简单,这里结尾代码有个as const
,它示意强制 TypeScript 将变量或表达式的类型视为不可变的
具体能够看下这篇文章: 杀手级的 TypeScript 性能:const 断言
function useEventTarget<T, U = T>(options?: Options<T, U>) { const { initialValue, transformer } = options || {}; const [value, setValue] = useState(initialValue); const transformerRef = useLatest(transformer); const reset = useCallback(() => setValue(initialValue), []); const onChange = useCallback((e: EventTarget<U>) => { 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; // 将数组变为只读元组,能够确保其内容不会在其申明和函数调用之间发生变化}
残缺源码
useExternal
动静注入 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 ( <> <p> Status: <b>{status}</b> </p> <p> Response: <i>{status === 'ready' ? window.TEST_SCRIPT?.start() : '-'}</i> </p> </> );};
实现思路
原理:通过 script 标签加载 JS 资源 / 创立 link 标签加载 CSS 资源,再通过创立标签返回的 Element 元素监听 load 和 error 事件 获取加载状态
- 正则判断传入的门路 path 是 JS 还是 CSS
- 加载 CSS/JS:创立 link/script 标签传入 path,反对传入 link/script 标签反对的属性,增加到 head/body 中,并返回 Element 元素与加载状态;这里需判断标签门路匹配是否存在,存在则返回上一次后果,以保障资源全局惟一
- 利用创立标签返回的 Element 元素监听 load 和 error 事件,并在回调中扭转加载状态
外围实现
主体实现构造:
export interface Options { type?: 'js' | 'css'; js?: Partial<HTMLScriptElement>; css?: Partial<HTMLStyleElement>;}const useExternal = (path?: string, options?: Options) => { const [status, setStatus] = useState<Status>(path ? 'loading' : 'unset'); const ref = useRef<Element>(); 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);
残缺源码
useTitle
用于设置页面题目。
官网文档
根本用法
import React from 'react';import { useTitle } from 'ahooks';export default () => { useTitle('Page Title'); return ( <div> <p>Set title of the page.</p> </div> );};
应用场景
当进入某页面须要改浏览器 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 外面,具体见该文:检测意外的副作用
残缺源码
useFavicon
设置页面的 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<string>(DEFAULT_FAVICON_URL); useFavicon(url); return ( <> <p> Current Favicon: <span>{url}</span> </p> <button style={{ marginRight: 16 }} onClick={() => { setUrl(GOOGLE_FAVICON_URL); }} > Change To Google Favicon </button> <button onClick={() => { setUrl(DEFAULT_FAVICON_URL); }} > Back To AHooks Favicon </button> </> );};
应用场景
当须要改浏览器 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]);};
残缺源码
useFullscreen
治理 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 ( <div ref={ref} style={{ background: 'white' }}> <div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div> <div> <button type="button" onClick={enterFullscreen}> enterFullscreen </button> <button type="button" onClick={exitFullscreen} style={{ margin: '0 8px' }}> exitFullscreen </button> <button type="button" onClick={toggleFullscreen}> toggleFullscreen </button> </div> </div> );};
原生全屏 API
- Element.requestFullscreen():用于收回异步申请使元素进入全屏模式
- Document.exitFullscreen():用于让以后文档退出全屏模式。调用这个办法会让文档回退到上一个调用 Element.requestFullscreen()办法进入全屏模式之前的状态
[已过期不倡议应用]:Document.fullscreen:只读属性报告文档以后是否以全屏模式显示内容- Document.fullscreenElement:返回以后文档中正在以全屏模式显示的 Element 节点,如果没有应用全屏模式,则返回 null
- Document.fullscreenEnabled:返回一个布尔值,表明浏览器是否反对全屏模式。全屏模式只在那些不蕴含窗口化的插件的页面中可用
- fullscreenchange:元素过渡到或过渡到全屏模式时触发的全屏更改事件的事件
- fullscreenerror:在 Element 过渡到或退出全屏模式产生谬误后处理事件
screenfull 库
useFullscreen 外部次要是依赖 screenfull 这个库进行实现的。
screenfull 对各种浏览器全屏的 API 进行封装,兼容性好。
上面是该库的 API:
- .request(element, options?):使元素或者页面切换到全屏
- .exit():退出全屏
- .toggle(element, options?):在全屏和非全屏之间切换
- .on(event, function):增加一个监听器,监听全屏切换或者谬误事件。event 反对
change
或者error
- .off(event, function):移除之前注册的事件监听
- .isFullscreen:判断是否为全屏
- .isEnabled:判断以后环境是否反对全屏
- .element:返回该元素是否是全屏模式展现,否则返回 undefined
实现思路
看看 useFullscreen
的导出值:
return [ state, { enterFullscreen: useMemoizedFn(enterFullscreen), exitFullscreen: useMemoizedFn(exitFullscreen), toggleFullscreen: useMemoizedFn(toggleFullscreen), isEnabled: screenfull.isEnabled, },] as const;
那么实现的方向就比较简单了:
- 外部封装并裸露 toggleFullscreen、enterFullscreen、exitFullscreen 办法,裸露外部是否全屏的状态,还有是否反对全屏的状态
- 通过 screenfull 库监听
change
事件,在change
事件外面扭转全屏状态与解决执行回调
外围实现
三个办法的实现:
// 进入全屏办法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,},
残缺源码
useHover
监听 DOM 元素是否有鼠标悬停。
官网文档
根本用法
import React, { useRef } from 'react';import { useHover } from 'ahooks';export default () => { const ref = useRef(null); const isHovering = useHover(ref); return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>;};
鼠标监听事件
- mouseenter:第一次挪动到触发事件元素中的激活区域时触发
- mouseleave:在定点设施(通常是鼠标)的指针移出某个元素时被触发
扩大下几个鼠标事件的区别:
- mouseenter:当鼠标移入某元素时触发。
- mouseleave:当鼠标移出某元素时触发。
- mouseover:当鼠标移入某元素时触发,移入和移出其子元素时也会触发。
- mouseout:当鼠标移出某元素时触发,移入和移出其子元素时也会触发。
- mousemove:鼠标在某元素上挪动时触发,即便在其子元素上也会触发。
外围实现
原理是监听 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;};
残缺源码