前端微应用-乾坤(qiankun)原理分析-import-html-entry

import-html-entry做为解析不同乾坤(qiankun)入口文件的依赖,主要功能是解析html文件,并返回一个promise对象,返回的promise对象中包含html文件解析后的内容,以及html文件解析后的script标签。

也是因为有这个库在,你才能像iframe样使用qiankun, 子应用预加载跟缓存 核心也很该插件挂钩。

本篇主要分析一下import-html-entry,以及qiankun应用基础使用。

乾坤的基本使用

乾坤的概念是不依赖任何框架,基座可以是任意框架或者HTML,子应用也是同样。

  1. 基座应用
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 });
  1. 子应用的应用

第一个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-entryentry引入下基本用法,如果想获取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。预加载 不过是趁着浏览器空隙时执行。

相关推荐
bigHead-19 分钟前
9. 从《蜀道难》学CSS基础:三种选择器的实战解析
前端·css
阿里小阿希1 小时前
解决 pnpm dev 运行报错的坎坷历程
前端·node.js
未脱发程序员1 小时前
分享一款开源的图片去重软件 ImageContrastTools,基于Electron和hash算法
前端·javascript·electron
视频砖家2 小时前
Web前端VSCode如何解决打开html页面中文乱码的问题(方法2)
前端·vscode·vscode乱码·vscode中文乱码·vscode中文编码
Nice__J2 小时前
智芯Z20K144x MCU开发之时钟架构
单片机·嵌入式硬件·架构
2401_837088502 小时前
CSS transition过渡属性
前端·css
我爱吃朱肉2 小时前
深入理解 CSS Flex 布局:代码实例解析
前端·css
喝养乐多长不高2 小时前
Spring Web MVC基础理论和使用
java·前端·后端·spring·mvc·springmvc
信徒_2 小时前
微服务系统设计
微服务·云原生·架构