省建设干部培训中心网站,网站接入网方式,找工程项目上哪个平台好呢,可以用asp做哪些网站初识微前端
微前端是什么
概念#xff1a; 微前端是指存在于浏览器中的微服务。
微前端是一种类似于微服务的架构#xff0c;它将微服务的理念应用于浏览器端#xff0c;即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一体的应用。这就意味着前端应用…初识微前端
微前端是什么
概念 微前端是指存在于浏览器中的微服务。
微前端是一种类似于微服务的架构它将微服务的理念应用于浏览器端即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一体的应用。这就意味着前端应用的拆分拆分后的应用实现应用自治、单一职责、技术栈无关三大特性再进行基座模式或自由组合的模式进行聚合达到微前端的目的。 自由组组织模式指的就是系统内部子系统之间能自行按照某种规则形成一定的结构或功能。
微前端的几个基本要素 技术栈无关、应用隔离、独立开发。
核心拆合
微前端背景
在前端框架、技术、概念层出不穷且随着前端标准的演进前端已经具备更好的性能和开发效率但是随之而来的是应用的复杂度更高、涉及的团队规模更广、更高的性能要求应用复杂度已经成为阻塞业务发展的重要瓶颈。
微前端就是诞生在这日益复杂化的场景中。
为什么使用微前端
为了解决团队平台系统多且相互独立系统体量大且页面多开发效率低、接入成本高。
当前应用痛点 项目中的组件和功能模块会越来越多导致整个项目的打包速度变慢因为文件夹的数量会随着功能模块的增多而增多查找代码会变得越来越慢如果只改动其中一个模块的情况需要把整个项目重新打包上线所有的项目都基本只能使用同一技术框架不便引入新技术栈。如react、vue等
微前端优势
简单、松耦合的代码库 微前端架构倾向于编写和维护更小、更简单、更容易开发的项目。技术栈无关各项目可以使用不同的技术栈。 增量升级 支持渐进式重构先让新旧代码和谐共存再逐步转化旧代码直到整个重构完成。 独立部署 每一个子应用都具备独立开发持续部署独立运行的能力。 团队自治 各子项目之间不存在依赖关系保持隔离。单一职责每个子项目只做和自己相关的业务工作。
微前端现有方案 浅谈single-spa
qiankun是基于single-spa的二次封装因此在谈qiankun之前我们先来简单了解一下single-spa。
single-spa的核心就是定义了一套协议。协议包含主应用的配置信息和子应用的生命周期通过这套协议主应用可以方便的知道在什么情况下激活哪个子应用。
生命周期 简单梳理一下single-spa的整个流程 从上面的图里面我们可能会产生下面四个问题
1主应用如何注册微应用
single-spa中提供了registerApplication方法来注册子应用。这个方法中接受几个特定的参数
singleSpa.registerApplication({ //注册微前端服务name: vueApp,app: () {return ...}, // 加载你的子应用activeWhen: /vueApp,//url 匹配规则表示啥时候开始走这个子应用的生命周期customProps: { // 自定义 props从子应用的 bootstrap, mount, unmount 回调可以拿到uid:6018990034}
});
singleSpa.start() // 启动主应用在single-spa中并未实现加载子应用的方法需要使用者自己实现比如通过动态创建script或者可以使用System.import()。
⚠️SystemJS 可以在浏览器里可以使用 ES6 的 import/export 并支持动态引入。
2主应用什么时候调度微应用的生命周期
主应用加载完成子应用后获取到子应用中暴露出的生命周期。
window[singleDemo] {bootstrap,mount,unmount
}上面这种暴露生命周期的方式也就是single-spa中子应用对外暴露变量的方式。使用不当会导致全局环境的污染。
因此的appName比如唯一否则会覆盖同名的子应用qiankun里面的沙箱机制就很好的解决了这种问题。
那上面的生命周期什么时候调度呢
一般来说主应用注册并加载完微应用并不会立即初始化。而是等到执行完start后。
当window.location.href匹配到url时去执行start方法后进行初始化调用子应用的bootStrap开始走对应子App的这一套生命周期。所以single-spa还要监听url的变化然后执行子app的生命周期流程。
3主应用如何监听路由以及控制路由跳转的
在上面我们了解到主应用监听路由url的切换匹配到相应的路由就根据对应路由去挂载和卸载微应用。
那么如何监听这个路由的跳转呢 4主应用如何挂载及卸载微应用的
子应用挂载时需要在mount方法中添加挂载逻辑
子应用卸载时需要在unmount方法中添加卸载逻辑在update方法中添加更新逻辑。
single-spa的挂载、更新、卸载并未提供而是需要用户自定实现。
single-spa 只起控制状态的作用它自己本身不亲自操刀的无论下载、挂载、卸载等这样也能做到更好的扩展性用户想怎么下载、挂载、卸载他们自己来决定只要你传入规范的参数即可。 single-spa为不同技术栈提供了一些逻辑抽象封装来对子应用进行包装。比如single-spa-vue\single-spa-react等。这些封装的模版里面对子应用进行包装然后在对应的生命周期钩子函数执行子应用挂载卸载更新等操作。
总结
一般来说微前端需要解决的问题分为两大类
应用的加载与切换路由问题、应用入口、应用加载应用的隔离与通信js隔离、css样式隔离、应用间通信
而single-spa则很好地解决了 路由问题、应用入口 两个问题但是应用的加载并未实现。因此qiankun在此基础上封装了一个应用加载方案并给出了js隔离、css样式隔离和应用间通信三个问题的解决方案同时提供了预加载功能。
qiankun
qiankun 使用 import-html-entry 插件将子应用的 html 作为入口框架会将 HTML document 作为子节点塞到主框架的容器中。就算子应用更新了其入口 html 文件的 url 始终不会变并且完整的包含了所有的初始化资源 url所以不用再自行维护子应用的资源列表了。并且对旧有的项目作为子应用接入成本几乎为零开发体验与独立开发时保持不变相较于 single-spa 的 js entry 而言更加灵活、方便、体验更好。 qiankun框架中子应用不需要关注qiankun框架无需引用其包只需按照标准实现导出接口即可
1.子应用集成
qiankun有两种集成微应用的方式基于路由配置、手动加载微应用
基于路由配置微应用
将微应用关联到一些url规则的方式实现当浏览器url发生变化时自动加载相应的微应用。
registerMicroApps(apps, lifeCycles?)注册子应用
apps微应用的一些注册信息
lifeCycles:选填
import { registerMicroApps, start } from qiankun; registerMicroApps([ {name: app-react,entry: //localhost:8080,container: #container,activeRule: /react,props: {name: kuitos,},}]); start(); //开启qiankun手动加载微应用
适用于手动加载/手动卸载一个微应用的场景
onMounted(() {state.microapp loadMicroApp({name: app-react,entry: //app.xxx.com,container: #app-react-container,props: {data: { ...$store.state, componentId: $componentId }}},// {// singular: true// sandboxfalse //默认是开启状态// })
})onUnmounted(() {state.microapp.unmount()})⚠️基座中展示的子应用关闭路由页面并未直接卸载子应用实例仍然占据内存并且下次打开也不是全新的。因此我们需要在应用卸载子应用。
2.运行时沙箱
loadApp时执行createSandbox生成运行时沙箱样式沙箱和js沙箱,qiankun 框架默认开启预加载、单例模式、样式沙箱 样式沙箱
样式沙箱包含了严格沙箱模式(默认开启)和实验性沙箱模式两种模式不可共存。
shadow DOM严格模式
shadow DOM可以将一个隐藏的、独立的DOM附加到一个元素上即微应用的容器。 Shadow host一个常规 DOM 节点Shadow DOM 会被附加到这个节点上。Shadow treeShadow DOM 内部的 DOM 树。Shadow boundaryShadow DOM 结束的地方也是常规 DOM 开始的地方。Shadow root: Shadow tree 的根节点。
我们常见的标签 、用的就是shadow dom。
实现
div idshadow-dom/div
div classwrapperstylep{color: #000;}/stylep外部部文本/p
/divscriptfunction shadowDOMIsolation(htmlString) {//1拿到当前元素的内容htmlString htmlString.trim();//2创建 shadowDOM容器const containerDom document.createElement(div);//3将内容放入shadowDom容器containerDom.innerHTML htmlString;//注意根元素只有一个const appElement containerDom.firstChild;const { innerHTML } appElement;//4清楚这个元素,便于后面追加shadowDOMappElement.innerHTMLlet shadow;if (appElement.attachShadow) {shadow appElement.attachShadow({mode: open,});} else {shadow appElement.createShadowRoot();}//追加shadowDomshadow.innerHTML innerHTML;return appElement;}
const shadowDOMSection document.querySelector(#shadow-dom);const appElement shadowDOMIsolation(div classwrapperstylep { color: purple }/stylep内部文本/p/div);shadowDOMSection.appendChild(appElement);
/script上面代码主要做了下面几件事
把当前元素的内容拿出来生成 shadowDOM再刚刚的内容放入这个 shadow DOM清除这个元素并追加 shadow DOM 即可 Scoped css 实验性模式
原理将微应用中的style全部提取出来将所有的选择器进行替换。
在目前的阶段该功能还不支持动态的、使用 标签来插入外联的样式但考虑在未来支持这部分场景。现阶段仅支持style 这种内联标签的情况 。
const styleNodes element.querySelectorAll(style) || [];forEach(styleNodes, (stylesheetElement: HTMLStyleElement) {css.process(element!, stylesheetElement, appName);});/*拿到样式节点中的所有样式规则然后重写样式选择器* 含有根元素选择器的情况用前缀替换掉选择器中的根元素选择器部分* 普通选择器将前缀插到第一个选择器的后面Ï*/process(styleNode: HTMLStyleElement, prefix: string ) {// 样式节点不为空即 stylexx/styleif (styleNode.textContent ! ) {// 创建一个文本节点内容为 style 节点内的样式内容const textNode document.createTextNode(styleNode.textContent || );// swapNode 是 ScopedCss 类实例化时创建的一个空 style 节点将样式内容添加到这个节点下this.swapNode.appendChild(textNode);const sheet this.swapNode.sheet as any; // type is missing/*** 得到所有的样式规则比如* [* {selectorText: body, style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: body { background: rgb(255, 255, 255); margin: 0px; }, …}* {selectorText: #oneGoogleBar, style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: #oneGoogleBar { height: 56px; }, …}* {selectorText: #backgroundImage, style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: #backgroundImage { border: none; height: 100%; poi…xed; top: 0px; visibility: hidden; width: 100%; }, …}* {selectorText: [show-background-image] #backgroundImage {xx}* ]*/const rules arrayifyCSSRule(sheet?.cssRules ?? []);/*** 重写样式选择器* 含有根元素选择器的情况用【前缀】替换掉选择器中的根元素选择器部分* 普通选择器将前缀插到第一个选择器的后面*/const css this.rewrite(rules, prefix);// 用重写后的样式替换原来的样式// eslint-disable-next-line no-param-reassignstyleNode.textContent css;// cleanupthis.swapNode.removeChild(textNode);return;}......}实现步骤
1获取样式节点
2创建容器swapNode
3为容器swapNode添加textNode
4获取到textNode的根div元素为其打上data-app-nameappName的标记
5用重写后的样式替换原来的样式
stylep{color:red;}
/style
div data-app-namemy-testp文本内容/p
/div!-- scoped css模式下样式变成 --
div[data-app-namemy-test] p{color:red
}cssRule:
https://www.wenjiangs.com/wiki/en-US/docs/Web/API/CSSRule
js沙箱
qiankun 的 js 沙箱分三种 proxySandBox、legacySandBox、snapshotSandBox 。
proxySandBox
原理基于Proxy实现的多例模式下的沙箱。创建变量 fakeWindow(虚拟的 window )并通过Proxy代理 fakeWindow对象所有更改都基于fakeWindow从而保证每个ProxySandbox实例之间属性互不影响。
激活时
1被激活的沙箱数1开启沙箱运行标识sandboxRunningtrue
失活时
1被激活的沙箱数-1开启沙箱运行标识sandboxRunningfalse
proxy如何获取属性值以及修改添加属性具体看下图 设置全局变量时先判断fakeWindow上是否有该属性若无则更改window若有该值则直接修改fakeWindow。
获取全局变量时先判断该属性是否为原生属性如果是原生属性则直接从window上获取非原生属性则优先从fakeWindow上获取。
总结此模式最大的特点是子应用的 window 是一个代理对象不是真正的 window子应用对 window 的操作实际上是对 fakeWindow 进行操作而不是操作真实 window
legacySandBox
legacySandbox是一种单例沙箱这个模式和proxySandBox类似基于proxy实现的。
legacySandbox原理基于 Proxy 实现的单例模式下的沙箱直接操作原生 window 对象并记录 window 对象的增删改查在每次微应用切换时初始化 window 对象。
*激活时将 window 对象恢复到上次即将失活时的状态遍历currentUpdatedPropsValueMap
失活时将 window 对象恢复为初始状态遍历addedPropsMapinSandbox和modifiedPropsOrginalMapInSandbox
关于在legacySandbox模式下获取全局属性设置全局属性。legacySandbox为了记录在修改或者添加属性时window的变更设置了三个变量池具体看下图 设置全局变量时
1window上不存在该属性则向addedPropsMapInSandbox添加该属性
2存在该属性但modifiedPropsOriginalValueMapInSandbox中不存在该属性则记录该初始值
3记录新增和修改的属性currentUpdatedPropsValueMap直接设置原生 window 对象因为是单例模式不会有其它的影响
获取全局变量时直接从window上获取
与proxySandBox的区别
legacySandbox子应用对 window 对象修改时实际上修改的就是真实 window这个代理 window 的作用是维护三个状态池分别用于子应用卸载时还原主应用的状态和子应用加载时还原子应用的状态
优缺点
优点采用代理的方式修改 window, 不用再遍历 window, 性能得到提升
缺点兼容性不如proxySandBox只能支持加载一个程序。单例模式
snapshotSandBox快照模式
在浏览器不支持 proxy 的情况下就会使用此模式。
原理基于diff方式实现的沙箱。把主应用的 window 对象做浅拷贝windowSnapshot将windowSnapshot的变更存成一个 Hash Map。之后无论微应用对 window 做任何改动当要在恢复环境时把这个 Hash Map 又应用到 window 上。
当微应用mount时
1将上一次的变更记录modifyPropsMap应用到微应用的全局window,无变更记录则跳过
2对主应用的window对象做浅拷贝用于后面还原主应用的window
当微应用umount时
1将微应用的window与快照window做Diff,Diff的结果modifyPropsMap用于下次恢复微应用环境的依据。
2根据当前的快照对象windowSnapshot还原window 举个例子说明一下
window:{a:1,b:2}
windowSnap:{a:1,b:2}
变动后的window:{a:1,b:2,c:3}
对于window和windowSnap生成diff后的对象modifyPropsMap
根据windowSnap还原window
3.资源预加载
1直接配置 prefetch 属性
2调用prefetchApps方法
import { prefetchApps } from qiankun;
prefetchApps([{ name: qianshuju_qp, entry: //qp.zhuanspirit.com }])手动加载微应用
import { loadMicroAppprefetchApps } from qiankun;
//第一种
loadMicroApp({ name: child-app, entry: http://localhost:7100, container: #child-app-container, prefetch: true, // 开启 Prefetch
}).then(() console.log(child-app 加载成功)
);当调用 loadMicroApp 方法时qiankun 会遍历子应用的 entry将其中的 prefetch 资源添加到主应用的 head 元素中。当用户访问子应用时浏览器会自动加载这些资源以便更快地加载子应用。
需要注意的是开启 Prefetch 会增加网络带宽的消耗因此我们应该根据实际情况选择是否开启 Prefetch避免资源的浪费。
基于路由配置微应用
import { registerMicroApps, start } from qiankun;
registerMicroApps([ { name: app-react, entry: //localhost:8080, container: #container, activeRule: /appreact,
}, ]);
start({ prefetch: [app-react,app-vue] });//在这里配置或者如果是多个实用prefetchApps()预加载的好处可以加速微应用的打开速度。通过在浏览器空闲时间预加载未打开的微应用资源可以减少用户等待时间提高用户体验。
4.应用间通信
从微前端的设计初衷来看我们需要尽可能少的进行应用间的通信。使我们的微前端架构可以更加灵活可控但是由于业务需要还是会存在应用间通信的。 简单介绍一下qiankun官方给出的方案
Actions 通信方案是通过全局状态池和观察者函数进行应用间通信比较适合业务划分清晰应用间通信较少的微前端应用场景。
qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信该实例有三个方法分别是
setGlobalState:设置 globalState - 设置新的值时内部将执行浅检查如果检查到 globalState 发生改变则触发通知通知到所有的观察者函数
onGlobalStateChange:注册观察者函数 - 响应 globalState 变化在 globalState 发生改变时触发该观察者函数
offGlobalStateChange:取消观察者函数 - 该实例不再响应 globalState 变化。
actions通信图解 Actions 通信方案也存在一些优缺点优点如下
使用简单官方支持性高适合通信较少的业务场景
缺点如下
子应用独立运行时需要额外配置无 Actions 时的逻辑子应用需要先了解状态池的细节再进行通信由于状态池无法跟踪通信场景较多时容易出现状态混乱、维护困难等问题
问题汇总
1弹窗的样式问题怎么解决
子应用中给弹窗设置了样式加载到主应用后主应用中查看弹窗发现样式失效。
因为比如element的弹窗默认是挂在 body 上的shadowDOM外部无法影响内部内部也无法影响外部子所以子应用中设置的全局样式会不生效。
那么另一种沙箱scoped CSS可以吗答案是肯定的不行。看了前面样式scoped css添加样式后的代码也是给容器内部的元素添加样式前缀并未给外部添加。
【解决方式】
1打包的时候给项目样式添加自定义前缀。
2vue使用scoped css \react 使用modules css均是实现组件级别的样式隔离。
3既然默认挂载在body上可以修改挂载位置。
2路由跳转问题
qiankun的子应用的router由于是子应用自己的路由所有的跳转均基于子应用的base因此没法直接通过 或者用 router.push/router.replace 跳转。
解决方式 /window.location.href链接可以跳转过去但是会刷新页面用户体验并不好将主应用的路由实例通过 props 传给子应用子应用用这个路由实例跳转。路由模式为 history 模式时通过 history.pushState() 方式跳转
3本地加载正常上线后无法加载报错跨域问题
原因父子应用的域名不一样某些业务场景下cros同源策略可能会引发跨域问题
解决找运维配置nigix解决跨域问题
4线上子应用不显示字体图标 解决webpack中配置url-loader把图标转为base64格式
常规webpack配置
module.exports {module: {rules: [{test: /.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,use: [{loader: url-loader,options: {limit: 999999999,}, // 此处随便},],},],},
};5子应用中修改window上面的属性不生效
原因微应用挂载window的 是 proxy 代理出来的 window并不是真实的window所以修改会被隔离掉
如果需要修改可以
在修改window对象之前先获取到qiankun提供的sandbox对象然后通过sandbox对象来修改window对象上的属性。例如
const { sandbox } window.__POWERED_BY_QIANKUN__;
// 修改 window 上的属性
sandbox.window.xxx new value;无论是 CSS 还是 JavaScript 沙箱都不是十全十美的我们只能通过各种约束来避免沙箱出现问题的可能。例如建立团队前缀命名空间 CSS、事件、本地存储和 Cookie以避免冲突并明确所有权。
6localStorage、sessionStorage应用覆盖问题
原因父子应用都是同一个 window所以 localStorage、sessionStorage、cookie, 这些方法就会造成数据覆盖问题注意微应用之间数据冲突、数据覆盖问题这里改写一个 setItem getItme 解决这个问题
7使用基于路由的配置打开后页面空白控制台无报错
问题1start()启动时机不对在main.js中启动页面挂载的容器并未生成导致挂载容器失败。
问题2在路由页面的created中,调用start()方法首次正常显示切换路由失败因为每次切换路由相当于重新加载一次页面挂载的id虽然一致但是页面路由切换被销毁重建因此加载失败
解决
全局注册registerMicroApps子应用改为局部注册loadMicroApp在app.vue中写挂载的容器。判断路由是属于子路由则展示容器否则展示
8子应用生命周期导出失败测试环境无法正常加载子应用、本地和线上正常。
排查思路
1推动运维对部署的文件进行排查
2nginx配置排查
3物理机/docker之间的差异问题
4代码断点查找问题
最终发现问题beetle的脚本插入逻辑与乾坤的子应用读取逻辑冲突
了解qiankun读取查找主应用的逻辑代码
找带有entry属性的找不到把最后一个
如果两种都没有找到,则需要检查修改qiankun子应用的配置文件将微应用的 name 和 Webpack 的 output.library 设为一致。
9qiankunCss样式问题
Qiankun的CSS沙箱主要是通过一种叫做Scoped CSS的技术实现的。在这种技术中每个子应用的CSS都会被添加一个独特的属性选择器这样它们就不会影响到其他应用的样式。然而这种方法也有一些潜在的问题
CSS样式污染虽然Qiankun有自己的样式隔离机制但这个机制并不完全。例如子应用的样式仍然可能会影响到全局的样式如body、html标签或者全局CSS类。这是因为CSS沙箱无法阻止子应用修改全局CSS规则。性能问题为了实现样式隔离Qiankun需要遍历并修改所有的CSS规则这在某些情况下可能会对性能产生影响。样式覆盖问题由于Qiankun是通过为每个子应用的CSS规则添加一个独特的属性选择器来实现样式隔离的所以如果子应用中有使用了!important的样式规则可能无法被正确的隔离。第三方库的样式隔离如果子应用使用了一些第三方库这些库的样式可能会泄露到全局环境中从而影响到其他子应用或者主应用的样式。动态添加的样式隔离如果子应用在运行时动态添加了一些样式例如通过document.createElement(‘style’) 或document.styleSheets[0].insertRule() 那么这些样式可能无法被正确的隔离。
总结
总是微前端是什么呢一句话在路由变化的时候去加载对应子应用的代码并在容器内跑起来。
简单说一下微前端在乾数据使用后带来的感受
好处分为以下几点
缩小项目打包体积解决系统上线拥挤问题用户使用无感知海盗乾派业务拆分、项目拆分拼装更灵活技术栈逐步统一
也是有很多麻烦之处需要消耗一定成本
避免样式污染问题需要制定一定的规范。如果你也想要tab切换不刷新(使用keep-alive)那需要做的工作更多主要是处理缓存防止堆内存溢出用chrome自带的performance monitor查看还有项目间切换时路由钩子等等的处理。
就目前来看基本没有什么问题
最后我想说
无论是那种微前端方案都会存在自己的适配用户。而采用微前端后也并不会让系统的复杂度凭空消失而是会由之前的代码层面的设计转向了系统架构设计划分的设计挑战。
应用场景上一般微前端还是应用在B端C端应用比较少。主要原因是移动端应用一般不会特别复杂。当然也有一些例外工具类的C端管理项目等。
并不是所有场景都适合微前端尤其是项目规模小、数量少的不建议使用微前端也并不是所有系统的归宿应该由场景、业务发展以及价值去决定。