useExternal
作用
动态注入 JS 或 CSS 资源,useExternal 可以保证资源全局唯一。
源码
ts
import { useEffect, useRef, useState } from 'react'
// js配置项
type JsOptions = {
type: 'js'
js?: Partial<HTMLScriptElement>
keepWhenUnused?: boolean
}
// css配置项
type CssOptions = {
type: 'css'
css?: Partial<HTMLStyleElement>
keepWhenUnused?: boolean
}
// 默认配置项
type DefaultOptions = {
type?: never
js?: Partial<HTMLScriptElement>
css?: Partial<HTMLStyleElement>
keepWhenUnused?: boolean
}
export type Options = JsOptions | CssOptions | DefaultOptions
// {[path]: count}
// remove external when no used
const EXTERNAL_USED_COUNT: Record<string, number> = {}
export type Status = 'unset' | 'loading' | 'ready' | 'error'
interface loadResult {
ref: Element
status: Status
}
// 加载js
const loadScript = (path: string, props = {}): loadResult => {
// 获取script标签
const script = document.querySelector(`script[src="${path}"]`)
// 如果没有script标签,创建script标签
if (!script) {
// 创建script标签
const newScript = document.createElement('script')
// 设置script标签的src
newScript.src = path
// 设置script标签的属性
Object.keys(props).forEach((key) => {
newScript[key] = props[key]
})
// 设置script标签的data-status属性
newScript.setAttribute('data-status', 'loading')
// 将script标签添加到body中
document.body.appendChild(newScript)
// 返回结果
return {
ref: newScript,
status: 'loading',
}
}
// 如果有script标签,返回结果
return {
ref: script,
status: (script.getAttribute('data-status') as Status) || 'ready',
}
}
// 加载css
const loadCss = (path: string, props = {}): loadResult => {
// 获取css标签
const css = document.querySelector(`link[href="${path}"]`)
// 如果没有css标签,创建css标签
if (!css) {
// 创建css标签
const newCss = document.createElement('link')
// 设置css标签的属性
newCss.rel = 'stylesheet'
// 设置css标签的href
newCss.href = path
// 设置css标签的属性
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'
}
// 设置css标签的data-status属性
newCss.setAttribute('data-status', 'loading')
// 将css标签添加到head中
document.head.appendChild(newCss)
// 返回结果
return {
ref: newCss,
status: 'loading',
}
}
// 如果有css标签,返回结果
return {
ref: css,
status: (css.getAttribute('data-status') as Status) || 'ready',
}
}
const useExternal = (path?: string, options?: Options) => {
// 保存状态
const [status, setStatus] = useState<Status>(path ? 'loading' : 'unset')
const ref = useRef<Element>()
useEffect(() => {
// 如果没有path,设置状态为unset
if (!path) {
setStatus('unset')
return
}
const pathname = path.replace(/[|#].*$/, '')
// 如果是css资源,调用loadCss
if (options?.type === 'css' || (!options?.type && /(^css!|\.css$)/.test(pathname))) {
const result = loadCss(path, options?.css)
ref.current = result.ref
setStatus(result.status)
// 如果是js资源,调用loadScript
} 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',
)
}
// 如果没有ref,直接返回
if (!ref.current) {
return
}
// 如果没有EXTERNAL_USED_COUNT[path],设置EXTERNAL_USED_COUNT[path]为1
if (EXTERNAL_USED_COUNT[path] === undefined) {
EXTERNAL_USED_COUNT[path] = 1
} else {
// 如果有EXTERNAL_USED_COUNT[path],EXTERNAL_USED_COUNT[path] + 1
EXTERNAL_USED_COUNT[path] += 1
}
// 设置ref的data-status属性
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)
// 组件卸载时移除事件监听
return () => {
ref.current?.removeEventListener('load', handler)
ref.current?.removeEventListener('error', handler)
EXTERNAL_USED_COUNT[path] -= 1
// 如果EXTERNAL_USED_COUNT[path]为0,且keepWhenUnused为false,移除ref
if (EXTERNAL_USED_COUNT[path] === 0 && !options?.keepWhenUnused) {
ref.current?.remove()
}
ref.current = undefined
}
}, [path])
// 返回状态
return status
}
export default useExternal