教做网站的学校,企业网站如何做架构图,免费发布广告的平台,百度地图的精准定位功能背景
在实际项目中#xff0c;随着日常跌倒导致的必然墒增#xff0c;项目会越来越冗余不好维护#xff0c;而且有时候一个项目会使用的其他团队的功能#xff0c;这种跨团队不好维护和管理等等问题#xff0c;所以基于解决这些问题#xff0c;出现了微前端的解决方案。…背景
在实际项目中随着日常跌倒导致的必然墒增项目会越来越冗余不好维护而且有时候一个项目会使用的其他团队的功能这种跨团队不好维护和管理等等问题所以基于解决这些问题出现了微前端的解决方案。微前端具有拆分和集成的特点本文主要讲解主流的single-spa以及qiankun的基本原理和使用。
微前端
定义微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略就是将多个应用集合到一起通过动态匹配的方式展示。 特点 与技术无关、渐进式增量升级和迁移、模块隔离、运行时子应用状态不共享 解决问题解决随着项目迭代会导致必然墒增导致项目越来越庞大冗余不好维护的问题微前端将一个大应用拆分为可以独立运行的子应用然后集合子应用。
Single-SPA
组合式路由分发由主容器监听路由变化动态进行子应用的挂载/卸载
特点
网页打开主应用会执行registerApplication注册子应用然后会监控路由变化当路由匹配activeWhen时会先坚持子应用状态先通过在子应用注入的生命周期卸载上一个子应用然后挂载匹配路由的子应用。函数流程大致是registerApplication - start - bootstrap - mount- unmount
1、主应用注册子应用
singleSpa.registerApplication({app1,() import(src/app1/main.js),(location) location.pathname.startsWith(/app1),customProps: { authToken: d83jD63UdZ6RS6f70D0 }
});
singleSpa.registerApplication({app2,() import(src/app2/main.js),(location) location.pathname.startsWith(/app2),customProps: (name, location) {return { authToken: d83jD63UdZ6RS6f70D0 };}
});2、子应用注入主应用控制的生命周期
bootstrap: 初始化
应用挂载之前的初始化这个生命周期函数会在应用第一次挂载前执行一次。
mount挂载
每当应用的activity function返回真值registerApplication的第三个参数但该应用处于未挂载状态时挂载的生命周期函数就会被调用。调用时函数会根据URL来确定当前被激活的路由然后加载对应的js文件创建DOM元素、监听DOM事件等以向用户呈现渲染的内容。任何子路由的改变如hashchange或popstate等不会再次触发mount需要各应用自行处理。
unmount卸载
每当应用的activity function返回假值但该应用已挂载时卸载的生命周期函数就会被调用。卸载函数被调用时会清理在挂载应用时被创建的DOM元素、事件监听、内存、全局变量和消息订阅等。 let domEl;
export function bootstrap(props) {return Promise.resolve().then(() {domEl document.createElement(div);domEl.id app1;document.body.appendChild(domEl);});
}
export function mount(props) {return Promise.resolve().then(() {// 在这里通常使用框架将ui组件挂载到dom。请参阅https://single-spa.js.org/docs/ecosystem.html。domEl.textContent App 1 is mounted!});
}
export function unmount(props) {return Promise.resolve().then(() {// 在这里通常是通知框架把ui组件从dom中卸载。参见https://single-spa.js.org/docs/ecosystem.htmldomEl.textContent ;})
}unload移除
可选的生命周期只有当主应用调用unloadApplication时才会触发子应用中的unload函数一旦应用被移除它的状态将会变成NOT_LOADED下次激活时会被重新初始化bootstrap阶段。
timeout超时设置
主应用可以针对每个阶段进行超时设置超过设置时间还没完成则会抛出异常。 全局配置
import { setBootstrapMaxTime, setMountMaxTime, setUnmountMaxTime, setUnloadMaxTime } from single-spa;// 设置 bootstrap 阶段的最大超时时间为 5 秒
setBootstrapMaxTime(5000, 应用 bootstrap 超时);// 设置 mount 阶段的最大超时时间为 10 秒
setMountMaxTime(10000, 应用 mount 超时);// 设置 unmount 阶段的最大超时时间为 5 秒
setUnmountMaxTime(5000, 应用 unmount 超时);// 设置 unload 阶段的最大超时时间为 5 秒
setUnloadMaxTime(5000, 应用 unload 超时);
给特定的子应用配置
import { registerApplication } from single-spa;registerApplication({name: my-app,app: () System.import(my-app),activeWhen: [/my-app],customProps: {bootstrapTimeout: 5000, // bootstrap 阶段的超时时间mountTimeout: 10000, // mount 阶段的超时时间unmountTimeout: 5000, // unmount 阶段的超时时间unloadTimeout: 5000, // unload 阶段的超时时间}
});
超时处理的默认行为 当 single-spa 检测到应用在指定阶段的操作超过了配置的超时时间时默认的行为是
Bootstrap 阶段超时: 应用将无法进入 MOUNTED 状态single-spa 会抛出错误。Mount 阶段超时: 应用将无法完全加载和显示single-spa 会抛出错误。Unmount 阶段超时: 应用无法正确卸载可能导致残留的状态或资源占用。Unload 阶段超时: 应用无法被卸载可能导致内存泄漏或其他问题。
3、主应用启动/卸载子应用
卸载有两种
子应用切换当子应用A切换B时会先卸载当前子应用A然后挂载匹配的子应用B这时候A的资源还是在内存中以便切回A时能快速响应如果切回A则不会进入bootstrap初始化周期。卸载子应用当主应用彻底卸妆子应用A时会调用A中的unload生命周期,并且会将子应用状态设置为NOT_LOADED然后将A从内存中移除再次启动A时会重新进入bootstrap初始化周期。 // 启动 single-spa
singleSpa.start();// 卸载子应用
unloadApplication(my-app).then(() {console.log(子应用已经卸载);// 模拟用户导航到子应用的路径触发重新加载navigateToUrl(/container);
});4、生命周期执行流程
加载阶段: 子应用第一次被加载时single-spa 会依次执行子应用的 bootstrap 和 mount 钩子。 挂载阶段: 子应用在页面上呈现时single-spa 会执行子应用的 mount 钩子。 卸载阶段: 当子应用不再需要显示时single-spa 会调用 unmount 钩子将子应用从页面上移除。这时子应用的状态和资源可能仍然保留在内存中以便快速重新加载。 完全卸载阶段: 调用 unloadApplication 时single-spa 会执行子应用的 unload 钩子并将子应用的资源从内存中释放。下一次匹配到这个子应用时会重新执行 bootstrap 钩子。
5、存在的问题
手动加载资源会根据子应用打包方式改变
single-spa采用的是Js Entry的方式来加载子应用。需要手动加载js、css等资源如果子应用打包方式改变比如生成的资源路径变化,主应用中也要变化。
import { registerApplication } from single-spa;
registerApplication({name: app,() import(src/app1/main.js), // 手动加载Js资源(location) location.pathname.startsWith(/app1),
})singleSpa.start()代码冲突
single-spa主要通过Js Entry将多个子应用引入到主应用多个Js运行在主应用的全局上下文中可能存在命名冲突、样式冲突、Js冲突等问题。需要手动做JS、CSS的隔离。
子应用通信
Single-SPA只是将多个子应用挂载在主应用而子应用之间互相通信没有内置功能需要自己手动实现。
qiankun
qiankun 是一个基于 single-spa 的微前端实现库解决了single-spa的一些问题就是一个更完善的single-spa库。主要是解决了single-spa的一些问题并做了丰富。使用思路和single-spa大致一样在主应用中注册子应用然后在子应用中写入主应用的生命周期等待主应用的调用。
需要手动写加载资源路径如果子应用打包方式改变主应用资源加载路径就要调整使用Js Entry导致多应用间Js、Css代码冲突问题子应用间通信
特点
沙箱隔离 嵌入主容器的不同子应用的Js、Css是相互隔离的1、通过scope的方式在dom节点和样式都加上前缀避免冲突。2、Proxy代理对象的方式子应用访问的是这个代理对象CSS 的隔离就是使用 shadow dom 这是浏览器支持的特性shadow root 下的 dom 的样式是不会影响其他 dom 的。
解决问题一
通过Html Entry的方式在主应用配置子应用的html即可qiankun会自动加载并解析html文件并将script脚本部分解析拆离出来单独加载其他html部分添加到主应用到dom中。以此来解决需要用户手动写资源url路径的问题这个功能的实现放在 import-html-entry 这个包里
registerMicroApps([{name: react app, // app name registeredentry: //localhost:7100, // 会自动加载html文件container: #yourContainer,activeRule: /yourActiveRule,},{name: vue app,entry: { scripts: [//localhost:7100/main.js] },container: #yourContainer2,activeRule: /yourActiveRule2,},
]);
子应用的html文件 挂载在主应用的dom上
解决问题二 JS、CSS隔离
JS隔离
主要就是全局window的隔离 qiankun 实现 window 隔离有三种沙箱
快照加载子应用前记录下 window 的属性卸载之后恢复到之前的快照diff加载子应用之后记录对 window 属性的增删改卸载之后恢复回去Proxy创建一个代理对象每个子应用访问到的都是这个代理对象 从源码中能看出来当浏览器支持Proxy的时候会根据useLooseSandbox来判断使用LegacySandbox还是ProxySandbox。这两个都是基于Proxy代理的但是LegacySandbox的隔离度不高对子应用进行基本隔离主要是兼容较老的一些浏览器。ProxySandbox是拦截子应用的所有全局变量和函数的访问和修改是强隔离的。当浏览器不支持Proxy时会降级使用快照来隔离。
由于前两种都是先记录window的更改当卸载之后再恢复之前的状态所以不能存在多个子应用否则会发生冲突一般还是使用Proxy代理沙箱。
Proxy代理沙箱
Proxy代理对象子应用访问的是这个代理对象Proxy对代理对象进行了get、set拦截操作会隔离不同子应用使其只会访问自身上下文不会影响全局对象。
createFakeWindow
通过createFakeWindow创建ProxyWindow代理通过createFakeWindow传入全局window会对其进行代理返回一个代理代理windowfakeWindow和一个保存拥有自定义getter属性的对象propertiesWithGetter 为什么需要propertiesWithGetter ProxySandbox 通过 Proxy 来代理对全局对象如 window的访问。但是有些属性具有自定义的 getter 方法这些 getter 方法在属性被访问时会执行特定的逻辑。这些属性不能简单地通过 Proxy 来直接代理因为访问时需要正确触发这些 getter 方法。 function createFakeWindow(globalContext, speedy) {var propertiesWithGetter new Map();var fakeWindow {};Object.getOwnPropertyNames(globalContext).filter(function (p) {var descriptor Object.getOwnPropertyDescriptor(globalContext, p);return !(descriptor null || descriptor void 0 ? void 0 : descriptor.configurable);}).forEach(function (p) {var descriptor Object.getOwnPropertyDescriptor(globalContext, p);if (descriptor) {var hasGetter Object.prototype.hasOwnProperty.call(descriptor, get);if (p top || p parent || p self || p window ||p document speedy || inTest ) {descriptor.configurable true;if (!hasGetter) {descriptor.writable true;}}if (hasGetter) propertiesWithGetter.set(p, true);}});return {fakeWindow: fakeWindow,propertiesWithGetter: propertiesWithGetter};}通过Proxy代理window
基于代理fakeWindow对象对子应用的操作进行拦截 以get为例
get: function get(target, p) {_this.registerRunningApp(name, proxy);if (p Symbol.unscopables) return unscopables;if (p window || p self) {return proxy;}if (p globalThis || inTest ) {return proxy;}if (p top || p parent || inTest ) {if (globalContext globalContext.parent) {return proxy;}return globalContext[p];}if (p hasOwnProperty) {return hasOwnProperty;}if (p document) {return _this.document;}if (p eval) {return eval;}if (p string globalVariableWhiteList.indexOf(p) ! -1) {return globalContext[p];}var actualTarget propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext;var value actualTarget[p];if (isPropertyFrozen(actualTarget, p)) {return value;}if (!isNativeGlobalProp(p) !useNativeWindowForBindingsProps.has(p)) {return value;}var boundTarget useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;return rebindTarget2Fn(boundTarget, value);},其中globalVariableWhiteList用来保存一个允许全局访问的变量或属性的白名单。这个白名单中的变量或属性在沙箱的代理过程中不会被隔离或修改允许子应用直接访问和操作这些全局变量qiankun不会对白名单中的属性进行修改。
const globalVariableWhiteList [window,self,globalThis,document,navigator,location,localStorage,sessionStorage,fetch,XMLHttpRequest,console,setTimeout,setInterval,clearTimeout,clearInterval,requestAnimationFrame,cancelAnimationFrame
];Css隔离
1、Shadow DOM(影子DOM)
允许创建隔离的 DOM 子树通过给真实DOM绑定一个Shadow DOM然后将我们的样式通过添加到Shadow DOM树的方式添加Shadow DOM树中的DOM天生不会影响真实DOM。shadow dom 是 web components 技术的一部分其实就一个 attachShadow 的 api(原生)。web components 添加内容的时候不直接 appendChild而是通过attachshadow创建一个影子DOM然后再在下面 appendChild。shadow dom 就是原生自带的CSS沙箱。内部样式影响不了外界外界样式也影响不了 shadow dom 内的元素。但是使用组件的Modal弹窗这些默认挂载在body中的样式就会有问题。 2、Scope CSS隔离
Scope CSS作用域 CSS通过给子应用的根节点添加一个前缀来进行区分结合样式重写来让样式只会影响本应用的DOM添加子应用唯一ID - 前缀一般用文件路径和项目package.name来组成hash为前缀。 以Vue的scope为例 Vue vue的scoped样式其实也有问题它是通过.vue文件在项目中的相对路径path文件名进行计算hash值的当主子应用中同时存在一个path和文件名相同时的.vue文件它的data-v-xxxxx算出来就是一样的此时样式还是会冲突。可以加上package.name来区分 React使用的是CSS Module来实现CSS隔离在编译时会进行hash换算所以在代码中要使用style[class_name]的方式来添加样式类
解决问题三 应用间通信
single-spa需要自己实现应用间的状态共享而qiankun提供的apiinitGlobalState在主容器中初始化需要共享的状态然后在挂载在子应用中的mount生命周期通过props.onGlobalStateChange来获取共享数据
initGlobalState 是 qiankun 提供的一个 API用于创建一个全局的共享状态对象并允许不同的微前端应用进行读写和监听这个共享状态的变化。这个共享状态管理系统与沙箱机制是分开的不依赖window而是由qiankun自身维护的一个状态管理系统。qiankun会监控这个状态当状态发生变化时会通知所有订阅了这个状态变化的应用。
import { initGlobalState, MicroAppStateActions } from qiankun;// 初始化 state
const actions: MicroAppStateActions initGlobalState(state);
// 设置状态
actions.setGlobalState(state);
// 监控状态变化
actions.onGlobalStateChange((state, prev) {// state: 变更后的状态; prev 变更前的状态console.log(state, prev);
});
// 取消监听
actions.offGlobalStateChange();// 从生命周期 mount 中获取通信方法使用方式和 master 一致
export function mount(props) {props.onGlobalStateChange((state, prev) {// state: 变更后的状态; prev 变更前的状态console.log(state, prev);});props.setGlobalState(state);
}