在企业中,各个研发部门往往各自开发自己的应用。当需要把这些应用聚合在一起时。以往的解决方案是在主应用中嵌入 iframe,使用 iframe 加载和切换子应用页面。 这种做法有几个缺点:
以 React 项目为例,我们将组件挂载到DOM上时常会调用 ReactDOM.render 方法,如
ReactDOM.render(<App />, mountNode);
假如把这封装成一个方法并对外暴露,那么在别的项目中调用这个方法并传入一个待绑定的DOM节点,不就可以集成这个项目了吗? 这么做需要把应用库化。
第一步,在入口文件导出应用绑定DOM的方法,如下
import ReactDOM from 'react-dom';
import App from './App';
export function render(mountNode) {
ReactDOM.render(<App />, mountNode);
}
/**
* 应在父应用的全局注入控制变量以标识所处环境,如下面的 window.__IS_FUSION_PLATFORM__
* 若子应用未检测到该控制变量,则认为未处在父应用中,可直接初始化以便独立使用
* 若检测到该控制变量,则认为处在父应用中,等待父应用调用即可
*/
if (!window.__IS_FUSION_PLATFORM__) {
render(document.getElementById('root'));
}
第二步,修改webpack配置,告诉webpack在编译时将应用库化,即入口处的导出可被其他应用引用,如下
// 其他配置省略
output: {
library: : {
name: 'myApp',
type: 'umd'
}
}
library的详细介绍可以参考官方文档,这里只讲一下在此处的作用:
之后,我们在其他项目中引入编译产出,即可调用导出的方法。 此外,需要注意页面和接口请求的跨域问题。在子应用中,我们可能把页面和接口放在同一个域下以避免跨域问题;但在将子应用聚合到父应用之后,若父应用和子应用不在同一个域,应将接口代理转发一下。
这一节简单演示一下上述内容,主要是讲发布和引用库的过程。如果对此很了解,可以跳过。 首先使用 create-react-app 初始化hw-library和hw-app两个项目。 在hw-library中,主要做了以下几点修改:
// App.js
export default function App() {
return (
<h1>HW Library</h1>
);
}
/* App.css */
.title {
margin-top: 200px;
text-align: center;
}
{
"module": "build/main.js",
}
import {render} from 'hw-library/build/main';
import 'hw-library/build/main.css';
render(
document.getElementById('root')
);
最后启动项目,就可以看到hw-library应用被渲染到了hw-app的节点上了,如下
这种通过引入JS来聚合应用的方式,被称为JS ENTRY,它是微前端框架single-spa的主要思想,这种方式存在一些弊端:
在微前端的架构中,页面并不是作为一个整体开发的,而是由各个独立维护的组件拼接而成的,这些组件可以复用于任何页面,而一个页面也完全可以由不同的组件异构出多样化的呈现。 single-spa的文档中有对微前端概念的一些介绍,如下:
与JS ENTRY思想不同的是,qiankun使用HTML ENTRY,也就是你需要将webpack打包编译出来的HTML给到qiankun而不是JS。qiankun将使用一系列的正则表达式将里面的HTML、CSS、JS全部匹配出来,这个功能主要依赖于第三方库import-html-entry的importHTML方法,如下:
<!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="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>
在被导入的HTML中,我们引入了antd和bootstrap两个外部样式文件、a.js和b.js两个本地外部文件、mobx和react两个外部JS文件。
// index.js
import importHTML from 'import-html-entry';
importHTML('./index.html').then(
result => {
console.log('template: ', result.template);
result.execScripts().then(exports => {
console.log('entry exports: ', exports);
});
result.getExternalScripts().then(exports => {
console.log('external scripts: ', exports);
});
result.getExternalStyleSheets().then(exports => {
console.log('external styles: ', exports);
});
}
)
在导入HTML文件的代码中,我们将importHTML的解析结果打印出来,如下:
这样,我们就可以就可以将每个子应用的CSS和JS分离出来了。为了避免多个应用挂载的CSS和JS互相影响或冲突,qiankun 对其分别做了处理。
隔离CSS有两种模式,一种为shadowDOM,另一种为scoped CSS。
你可以理解shadowDOM为DOM中DOM,他对内部的DOM和CSS做了封装,也就是shadowDOM中的CSS只会影响其挂载节点内的DOM样式,不会影响外部的样式。shadowDOM并不存在于DOM树上,但通过一些内置组件还是能够看到他们的存在,比如video、audio的播放控制栏。 我们可以使用attachShadow给指定元素挂在一个shadow dom,并返回对shadow root的引用,如下:
// Element.attachShadow
const shadowroot = document.getEelementById('target').attachShadow({mode: 'open'});
shadowroot.innerHTML = `
<div>
<style></style>
<div>test</div>
</div>
`;
这样就可以解决子应用节点下的CSS隔离问题了。
在HTML ENTRY这一节,我们讲过可以使用import-html-entry将所有style标签解析出来、对于外部link标签中的样式也可以另外用fetch请求到。这样我们就可以将子应用的所有样式代码拿到了。 scoped CSS隔离CSS代码需要对子应用的代码进行特殊处理,也就是将所有CSS选择器前面加一个父级元素,如下
/* 原来为span,加上父级后为 */
div[data-app-name=myApp] span {
/* ... */
}
这样,子应用的样式代码就只会作用在data-app-name=myApp的div下面了。
在隔离JS时,qiankun使用了沙箱模式,分为三种:SanpshotSandbox、LegacySandbox、ProxySandbox。
SanpshotSandbox(快照沙箱)的原理是将主应用的window对象做浅拷贝,将 window 的键值对存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上就可以了。 过程如下:
LegacySandbox 是基于 SanpshotSandbox 的一种优化模式。SanpshotSandbox 在子应用每次unmount时,都需要对window上的每个属性值进行一次diff,不是那么优雅。 LegacySandbox的想法则是 通过监听对 window 的修改来直接记录 diff 内容,因为只要对 window 属性进行设置,那么就会有两种情况:
SanpshotSandbox和LegacySandbox都是单例模式下使用的沙箱,即父应用中只同时展示一个子应用,无论set和get都是直接操作window对象。如果想在父应用中同时展示多个子应用,这两种模式依然会有环境污染的问题。 为了避免真实的 window 被污染,qiankun 实现了 ProxySandbox。它的想法是:
// scriptText 为子应用的JS代码
const executableScript = `
;(function(window, self, globalThis){
;${scriptText}${sourceUrl}
}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`
eval(executableScript)
可以发现这里的代码做了三件事:
function fn(window, self, globalThis) {
window.a = 1;
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);
那么,在子应用中修改的是window.a,到父应用中修改的则是 window.proxy.a了。这样就实现了为子应用分配fakeWindow。 qiankun的具体用法可以参考官方文档。