Skip to content

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

如有转载或 CV 的请标注本站原文地址