import-html-entry 原理
标签: 了解
# 目录
# 介绍
Treats the index html as manifest and loads the assets(css,js), get the exports from entry script.
# importEntry
export function defaultGetPublicPath(entry) {
if (typeof entry === "object") {
return "/";
}
try {
// new URL('https://example.org/foo', 'https://example.org/') 和 new URL('/foo', 'https://example.org/') 结果是一样的
const { origin, pathname } = new URL(entry, location.href);
const paths = pathname.split("/");
// 移除最后一个元素
paths.pop();
return `${origin}${paths.join("/")}/`;
} catch (e) {
console.warn(e);
return "";
}
}
export function importEntry(entry, opts = {}) {
const {
fetch = defaultFetch,
getTemplate = defaultGetTemplate,
postProcessTemplate,
} = opts;
const getPublicPath =
opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
if (!entry) {
throw new SyntaxError("entry should not be empty!");
}
// html entry
if (typeof entry === "string") {
return importHTML(entry, {
fetch,
getPublicPath,
getTemplate,
postProcessTemplate,
});
}
// config entry
if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) {
const { scripts = [], styles = [], html = "" } = entry;
// 为 styles 打占位标记
const getHTMLWithStylePlaceholder = (tpl) =>
// reduceRight 支持 ie,因为是写在模板的前面,所以用 reduceRight 保证先后顺序
// see https://caniuse.com/?search=reduceRight
styles.reduceRight(
(html, styleSrc) => `${genLinkReplaceSymbol(styleSrc)}${html}`,
tpl
);
// 为 scripts 打占位标记
const getHTMLWithScriptPlaceholder = (tpl) =>
scripts.reduce(
(html, scriptSrc) => `${html}${genScriptReplaceSymbol(scriptSrc)}`,
tpl
);
return getEmbedHTML(
// 回调给外部定制 template
getTemplate(
// 给 script 和 styles 打占位标记
// 注意,这两种占位都打在 html 的两端
getHTMLWithScriptPlaceholder(getHTMLWithStylePlaceholder(html))
),
styles,
{ fetch }
).then((embedHTML) => ({
template: embedHTML,
assetPublicPath: getPublicPath(entry),
getExternalScripts: () => getExternalScripts(scripts, fetch),
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(scripts[scripts.length - 1], scripts, proxy, {
fetch,
strictGlobal,
beforeExec: execScriptsHooks.beforeExec,
afterExec: execScriptsHooks.afterExec,
});
},
}));
} else {
throw new SyntaxError("entry scripts or styles should be array!");
}
}
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
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
# html 外部的 style 和 script 会不会生效?
<!-- html 标签外部的样式 -->
<style>
#root {
width: 100px;
height: 100px;
background: red;
}
</style>
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root"></div>
</body>
</html>
<!-- html 标签外部的脚本 -->
<script src="./index.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
将占位标记打在 html 的外部,构成类似上面的代码,这样的代码样式仍然会生效,脚本也会生效。可以参考这个示例:html 标签外部的样式和脚本示例
# importHTML
过程跟 importEntry 类似,不在赘述。
// 读取 fetch 的结果为字符串,并且支持自动检测字符编码类型
export function readResAsString(response, autoDetectCharset) {
// 未启用自动检测
if (!autoDetectCharset) {
return response.text();
}
// 如果没headers,发生在test环境下的mock数据,为兼容原有测试用例
if (!response.headers) {
return response.text();
}
// 如果没返回content-type,走默认逻辑
const contentType = response.headers.get("Content-Type");
if (!contentType) {
return response.text();
}
// 解析content-type内的charset
// Content-Type: text/html; charset=utf-8
// Content-Type: multipart/form-data; boundary=something
// GET请求下不会出现第二种content-type
let charset = "utf-8";
const parts = contentType.split(";");
if (parts.length === 2) {
const [, value] = parts[1].split("=");
const encoding = value && value.trim();
if (encoding) {
charset = encoding;
}
}
// 如果还是utf-8,那么走默认,兼容原有逻辑,这段代码删除也应该工作
if (charset.toUpperCase() === "UTF-8") {
return response.text();
}
// 走流读取,编码可能是gbk,gb2312等,比如sofa 3默认是gbk编码
return response.blob().then(
(file) =>
new Promise((resolve, reject) => {
const reader = new window.FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsText(file, charset);
})
);
}
export function defaultGetPublicPath(entry) {
if (typeof entry === "object") {
return "/";
}
try {
// new URL('https://example.org/foo', 'https://example.org/') 和 new URL('/foo', 'https://example.org/') 结果是一样的
const { origin, pathname } = new URL(entry, location.href);
const paths = pathname.split("/");
// 移除最后一个元素
paths.pop();
return `${origin}${paths.join("/")}/`;
} catch (e) {
console.warn(e);
return "";
}
}
export default function importHTML(url, opts = {}) {
let fetch = defaultFetch;
let autoDecodeResponse = false;
let getPublicPath = defaultGetPublicPath;
let getTemplate = defaultGetTemplate;
const { postProcessTemplate } = opts;
// compatible with the legacy importHTML api
if (typeof opts === "function") {
fetch = opts;
} else {
// fetch option is availble
if (opts.fetch) {
// fetch is a funciton
if (typeof opts.fetch === "function") {
fetch = opts.fetch;
} else {
// configuration
fetch = opts.fetch.fn || defaultFetch;
autoDecodeResponse = !!opts.fetch.autoDecodeResponse;
}
}
getPublicPath =
opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
getTemplate = opts.getTemplate || defaultGetTemplate;
}
return (
// 先从缓存中取,缓存中有直接使用缓存的结果,否则请求模板并解析,并将解析结果缓存
embedHTMLCache[url] ||
(embedHTMLCache[url] = fetch(url)
.then((response) => readResAsString(response, autoDecodeResponse))
.then((html) => {
const assetPublicPath = getPublicPath(url);
// 解析模板,获取 template, scripts, entry, styles
const { template, scripts, entry, styles } = processTpl(
getTemplate(html),
assetPublicPath,
postProcessTemplate
);
return getEmbedHTML(template, styles, { fetch }).then((embedHTML) => ({
template: embedHTML,
assetPublicPath,
getExternalScripts: () => getExternalScripts(scripts, fetch),
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(entry, scripts, proxy, {
fetch,
strictGlobal,
beforeExec: execScriptsHooks.beforeExec,
afterExec: execScriptsHooks.afterExec,
});
},
}));
}))
);
}
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
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
# getEmbedHTML
获取优化样式后的模板,将外部样式转换为内部样式以优化性能
/**
* convert external css link to inline style for performance optimization
* 获取优化样式后的模板,将外部样式转换为内部样式以优化性能,注意这里不是行内样式
* ? 为什么可以优化性能?
* 将 template 中 style 的
* @param template
* @param styles
* @param opts
* @return embedHTML
*/
function getEmbedHTML(template, styles, opts = {}) {
const { fetch = defaultFetch } = opts;
let embedHTML = template;
return getExternalStyleSheets(styles, fetch).then((styleSheets) => {
// 获取外部样式并转化为内部样式替换到样式占位标记的位置
embedHTML = styles.reduce((html, styleSrc, i) => {
html = html.replace(
genLinkReplaceSymbol(styleSrc),
`<style>/* ${styleSrc} */${styleSheets[i]}</style>`
);
return html;
}, embedHTML);
return embedHTML;
});
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# getExternalScripts
获取模板中的脚本内容。
// 是否是内部脚本
const isInlineCode = (code) => code.startsWith("<");
// 获取内部脚本内部的内容
export function getInlineCode(match) {
const start = match.indexOf(">") + 1;
const end = match.lastIndexOf("<");
return match.substring(start, end);
}
// RIC and shim for browsers setTimeout() without it
export const requestIdleCallback =
window.requestIdleCallback ||
function requestIdleCallback(cb) {
// 模拟 requestIdleCallback 的返回值
return setTimeout(() => {
cb();
}, 1);
};
// for prefetch
export function getExternalScripts(
scripts,
fetch = defaultFetch,
errorCallback = () => {}
) {
const fetchScript = (scriptUrl) =>
// 使用缓存机制
scriptCache[scriptUrl] ||
(scriptCache[scriptUrl] = fetch(scriptUrl)
.then((response) => {
// usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event
// https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603
// 400 以上通常是请求错误
if (response.status >= 400) {
errorCallback();
throw new Error(
`${scriptUrl} load failed with status ${response.status}`
);
}
return response.text();
})
.catch((e) => {
errorCallback();
throw e;
}));
return Promise.all(
scripts.map((script) => {
if (typeof script === "string") {
if (isInlineCode(script)) {
// if it is inline script
return getInlineCode(script);
} else {
// 外部脚本发请求获取
// external script
return fetchScript(script);
}
} else {
// 如果是一个对象,processTpl 解析的结果
// use idle time to load async script
const { src, async } = script;
// 如果是异步的脚本,先返回一个对象 content 在 IDLE 时加载
if (async) {
return {
src,
async: true,
content: new Promise((resolve, reject) =>
// 这里 resolve, reject 这样写是因为可以不传参
requestIdleCallback(() => fetchScript(src).then(resolve, reject))
),
};
}
// 同步的发请求获取
return fetchScript(src);
}
})
);
}
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
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
# getExternalStyleSheets
获取模板中的样式表内容,和 getExternalScripts 类似不在赘述。
// for prefetch
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
return Promise.all(
styles.map((styleLink) => {
if (isInlineCode(styleLink)) {
// if it is inline style
return getInlineCode(styleLink);
} else {
// external styles
return (
styleCache[styleLink] ||
(styleCache[styleLink] = fetch(styleLink).then((response) =>
response.text()
))
);
}
})
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# execScripts
执行 js 脚本文件,如果有入口脚本文件 (entry),将 entry 文件执行的结果返回。
const evalCache = {};
// 在 window 环境下安全的执行代码,并且加入缓存机制
export function evalCode(scriptSrc, code) {
const key = scriptSrc;
if (!evalCache[key]) {
// 将待执行的代码包装成函数
const functionWrappedCode = `window.__TEMP_EVAL_FUNC__ = function(){${code}}`;
// 在安全的 window 环境执行上述代码,将包装的函数挂载到 window.__TEMP_EVAL_FUNC__ 上
// (0, eval)('console.log(this)') 返回 Window
(0, eval)(functionWrappedCode);
// 将函数加入到缓存中
evalCache[key] = window.__TEMP_EVAL_FUNC__;
// 函数已经执行完毕,因为code是自执行函数,删除临时变量
delete window.__TEMP_EVAL_FUNC__;
}
// 如果命中缓存,从缓存中取出函数并执行
const evalFunc = evalCache[key];
evalFunc.call(window);
}
// 将要执行脚本包装成带对应执行环境的自执行函数
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
// 注释内容,不影响执行结果
const sourceUrl = isInlineCode(scriptSrc)
? ""
: `//# sourceURL=${scriptSrc}\n`;
// 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
// 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
const globalWindow = (0, eval)("window");
globalWindow.proxy = proxy;
// TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
return strictGlobal
? `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
: `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}
/**
* FIXME to consistent with browser behavior, we should only provide callback way to invoke success and error event
* @param entry
* @param scripts
* @param proxy
* @param opts
* @returns {Promise<unknown>}
*/
export function execScripts(entry, scripts, proxy = window, opts = {}) {
const {
fetch = defaultFetch,
strictGlobal = false,
success,
error = () => {},
beforeExec = () => {},
afterExec = () => {},
} = opts;
return getExternalScripts(scripts, fetch, error).then((scriptsText) => {
const geval = (scriptSrc, inlineScript) => {、
// 执行 beforeExec 钩子
const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
// 获取包装后要执行的代码(自执行函数)
const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
// 执行包装后的代码
evalCode(scriptSrc, code);
// 执行 afterExec 钩子
afterExec(inlineScript, scriptSrc);
};
function exec(scriptSrc, inlineScript, resolve) {
// 获取到入口脚本
if (scriptSrc === entry) {
noteGlobalProps(strictGlobal ? proxy : window);
try {
// bind window.proxy to change `this` reference in script
geval(scriptSrc, inlineScript);
const exports =
proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
// resolve 执行入口脚本导出的内容
resolve(exports);
} catch (e) {
// entry error must be thrown to make the promise settled
console.error(
`[import-html-entry]: error occurs while executing entry script ${scriptSrc}`
);
throw e;
}
} else {
if (typeof inlineScript === "string") {
try {
// bind window.proxy to change `this` reference in script
// 执行代码
geval(scriptSrc, inlineScript);
} catch (e) {
// consistent with browser behavior, any independent script evaluation error should not block the others
throwNonBlockingError(
e,
`[import-html-entry]: error occurs while executing normal script ${scriptSrc}`
);
}
} else {
// external script marked with async
// 如果是异步脚本
inlineScript.async &&
inlineScript?.content
.then((downloadedScriptText) =>
// 执行 content 中的脚本内容
geval(inlineScript.src, downloadedScriptText)
)
.catch((e) => {
throwNonBlockingError(
e,
`[import-html-entry]: error occurs while executing async script ${inlineScript.src}`
);
});
}
}
}
// i 表示从下标 i 开始处理
function schedule(i, resolvePromise) {
if (i < scripts.length) {
const scriptSrc = scripts[i];
const inlineScript = scriptsText[i];
// 执行脚本文件
// 因为 entry 只有一个,所以一个 resolvePromise 传入没有问题
exec(scriptSrc, inlineScript, resolvePromise);
// resolve the promise while the last script executed and entry not provided
// 如果没有提供入口脚本,且所有的脚本都执行完了直接 resolve
if (!entry && i === scripts.length - 1) {
resolvePromise();
} else {
// 继续执行下一个脚本
schedule(i + 1, resolvePromise);
}
}
}
// 如果传了 success 就在 success 中处理,否则就在 Promise.then 里处理
return new Promise((resolve) => schedule(0, success || resolve));
});
}
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
133
134
135
136
137
138
139
140
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
133
134
135
136
137
138
139
140
上述代码还有如下的疑点:
noteGlobalProps 和 getGlobalProp 是什么意思?
const isIE11 =
typeof navigator !== "undefined" &&
navigator.userAgent.indexOf("Trident") !== -1;
// 性能优化考虑
function shouldSkipProperty(global, p) {
if (!global.hasOwnProperty(p) || (!isNaN(p) && p < global.length))
return true;
if (isIE11) {
// https://github.com/kuitos/import-html-entry/pull/32,最小化 try 范围
try {
return (
global[p] &&
typeof window !== "undefined" &&
global[p].parent === window
);
} catch (err) {
return true;
}
} else {
return false;
}
}
// safari unpredictably lists some new globals first or second in object order
let firstGlobalProp, secondGlobalProp, lastGlobalProp;
export function getGlobalProp(global) {
let cnt = 0;
let lastProp;
let hasIframe = false;
for (let p in global) {
if (shouldSkipProperty(global, p)) continue;
// 遍历 iframe,检查 window 上的属性值是否是 iframe,是则跳过后面的 first 和 second 判断
for (let i = 0; i < window.frames.length && !hasIframe; i++) {
const frame = window.frames[i];
if (frame === global[p]) {
hasIframe = true;
break;
}
}
if (
!hasIframe &&
((cnt === 0 && p !== firstGlobalProp) ||
(cnt === 1 && p !== secondGlobalProp))
)
return p;
cnt++;
lastProp = p;
}
if (lastProp !== lastGlobalProp) return lastProp;
}
export function noteGlobalProps(global) {
// 获取 global 上最后一个属性
// alternatively Object.keys(global).pop()
// but this may be faster (pending benchmarks)
firstGlobalProp = secondGlobalProp = undefined;
for (let p in global) {
if (shouldSkipProperty(global, p)) continue;
if (!firstGlobalProp) firstGlobalProp = p;
else if (!secondGlobalProp) secondGlobalProp = p;
lastGlobalProp = p;
}
return lastGlobalProp;
}
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
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
# processTpl
# 参考
编辑 (opens new window)
上次更新: 2022/09/06, 14:25:16