import-html-entry
做为解析不同乾坤(qiankun)
入口文件的依赖,主要功能是解析html
文件,并返回一个promise对象,返回的promise对象中包含html文件解析后的内容,以及html文件解析后的script标签。
也是因为有这个库在,你才能像iframe
样使用qiankun
, 子应用预加载跟缓存 核心也很该插件挂钩。
本篇主要分析一下import-html-entry
,以及qiankun
应用基础使用。
乾坤的基本使用
乾坤的概念是不依赖任何框架,基座可以是任意框架或者
HTML
,子应用也是同样。
- 基座应用
js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { registerMicroApps, start, loadMicroApp } from 'qiankun';
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
const commonComponents = {};
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer', // 挂载在那个容器下
activeRule: '/app-vue-hash', // 不过是key名字变了
props: { data : { store, router, loadMicroApp, commonComponents } }
},
{
name: 'app-vue-history',
entry: 'http://localhost:2222',
container: '#appContainer',
activeRule: '/app-vue-history',
props: { data : store }
},
]);
// 共享组件必须开启多实例
start({ singular: false });
- 子应用的应用
第一个vue应用指定端口1111
,第二个应用指定端口2222
,这只是在本地调试,如果部署线上环境,需要修改entry
地址。
子应用暴漏mount、unmount、bootstrap、update
相关API,如下:
js
import './public-path';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
import HelloWorld from '@/components/HelloWorld.vue'
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render() {
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount('#appVueHistory');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
//测试全局变量污染
window.a = 1;
export async function bootstrap() {
console.log('vue app bootstraped');
}
export async function mount(props) {
console.log('props from main framework', props);
if(props.data.commonComponents){
props.data.commonComponents.HelloWorld = HelloWorld
}else{
render();
}
// 测试一下 body 的事件,不会被沙箱移除
// document.body.addEventListener('click', e => console.log('document.body.addEventListener'))
// document.body.onclick = e => console.log('document.body.addEventListener')
}
export async function unmount() {
if(instance){
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
router = null;
}
}
这里不讲怎么使用
qiankun
,只是为了import-html-entry
的entry
引入下基本用法,如果想获取demo,可以通过龚顺大佬的git获取,里面有各种场景的demo,可以按需了解,对初步接触者非常友好。
import-html-entry插件
这个插件干了啥呢?主要是用来解析HTML文件,咱们先看一个简单的react
打包后的HTML资源:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<script type="module" crossorigin src="/assets/index-CY8RT7Xj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cd5-0EfR.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
经过import-html-entry
后会被解析成这个样子:

js
import { importEntry } from 'import-html-entry';
const { template, execScripts, assetPublicPath, getExternalScripts,getExternalStyleSheets } = await importEntry('http://localhost:1111');
/**
* assetPublicPath: 资源路径
* template: HTML字符串 不包含script标签 link标签会被转成style标签
* execScripts: 运行存放的script标签并且获取到导出的生命周期函数
* getExternalScripts: 存放HTML中的js
* getExternalStyleSheets 存放HTML中的css
*/
`
"<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<!-- script http://127.0.0.1:5500/assets/index-CY8RT7Xj.js replaced by import-html-entry -->
<style>/* http://127.0.0.1:5500/assets/index-Cd5-0EfR.css */:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}#root{height:100%}.logo{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.react:hover{filter:drop-shadow(0 0 2em #61dafbaa)}@keyframes logo-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (prefers-reduced-motion: no-preference){a:nth-of-type(2) .logo{animation:logo-spin infinite 20s linear}}.card{padding:2em}.read-the-docs{color:#888}
</style>
</head>
<body>
<div id="root"></div>
<!-- inline scripts replaced by import-html-entry -->
</body>
</html>
"
`
他是怎么做到获取对应的HTML呢(entry: 'http://localhost:1111',
)?
默认是通过fetch
,当然你这个可以自定义如下:
js
fetch('http://localhost:1111').then((res) => console.log(res.text()))
拿到文本后利用正则做文本替换抽离:(取style、以及script)
js
import { getInlineCode, isModuleScriptSupported } from './utils';
const ALL_SCRIPT_REGEX = /(<script[\s\S]*?>)[\s\S]*?<\/script>/gi;
const SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|')text\/ng-template\3).)*?>.*?<\/\1>/is;
const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/;
const SCRIPT_TYPE_REGEX = /.*\stype=('|")?([^>'"\s]+)/;
const SCRIPT_ENTRY_REGEX = /.*\sentry\s*.*/;
const SCRIPT_ASYNC_REGEX = /.*\sasync\s*.*/;
const SCRIPT_NO_MODULE_REGEX = /.*\snomodule\s*.*/;
const SCRIPT_MODULE_REGEX = /.*\stype=('|")?module('|")?\s*.*/;
const LINK_TAG_REGEX = /<(link)\s+.*?>/isg;
const LINK_PRELOAD_OR_PREFETCH_REGEX = /\srel=('|")?(preload|prefetch)\1/;
const LINK_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
const LINK_AS_FONT = /.*\sas=('|")?font\1.*/;
const STYLE_TAG_REGEX = /<style[^>]*>[\s\S]*?<\/style>/gi;
const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/;
const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
const HTML_COMMENT_REGEX = /<!--([\s\S]*?)-->/g;
const LINK_IGNORE_REGEX = /<link(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
const STYLE_IGNORE_REGEX = /<style(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
const SCRIPT_IGNORE_REGEX = /<script(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
export default function processTpl(tpl, baseURI) {
let scripts = [];
const styles = [];
let entry = null;
const moduleSupport = isModuleScriptSupported();
const template = tpl
/*
remove html comment first
*/
.replace(HTML_COMMENT_REGEX, '') // remove html comment
.replace(LINK_TAG_REGEX, match => { // 替换link标签 并且styles。push(URL)
})
.replace(STYLE_TAG_REGEX, match => { // style 这里主要是做忽略用的标签
if (STYLE_IGNORE_REGEX.test(match)) {
return genIgnoreAssetReplaceSymbol('style file');
}
return match;
})
.replace(ALL_SCRIPT_REGEX, (match, scriptTag) => { // script 标签处理
const scriptIgnore = scriptTag.match(SCRIPT_IGNORE_REGEX);
});
scripts = scripts.filter(function (script) {
// filter empty script
return !!script;
});
return {
template,
scripts,
styles,
// set the last script as entry if have not set
entry: entry || scripts[scripts.length - 1],
};
}
execScripts
执行对应的scripts
通过存储的scripts
可以获取到对应的js code
,然后使用eval
执行这些js code
(;(function(window, self){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy);
)用with
包裹了一层
getExternalScripts,getExternalStyleSheets
两个方法主要是给qiankun
做应用预加载用的。
咱们从上面的运行可以看到getExternalScripts,getExternalStyleSheets
可以拿到对应的代码。然后等切换到该子应用后直接执行该代码。
总结
关于为什么要用import-html-entry
解析html
这么做呢?
-
第一点是为了获取子应用导出的生命周期(mount、bootstrap、unmount、update),如果不这么处理大家可以想想,让它自己通过
script
执行后咱们要怎么才能获取到呢? 放windows上也算是种方案。还有别的吗? -
第二点是为了能实现预加载,提前获取到别的子应用的一些资源,等切换到该子应用的话就能直接载对应的文件就行。
Q: 如果qiankun
没有配置预加载(prefetchApps)
跟iframe不就一样了?资源还都是要载的不过是换个形式?那为什么说性能有提升呢?
A: 确实是这个样,qiankun
通过fetch
的形式也是把对应的资源加载了。但是qiankun
缓存了加载过的子应用信息,可以避免重复加载。这点比iframe
要优。
Q:解析HTML是编译时 还是运行时 ?
A: 运行时,不管是不是预加载 ,都需要在运行时解析HTML。预加载 不过是趁着浏览器空隙时执行。