-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathuseExternal.ts
132 lines (108 loc) · 3.46 KB
/
useExternal.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { useEffect, useRef, useState } from 'react';
export type Options = {
type?: 'js' | 'css';
js?: Partial<HTMLScriptElement>;
css?: Partial<HTMLStyleElement>;
};
// {[path]: count}
// remove external when no used
const EXTERNAL_USED_COUNT: Record<string, number> = {};
export type Status = 'unset' | 'loading' | 'ready' | 'error';
type loadResult = {
ref: Element;
status: Status;
};
const loadScript = (path: string, props: HTMLScriptElement | {} = {}): loadResult => {
const script = document.querySelector(`script[src="${path}"]`);
if (!script) {
const newScript = document.createElement('script');
newScript.src = path;
Object.assign(newScript, props);
newScript.setAttribute('data-status', 'loading');
document.body.appendChild(newScript);
return {
ref: newScript,
status: 'loading',
};
}
return {
ref: script,
status: (script.getAttribute('data-status') as Status) || 'ready',
};
};
const loadCss = (path: string, props: HTMLLinkElement | {} = {}): loadResult => {
const css = document.querySelector(`link[href="${path}"]`);
if (!css) {
const newCss = document.createElement('link');
newCss.rel = 'stylesheet';
newCss.href = path;
Object.assign(newCss, props);
// 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';
}
newCss.setAttribute('data-status', 'loading');
document.head.appendChild(newCss);
return {
ref: newCss,
status: 'loading',
};
}
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(() => {
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);
ref.current = result.ref;
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'). ");
}
if (!ref.current) {
return;
}
if (EXTERNAL_USED_COUNT[path] === undefined) {
EXTERNAL_USED_COUNT[path] = 1;
} else {
EXTERNAL_USED_COUNT[path] += 1;
}
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;
if (EXTERNAL_USED_COUNT[path] === 0) {
ref.current?.remove();
}
ref.current = undefined;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path]);
return status;
};
export default useExternal;