本文正在参加「金石计划」
传送门
之前写过一些微前端的文章,建议结合在一起阅读,本文将对比乾坤的实现方式进行讲解:
前言
在我们分析「无界」这个具体的微前端方案之前,我们先假设我们自己是框架设计者,思考一下一个微前端框架需要解决哪些问题;
给读者思考一分钟......
子应用如何加载,生命周期管理
js隔离措施
css隔离措施
通信机制
......
我们就以这些问题为切入点去看一看「无界」是怎么解决这些具体问题的。先回顾一下「无界」微前端怎么使用;
「无界」的使用方式
无界对于不同的前端框架都有对应的组件化封装。假如我们主项目是React
技术栈,那么直接用WujieReact
这个组件就可以了:
ReactDOM.render(
<>
<App />
<WujieReact
width="100%"
height="100%"
name="browser"
url={'http://localhost:3001/'}
/>
</>,
document.getElementById('root'),
);
无界微前端没有什么改造成本,那么他是怎么做到的呢?接下来我们通过半源码、半手写代码的方式去分析「无界」这个微前端框架;
子应用如何加载,生命周期管理
子应用的加载与乾坤的方式相同,都通过一个importHTML
函数进行加载;它的具体过程就是:
-
fetch
我们传入的这个url
,得到一个html
字符串,然后通过正则表达式
匹配到内部样式表、外部样式表、脚本;源码通过/<(link)\s+.*?>/gis
匹配外部样式,通过/(<script[\s\S]*?>)[\s\S]*?<\/script>/gi
匹配脚本;通过/<style[^>]*>[\s\S]*?<\/style>/gi
匹配内部样式;我们尝试一下自己写一个importHTML
,解析一下我们当前这一篇文章:const STYLE_REG = /<style>(.*)<\/style>/gi; const SCRIPT_REG = /<script>(.*)<\/script>/gi; const LINK_REG = /<(link)\s+.*?>/gi async function imoprtHTML() { let html = await fetch("https://juejin.cn/post/7209162467928096825"); html = await html.text(); const ans = html.replace(STYLE_REG, match=>{ // ... 很多逻辑 return match; } ).replace(SCRIPT_REG, match=>{ // ... 很多逻辑 return match; } ).replace(LINK_REG, match=>{ // ... 很多逻辑 debugger return match; }) }
-
第二步对于外部样式表、外部脚本我们也需要通过
fetch
获取到内容然后将代码存储起来 -
将合并的样式表添加到页面上
-
执行js,这个详细过程我们后文分析
-
子应用加载完毕
这个过程中涉及到哪些生命周期呢?
- beforeLoad:子应用开始加载静态资源前触发,也就是
importHTML
之前触发 - beforeMount:子应用渲染前触发 (生命周期改造专用)
- afterMount:子应用渲染后触发(生命周期改造专用)
- beforeUnmount:子应用卸载前触发(生命周期改造专用)
- afterUnmount:子应用卸载后触发(生命周期改造专用)
- activated:子应用进入后触发(保活模式专用)
- deactivated:子应用离开后触发(保活模式专用)
从上面我们就了解到了「无界」框架整体的一个加载流程,整体看起来它的生命周期比较多,比乾坤要多很多,但是比较好记,因为与Vue
的生命周期在命名上有些类似;接下来我们具体分析一下js的隔离措施;
js沙箱
先回顾一下乾坤是怎么创建js
沙箱的:乾坤设计了三种沙箱,在不支持Proxy
的浏览器基于window
进行diff
,把修改了的变量存储到一个对象中,在子应用卸载时还原;支持Proxy
的使用Proxy
对window
进行代理,然后存储修改了的变量;多个子应用共存的情况下会拷贝一份window
对象;对拷贝的对象进行代理,子应用操作的也是这个拷贝的window对象;最后使用一个函数包裹子应用的js代码,绑定this
和window
,然后eval
执行;这里使用到了eval
执行代码,并且还使用到了with
,这两个都是性能杀手,我们对比着看看「无界」怎么处理的?
「无界」是通过iframe
进行环境隔离,也就是说我们的子应用上的全局对象其实是动态创建的一个iframe
的全局对象,我们简单实现一下这个过程:
function iframePool() {
const pool = [];
return function() {
const iframe = document.createElement("iframe");
pool.push(iframe);
return {
getIframe() {
const ele = pool.pop();
return ele;
},
recover(ele) {
pool.push(ele);
}
}
}
}
const container = iframePool()();
const iframe = container.getIframe();
document.body.appendChild(iframe);
container.recover(iframe);
window.a = 1234;
const proxyWindow = iframe.contentWindow;
const externalScripts = `
window.abc = 1;
console.log(window.abc)
`
function excute() {
const doc = proxyWindow.document;
const sc = doc.createElement("script");
sc.innerHTML = externalScripts;
doc.body.appendChild(sc);
console.log(proxyWindow.abc, "********")
}
excute();
简单解释一下这个过程:我们获取到了子应用代码之后将代码添加到iframe
中,这样很简单地就实现了一个沙箱,然后子应用的全局环境绑定到iframe的window上就可以了,看起来很简单是不是?而且它也没有兼容性问题啊,但是iframe
本身的一些问题可能会影响到主应用,这个需要我们具体去测评一下,iframe
会阻塞页面的onload事件,通常用来加载广告这种无关紧要的内容,如果加载子应用会不会有性能问题,这个就需要不断的实践验证了
css隔离措施:在web-components
容器下css
可以自然隔离
通信机制
「无界」的通信机制是通过发布订阅模式实现的,就是创建一个全局的发布订阅者,然后挂载到了「无界」对象上:
这一块源码比较好实现,读者可以自行实现一个发布订阅模式
「无界」插件
「无界」中还使用了插件架构,主要分为html
插件、css
插件、js
插件,html插件是在html解析完成之后执行,也就是上文提到的importHTML
cssLoader是在插入到DOM之前执行:
jsLoader插件在插入到iframe之前执行
当然还有一些其他的插件,无非都是在这些方法之前或者之后执行,这里列举一下:js-excludes、css-excludes、js-before-loaders、js-after-loaders、css-before-loaders、css-after-loaders
优雅降级
「无界」是存在优雅降级的,对于不支持webcomponents
的浏览器,直接降级为iframe
,虽然直接使用iframe体验上会差很多,但是目前已经有95%的浏览器已经支持webcomponents,所以影响的只是一些还在使用IE浏览器的用户,况且我们的主流框架也都抛弃了IE
后记
我们先站在高处思考了一下一个微前端框架应该解决什么问题;然后又从无界的使用、生命周期、js沙箱的创建、通信机制的实现、插件、优雅降级等几个方面详细地分析了一下「无界」内部的原理;「无界」
和「乾坤」
相比,它们是两种不同类型的微前端方案,一种是基于webcomponents
,一种是基于路由
,它们孰优孰劣需要结合项目自身的要求去分析;
从jQuery到三大框架经历了很长时间,那么一个完美的微前端方案需要等多长时间呢?