问题
一、QianKun 拦截请求的问题
现象: A 页面中加载了腾讯地图、高德地图,并且 A 页面通过 QianKun 的方式加载,但是打开页面后发现相关 map.qq.com
的请求都出现跨域拦截的情况,导致地图加载不出来。
原因: qiankun 会将微应用的动态 script 加载(例如 JSONP)转化为 fetch 请求,因此需要相应的后端服务支持跨域,否则会导致错误。
解决: 在单实例模式下,你可以使用 excludeAssetFilter 参数来放行这部分资源请求,但是注意,被该选项放行的资源会逃逸出沙箱,由此带来的副作用需要你自行处理。
microAppMap[activeAppName] = loadMicroApp(otherApps, {
singular: false,
sandbox: {
experimentalStyleIsolation: otherApps.props.styleIsolation,
},
excludeAssetFilter: (assetUrl) => {
const whiteList: any[] = [];
const whiteWords = ['map.qq.com'];
if (whiteList.includes(assetUrl)) {
return true;
}
return whiteWords.some((w) => {
return assetUrl.includes(w);
});
},
});
二、微应用路由模式问题
现象: 微应用 A
基于 React
, 使用了 HashRouter
, 如果在多 tab
模式下,同样加载了 React
、 HashRouter
的 B
应用,这时候 B
页面内部跳转路由时,B
页面的内容渲染到了 A
页面中,造成渲染混乱的问题。
三、第三方引入-高德地图
现象: 微应用 A 通过script
标签引入第三方地图,如下<script src="//webapi.amap.com/maps?v=1.4.0&key=e198259ef493ed9c7767058831f93a26&plugin=AMap.PlaceSearch"></script>
, 微应用 A 通过主应用打开,那么主应用会挂掉。
原因: 子应用引入地图时无法往window上挂_jsload_函数,以至于需要使用时报错
解决:
-
主应用引入高德地图
<script src="//webapi.amap.com/maps?v=1.4.0&key=e198259ef493ed9c7767058831f93a26&plugin=AMap.PlaceSearch"></script>
<script src="//webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script> -
子应用通过
script
标签引入第三方地图时,需要加ignore
属性,以供独立运行使用<script ignore src="//webapi.amap.com/maps?v=1.4.0&key=e198259ef493ed9c7767058831f93a26&plugin=AMap.PlaceSearch"></script>
四、主应用为 Vue ,使用 Vue-Router 中的 push 来切换微应用带来的问题
现象: 我们平台网址为 https://qingju.xiaoju.com
, 主应用为 Vue
, 使用 Vue-Router
中的 push
来切换微应用时,如果微应用为 React
, 且 React
微应用还是一个路由页面,切换到了详情路由, 那么主应用再次通过 push
切换其他微应用时, 导致请求的网址变为 https://qingju.xiaoju.comundefined
。 如果在测试环境 https://ff.intra.xiaojukeji.com/qingju-transfer/
是没有这个问题的
原因: Vue-Router
中的 push
方法底层实现有问题
解决: 首先实现一个 push
方法来替代 Vue-Router
中的 push
方法
router.push(`/${menu.detailPath || newUniqKey}`);
push(state.currentUniqKey, `/${menu.detailPath || newUniqKey}`);
state.currentUniqKey = `/${menu.detailPath || newUniqKey}`;
function push(current: string, forward: string) {
/**
* @description: mappRequestPrefix 含义
* - 正式环境: mappRequestPrefix = "https://qingju.xiaojukeji.com/"
* - 测试环境: mappRequestPrefix = "https://ff.intra.xiaojukeji.com/qingju-transfer/"
*/
const { mappRequestPrefix } = window;
const currentCopy = mappRequestPrefix + current;
const forwardCopy = mappRequestPrefix + forward;
window.history.pushState(
{
back: null,
current: currentCopy, // current 必须为一个完整的 url 地址
forward:forwardCopy, // forward 必须为一个完整的 url 地址
position: window.history.length,
replaced: true,
scroll: null,
},
'',
forwardCopy
);
}
五、在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,你会如何处理静态资源的加载问题?
在使用 qiankun
时,如果子应用是基于 jQuery
的多页应用,静态资源的加载问题可能会成为一个挑战。这是因为在微前端环境中,子应用的静态资源路径可能需要进行特殊处理才能正确加载。这里有几种可能的解决方案:
-
方案一: 使用公共路径, 在子应用的静态资源路径前添加公共路径前缀。例如,如果子应用的静态资源存放在
http://localhost:8080/static/
,那么可以在所有的静态资源路径前添加这个前缀。 -
方案二: 劫持标签插入函数, 这个方案分为两步:
-
对于
HTML
中已有的img/audio/video
等标签,qiankun
支持重写getTemplate
函数,可以将入口文件index.html
中的静态资源路径替换掉。 -
对于动态插入的
img/audio/video
等标签,劫持appendChild
、innerHTML
、insertBefore
等事件,将资源的相对路径替换成绝对路径。
例如,我们可以传递一个
getTemplate
函数,将图片的相对路径转为绝对路径,它会在处理模板时使用:start({
getTemplate(tpl,...rest) {
// 为了直接看到效果,所以写死了,实际中需要用正则匹配
return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">');
}
});对于动态插入的标签,劫持其插入
DOM
的函数,注入前缀。beforeMount: app => {
if(app.name === 'purehtml'){
// jQuery 的 html 方法是一个挺复杂的函数,这里只是为了看效果,简写了
$.prototype.html = function(value){
const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">')
this[0].innerHTML = str;
}
}
} -
六、在使用 qiankun 时,如果子应用动态插入了一些标签,你会如何处理?
在使用 qiankun
时,如果子应用动态插入了一些标签,我们可以通过劫持 DOM
的一些方法来处理。例如,我们可以劫持 appendChild
、innerHTML
和 insertBefore
等方法,将资源的相对路径替换为绝对路径。
以下是一个例子,假设我们有一个子应用,它使用 jQuery
动态插入了一张图片:
const render = $ => {
$('#app-container').html('<p>Hello, render with jQuery</p><img src="./img/my-image.png">');
return Promise.resolve();
};
我们可以在主应用中劫持 jQuery
的 html
方法,将图片的相对路径替换为绝对路径:
beforeMount: app => {
if(app.name === 'my-app'){
// jQuery 的 html 方法是一个复杂的函数,这里为了简化,我们只处理 img 标签
$.prototype.html = function(value){
const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">')
this[0].innerHTML = str;
}
}
}
在这个例子中,我们劫持了 jQuery
的 html
方法,将图片的相对路径 ./img/my-image.png
替换为了绝对路径 http://localhost:8080/img/my-image.png
。这样,无论子应用在哪里运行,图片都可以正确地加载。
七、在使用 qiankun 时,你如何处理老项目的资源加载问题?你能给出一些具体的解决方案吗?
在使用 qiankun
时,处理老项目的资源加载问题可以有多种方案,具体的选择取决于项目的具体情况。以下是一些可能的解决方案:
-
使用
qiankun
的getTemplate
函数重写静态资源路径:对于HTML
中已有的img/audio/video
等标签,qiankun
支持重写getTemplate
函数,可以将入口文件index.html
中的静态资源路径替换掉。例如:start({
getTemplate(tpl,...rest) {
// 为了直接看到效果,所以写死了,实际中需要用正则匹配
return tpl.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">');
}
}); -
劫持标签插入函数:对于动态插入的
img/audio/video
等标签,我们可以劫持appendChild
、innerHTML
、insertBefore
等事件,将资源的相对路径替换成绝对路径。例如,我们可以劫持jQuery
的html
方法,将图片的相对路径替换为绝对路径:beforeMount: app => {
if(app.name === 'my-app'){
$.prototype.html = function(value){
const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">')
this[0].innerHTML = str;
}
}
} -
使用
iframe
嵌入老项目:虽然qiankun
支持jQuery
老项目,但是似乎对多页应用没有很好的解决办法。每个页面都去修改,成本很大也很麻烦,但是使用iframe
嵌入这些老项目就比较方便。
八、你能解释一下 qiankun 的 start 函数的作用和参数吗?如果只有一个子项目,你会如何启用预加载?
qiankun
的 start
函数是用来启动微前端应用的。在注册完所有的子应用之后,我们需要调用 start
函数来启动微前端应用。
start
函数接收一个可选的配置对象作为参数,这个对象可以包含以下属性:
-
prefetch
: 预加载模式,可选值有true
、false
、'all'
、'popstate'
。默认值为true
,即在主应用 start 之后即刻开始预加载所有子应用的静态资源。如果设置为'all'
,则主应用start
之后会预加载所有子应用静态资源,无论子应用是否激活。如果设置为'popstate'
,则只有在路由切换的时候才会去预加载对应子应用的静态资源。 -
sandbox
: 沙箱模式,可选值有true
、false
、{ strictStyleIsolation: true }
。默认值为true
,即为每个子应用创建一个新的沙箱环境。如果设置为false
,则子应用运行在当前环境下,没有任何的隔离。如果设置为{ strictStyleIsolation: true }
,则会启用严格的样式隔离模式,即子应用的样式会被完全隔离,不会影响到其他子应用和主应用。 -
singular
: 是否为单例模式,可选值有true
、false
。默认值为true
,即一次只能有一个子应用处于激活状态。如果设置为false
,则可以同时激活多个子应用。 -
fetch
: 自定义的fetch
方法,用于加载子应用的静态资源。
如果只有一个子项目,要想启用预加载,可以这样使用 start
函数:
start({ prefetch: 'all' });
这样,主应用 start
之后会预加载子应用的所有静态资源,无论子应用是否激活。
九、在使用 qiankun 时,你如何处理 js 沙箱不能解决的 js 污染问题?
qiankun
的 js
沙箱机制主要是通过代理 window
对象来实现的,它可以有效地隔离子应用的全局变量,防止子应用之间的全局变量污染。然而,这种机制并不能解决所有的 js
污染问题。例如,如果我们使用 onclick
或 addEventListener
给 <body>
添加了一个点击事件,js
沙箱并不能消除它的影响。
对于这种情况,我们需要依赖于良好的代码规范和开发者的自觉。在开发子应用时,我们需要避免直接操作全局对象,如 window
和 document
。如果必须要操作,我们应该在子应用卸载时,清理掉这些全局事件和全局变量,以防止对其他子应用或主应用造成影响。
例如,如果我们在子应用中添加了一个全局的点击事件,我们可以在子应用的 unmount
生命周期函数中移除这个事件:
export async function mount(props) {
// 添加全局点击事件
window.addEventListener('click', handleClick);
}
export async function unmount() {
// 移除全局点击事件
window.removeEventListener('click', handleClick);
}
function handleClick() {
// 处理点击事件
}
这样,当子应用卸载时,全局的点击事件也会被移除,不会影响到其他的子应用。
十、你能解释一下 qiankun 如何实现 keep-alive 的需求吗?
在 qiankun
中,实现 keep-alive
的需求有一定的挑战性。这是因为 qiankun
的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive
的需求是相悖的,因为 keep-alive
需要保留子应用的状态,而不是在子应用卸载时将其状态清除。
然而,我们可以通过一些技巧来实现 keep-alive
的效果。一种可能的方法是在子应用的生命周期函数中保存和恢复子应用的状态。例如,我们可以在子应用的 unmount
函数中保存子应用的状态,然后在 mount
函数中恢复这个状态:
// 伪代码
let savedState;
export async function mount(props) {
// 恢复子应用的状态
if (savedState) {
restoreState(savedState);
}
}
export async function unmount() {
// 保存子应用的状态
savedState = saveState();
}
function saveState() {
// 保存子应用的状态
// 这个函数的实现取决于你的应用
}
function restoreState(state) {
// 恢复子应用的状态
// 这个函数的实现取决于你的应用
}
这种方法的缺点是需要手动保存和恢复子应用的状态,这可能会增加开发的复杂性。此外,这种方法也不能保留子应用的 DOM
状态,只能保留 JavaScript
的状态。
还有一种就是手动loadMicroApp+display:none
,直接隐藏Dom
十一、QianKun 如何处理路由?
微前端应用拆分成子应用后,子应用路由应具备自治能力,可以充分的利用解耦后的开发优势,但与之对应的是应用间的路由可能会发生冲突,两种路由模式下可能产生用户难以理解的路由状态、无法激活不同的前端框架下带来的试图无法更新等问题。因此,主要提供了三种策略:
-
提供
Router Map
, 自动化完成子应用的调度, 降低开发者理解成本XXX.run({
domGetter: "#submodule",
apps: [
{
name: "vue-app",
activeWhen: '/vue-app',
entry: "http//localhost:9090"
},
{
name: "react-app",
activeWhen: '/react-app',
entry: "http//localhost:9091"
}
]
}) -
为不同子应用提供不同的
basename
用于隔离应用间的路由抢占问题: 当应用处于激活状态时, 根据应用的激活状态条件自动计算出应用所需的基础路径,并在渲染时告诉框架,以便于应用路由不发生冲突。export function provider({ dom,basename }){
return {
render(){
ReactDOM.render(<App basename={basename} />);
},
destroy(){
ReactDOM.unmountComponentAtNode(dom.querySelector("#app"));
}
}
} -
路由发生变化时能准确激活并触发应用试图更新: 点击某个子应用, 手动触发对应路由的
push
函数, 并监听路由变化, 每当路由变化时, 开始更新对应的子应用视图。