概述
import-html-entry
是一个用于动态加载和处理 HTML 和 JS 文件的库,主要用于微前端架构中。它能够从远程服务器拉取 HTML 内容,并对其中的 JS 和 CSS 进行处理,以便在主应用中加载和执行。这个库是 qiankun
微前端框架的核心依赖之一,提供了强大的动态加载和执行能力。在微前端框架 qiankun
中,import-html-entry
被用来解决 JS Entry
的问题,通过 HTML Entry
的方式,让用户接入微应用就像使用 iframe
一样简单。
使用方法
安装
首先,你需要通过 npm 或 yarn 安装 import-html-entry
:
bash
npm install import-html-entry
或者
bash
yarn add import-html-entry
基本使用
以下是一个简单的示例,展示如何使用 import-html-entry
加载一个远程的 HTML 文件, 我们看官网的例子
在index.html中 使用import-html-entry加载./template.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<script type="module">
window.onerror = e => {
console.log('error', e.message);
};
window.onunhandledrejection = (e) => {
console.log('unhandledrejection', e.reason.message);
};
import('./dist/index.js').then(({ importEntry }) => {
importEntry('./template.html').then(res => {
console.log(res);
return res.execScripts().then(exports => {
console.log(exports);
});
}).catch(e => {
console.log('importEntry failed', e.message);
});
});
</script>
</body>
</html>
template.html如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<link href="https://unpkg.com/antd@3.13.6/dist/antd.min.css" rel="stylesheet">
<link href="https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap-grid.min.css" rel="stylesheet">
</head>
<body>
<script src="./a.js"></script>
<script ignore>alert(1)</script>
<script src="./b.js"></script>
<script src="./c.js"></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"></script>
<script src="https://www.baidu.com"></script>
</body>
</html>
template.html被import-html-entry处理过后如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<style>
/* antd样式被内链进入 */
</style>
<style>
/* bootstrap样式被内链进入 */
</style>
</head>
<body>
<!-- script http://127.0.0.1:7001/a.js replaced by import-html-entry -->
<!-- ignore asset js file replaced by import-html-entry -->
<!-- script http://127.0.0.1:7001/b.js replaced by import-html-entry -->
<!-- script http://127.0.0.1:7001/c.js replaced by import-html-entry -->
<!-- script https://unpkg.com/react@16.4.2/umd/react.production.min.js replaced by import-html-entry -->
<!-- script https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js replaced by import-html-entry -->
<!-- script https://www.baidu.com/ replaced by import-html-entry -->
</body>
</html>
可以发现html中的css被处理成为内链样式的了,其中的js代码script被注释掉了
importHTML
返回值有如下几个:
1、template---处理过后的html
2、assetPublicPath---资源路径
3、getExternalScripts---执行后返回脚本信息
4、getExternalStyleSheets---执行后返回样式信息
5、execScripts---js代码执行器,可以传入代理的window对象
我们可以看出来,经过import-html-entry
处理后能够拿到这个html中的js、css内容,其中css会被处理成为内链样式嵌入HTML中,js我们可以通过execScripts传入自己的代理window可以实现js沙箱隔离
qiankun中如何使用的?
我们观察qiankun
源码中是如何使用的import-html-entry
的
在src/loader.js中如下:
js
// 266行
const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);
// 347行
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});
// get the lifecycle hooks from module exports
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
可以看到和预期一样
1、使用import-html-entry
拿到js执行器
2、执行execScripts,并且传入自己的globalContext
3、根据导出,拿到生命周期函数lifecycle
源码解析
import-html-entry
的核心功能是通过 fetch
获取指定 URL 的 HTML 内容,然后解析并处理这个 HTML 模板,最终返回一个包含处理后的 HTML、CSS 和 JS 的 Promise
对象。具体步骤如下:
-
拉取 HTML 并处理 :通过
fetch
获取到 URL 对应的全部内容(即 HTML 文件的字符串),然后解析出以下内容:经过初步处理后的 HTML(去掉外链 CSS 和外链 JS)、由所有script
组成的数组、由所有style
组成的数组。 -
嵌入 CSS :通过
fetch
拉取到上述style
数组里面对应的 CSS,然后将拉取到的每一个 href 对应的 CSS 通过<style>
包裹起来且嵌入到 HTML 中。 -
执行 JS 脚本:支持执行页级 JS 脚本以及拉取上述 HTML 中所有的外联 JS 并支持执行。因此,在微前端中,使用此依赖可以直接获取到子应用(某 URL)对应的 HTML 且此 HTML 上已经嵌好了所有的 CSS,同时还可以直接执行子应用的所有 JS 脚本且此脚本还为 JS 隔离(避免污染全局)做了预处理。
整体流程如下图所示:
execScripts
code = getExecutableScript()
通过function+with实现js沙箱
js
function getExecutableScript(scriptSrc, scriptText, opts = {}) {
const { proxy, strictGlobal, scopedGlobalVariables = [] } = opts;
const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;
// 将 scopedGlobalVariables 拼接成变量声明,用于缓存全局变量,避免每次使用时都走一遍代理
const scopedGlobalVariableDefinition = scopedGlobalVariables.length ? `const {${scopedGlobalVariables.join(',')}}=this;` : '';
// 通过这种方式获取全局 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
? (
scopedGlobalVariableDefinition
? `;(function(){with(this){${scopedGlobalVariableDefinition}${scriptText}\n${sourceUrl}}}).bind(window.proxy)();`
: `;(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);`;
}
evalCode(scriptSrc, code)
通过eval执行代码
js
export function evalCode(scriptSrc, code) {
const key = scriptSrc;
if (!evalCache[key]) {
const functionWrappedCode = `(function(){${code}})`;
evalCache[key] = (0, eval)(functionWrappedCode);
}
const evalFunc = evalCache[key];
evalFunc.call(window);
}
processTpl
js
const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath, postProcessTemplate);
看一下执行结果。
通过processTpl实现。
1、替换HTML
2、导出js入口列表
3、style列表
4、找到入口文件