当前位置: 首页 > news >正文

微前端探秘:初始微前端、现有方案和未来趋势

初识微前端

微前端是什么

概念: 微前端是指存在于浏览器中的微服务。

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一体的应用。这就意味着前端应用的拆分,拆分后的应用实现应用自治、单一职责、技术栈无关三大特性,再进行基座模式或自由组合的模式进行聚合,达到微前端的目的。

自由组组织模式指的就是:系统内部子系统之间能自行按照某种规则形成一定的结构或功能。

微前端的几个基本要素: 技术栈无关、应用隔离、独立开发。

核心:拆,合

微前端背景

在前端框架、技术、概念层出不穷,且随着前端标准的演进,前端已经具备更好的性能和开发效率,但是随之而来的是应用的复杂度更高、涉及的团队规模更广、更高的性能要求,应用复杂度已经成为阻塞业务发展的重要瓶颈。

微前端就是诞生在这日益复杂化的场景中。

为什么使用微前端

为了解决团队平台系统多且相互独立,系统体量大且页面多,开发效率低、接入成本高。

当前应用痛点:

  • 项目中的组件和功能模块会越来越多,导致整个项目的打包速度变慢;
  • 因为文件夹的数量会随着功能模块的增多而增多,查找代码会变得越来越慢;
  • 如果只改动其中一个模块的情况,需要把整个项目重新打包上线;
  • 所有的项目都基本只能使用同一技术框架,不便引入新技术栈。如: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// sandbox:false //默认是开启状态// })
})onUnmounted(() => {state.microapp.unmount()})

⚠️:基座中展示的子应用,关闭路由页面并未直接卸载子应用实例,仍然占据内存,并且下次打开也不是全新的。因此我们需要在应用卸载子应用。

2.运行时沙箱

loadApp时执行createSandbox,生成运行时沙箱(样式沙箱和js沙箱),qiankun 框架默认开启预加载、单例模式、样式沙箱

样式沙箱

样式沙箱包含了严格沙箱模式(默认开启)和实验性沙箱模式,两种模式不可共存。

shadow DOM严格模式

shadow DOM可以将一个隐藏的、独立的DOM附加到一个元素上,即微应用的容器。

  • Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
  • Shadow tree:Shadow DOM 内部的 DOM 树。
  • Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
  • Shadow root: Shadow tree 的根节点。

我们常见的标签 、用的就是shadow dom。

实现:

<div id="shadow-dom"></div>
<div class="wrapper"><style>p{color: #000;}</style><p>外部部文本</p>
</div>
 <script>function 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.innerHTML=""let 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 class="wrapper"><style>p { color: purple }</style><p>内部文本</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 = '') {// 样式节点不为空,即 <style>xx</style>if (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 = arrayify<CSSRule>(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-name=appName的标记

(5)用重写后的样式替换原来的样式

<style>p{color:red;}
</style>
<div data-app-name="my-test"><p>文本内容</p>
</div><!-- scoped css模式下样式变成: -->
div[data-app-name="my-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,开启沙箱运行标识sandboxRunning:true

失活时:

(1)被激活的沙箱数-1,开启沙箱运行标识sandboxRunning:false

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的变更,设置了三个变量池,具体看下图:

设置全局变量时:

(1)window上不存在该属性,则向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 { loadMicroApp,prefetchApps } 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)打包的时候,给项目样式添加自定义前缀。

(2)vue使用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,以避免冲突并明确所有权。

(6)localStorage、sessionStorage应用覆盖问题

原因:父子应用都是同一个 window,所以 localStorage、sessionStorage、cookie, 这些方法就会造成数据覆盖问题:注意微应用之间数据冲突、数据覆盖问题,这里改写一个 setItem getItme 解决这个问题

(7)使用基于路由的配置,打开后页面空白,控制台无报错

问题1:start()启动时机不对,在main.js中启动,页面挂载的容器并未生成,导致挂载容器失败。

问题2:在路由页面的created中,调用start()方法,首次正常显示,切换路由失败,因为每次切换路由相当于重新加载一次页面,挂载的id虽然一致,但是页面路由切换被销毁重建因此加载失败

解决:

  • 全局注册(registerMicroApps)子应用改为局部注册(loadMicroApp)
  • 在app.vue中写挂载的容器。判断路由是属于子路由则展示容器,否则展示

(8)子应用生命周期导出失败,测试环境无法正常加载子应用、本地和线上正常。

排查思路:

(1)推动运维对部署的文件进行排查

(2)nginx配置排查

(3)物理机/docker之间的差异问题

(4)代码断点查找问题

最终发现问题:beetle的脚本插入逻辑与乾坤的子应用读取逻辑冲突

了解qiankun读取查找主应用的逻辑代码:

  • 找带有entry属性的
  • 找不到把最后一个

如果两种都没有找到,则需要检查修改qiankun子应用的配置文件,将微应用的 name 和 Webpack 的 output.library 设为一致。

(9)qiankunCss样式问题

Qiankun的CSS沙箱主要是通过一种叫做Scoped CSS的技术实现的。在这种技术中,每个子应用的CSS都会被添加一个独特的属性选择器,这样它们就不会影响到其他应用的样式。然而,这种方法也有一些潜在的问题:

  1. CSS样式污染:虽然Qiankun有自己的样式隔离机制,但这个机制并不完全。例如,子应用的样式仍然可能会影响到全局的样式,如body、html标签或者全局CSS类。这是因为,CSS沙箱无法阻止子应用修改全局CSS规则。
  2. 性能问题:为了实现样式隔离,Qiankun需要遍历并修改所有的CSS规则,这在某些情况下可能会对性能产生影响。
  3. 样式覆盖问题:由于Qiankun是通过为每个子应用的CSS规则添加一个独特的属性选择器来实现样式隔离的,所以如果子应用中有使用了!important的样式规则,可能无法被正确的隔离。
  4. 第三方库的样式隔离:如果子应用使用了一些第三方库,这些库的样式可能会泄露到全局环境中,从而影响到其他子应用或者主应用的样式。
  5. 动态添加的样式隔离:如果子应用在运行时动态添加了一些样式(例如,通过document.createElement(‘style’)document.styleSheets[0].insertRule() ),那么这些样式可能无法被正确的隔离。

总结

总是微前端是什么呢?一句话:在路由变化的时候,去加载对应子应用的代码,并在容器内跑起来。

简单说一下微前端在乾数据使用后带来的感受:

好处,分为以下几点:

  • 缩小项目打包体积
  • 解决系统上线拥挤问题
  • 用户使用无感知
  • 海盗乾派业务拆分、项目拆分拼装更灵活
  • 技术栈逐步统一

也是有很多麻烦之处,需要消耗一定成本:

  • 避免样式污染问题,需要制定一定的规范。
  • 如果你也想要tab切换不刷新(使用keep-alive),那需要做的工作更多,主要是处理缓存,防止堆内存溢出(用chrome自带的performance monitor查看),还有项目间切换时路由钩子等等的处理。

就目前来看,基本没有什么问题~

最后我想说:

无论是那种微前端方案,都会存在自己的适配用户。而采用微前端后,也并不会让系统的复杂度凭空消失,而是会由之前的代码层面的设计转向了系统架构设计划分的设计挑战。

应用场景上,一般微前端还是应用在B端,C端应用比较少。主要原因是移动端应用一般不会特别复杂。当然也有一些例外:工具类的C端管理项目等。

并不是所有场景都适合微前端,尤其是项目规模小、数量少的不建议使用,微前端也并不是所有系统的归宿,应该由场景、业务发展以及价值去决定。

相关文章:

微前端探秘:初始微前端、现有方案和未来趋势

初识微前端 微前端是什么 概念&#xff1a; 微前端是指存在于浏览器中的微服务。 微前端是一种类似于微服务的架构&#xff0c;它将微服务的理念应用于浏览器端&#xff0c;即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一体的应用。这就意味着前端应用…...

运维(SRE)成长之路-第2天 文本编辑工具之神VIM

vi和vim简介 在Linux中我们经常编辑修改文本文件&#xff0c;即由ASCII, Unicode 或其它编码的纯文字的文件。之前介绍过nano&#xff0c;实际工作中我们会使用更为专业&#xff0c;功能强大的工具 文本编辑种类&#xff1a; 全屏编辑器&#xff1a;nano&#xff08;字符工具…...

45从零开始学Java之详解static修饰符、静态变量和静态方法

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 在前一篇文章中&#xff0c;壹哥给大家讲解了abstract关键字&#xff0c;从而我们掌握了抽象类与抽象…...

电商超卖,从业务到设计

编辑导语&#xff1a;超卖这一概念的定义可以从不同层面进行阐述&#xff0c;比如平台层面、渠道层面、仓库层面等。而假设因超卖导致订单难以履行&#xff0c;则容易让用户体验“打折”。为什么有时电商超卖的现象会发生&#xff1f;可以从哪些角度来降低超卖导致的风险&#…...

【MySQL】表的约束

表的约束 表的约束1. 空属性2. 默认值3. 列描述4. zerofill&#xff08;自动补零&#xff09;5. 主键—primary key5.1 复合主键 6. 自增长—auto_increment7.唯一键 --- unique8. 外键 --- foreign key…reference9. 综合案例 表的约束 真正约束字段的是数据类型&#xff0c;…...

【计算机网络】第一章 概述(下)

文章目录 第一章 概述1.5 计算机网络的性能指标1.5.1 速率1.5.2 带宽1.5.3 吞吐量1.5.4 时延 1.6 计算机网络体系结构1.6.1 常见的体系结构1.6.2 分层的必要性1.6.4 体系结构中的专用术语 1.8 习题 第一章 概述 1.5 计算机网络的性能指标 常用的 计算机网络 的性能指标有以下 …...

化工园区人员全过程轨迹化安全解决方案

1、项目背景 化工园区化工厂是生产安全重点单位&#xff0c;对人员定位管理需求强烈。对人员定位主要需求是&#xff1a;一般区域人数统计、人员轨迹、重点区域人员实时精准定位。 华安联大安全化工园区人员全过程轨迹化安全解决方案通过人员实时定位管理、移动轨迹追溯、险情…...

Java泛型中的T、E、K、V、?通配符,你确定都了解吗?

目录 前言 泛型带来的好处 泛型中通配符 小结 前言 Java 泛型&#xff08;generics&#xff09;是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制&#xff0c;该机制允许开发者在编译时检测到非法的类型。 泛型的本质是参数化类型&#xff0c;也就是说所操…...

Jenkins部署及使用

Jenkins 1.定义 1.Jenkins是一款开源CI/CD软件&#xff0c;用于自动化各种任务&#xff0c;包括构建、测试和部署软件 1.CI/CD 1.CI&#xff1a;持续集成(Continuous Integration) 1.协同开发是目前主流的开发方式&#xff0c;一般由多位开发人员同时处理同一个应用的不同模块…...

UML类图(二)

相信希望&#xff0c;相信自己 上一章简单介绍了 设计模式的原则(一), 如果没有看过,请观看上一章 本文参考文章: 常见UML符号详解 UML (Unified modeling language) 统一建模语言&#xff0c;是一种用于软件系统分析和设计的语言工具&#xff0c; 它用于帮助软件开发人员进行…...

【IoU全总结】GIoU, DIoU, CIoU, EIoUFocal, αIoU, SIoU,WIoU【基础收藏】

&#x1f951; Welcome to Aedream同学 s blog! &#x1f951; 并不存在效果一定优秀的IoU&#xff0c;需要结合自己的网络、数据集试验。 不想深究原理可直接跳转总结。文内公式均为手打&#xff0c;非图片&#xff0c;方便查看 文章目录 L1 Loss&#xff0c;L2Loss&#xff0…...

docker 安装 mysql

第一步&#xff0c;安装docker ,确保centos环境符合要求&#xff0c;有网 yum install docker -y 第二步&#xff1a;拉取mysql 首先可以先查询支持的mysql&#xff1a; search # 拉取镜像 docker pull mysql # 或者 docker pull mysql:latest # 以上两个命令是一致的&…...

Java 流程控制之 for 循环

Java语言中的for循环是一种常用的循环结构&#xff0c;用于重复执行一段代码&#xff0c;直到指定的条件不再成立。在本篇博客中&#xff0c;我们将深入探讨Java中的for循环&#xff0c;包括其语法、用法和示例。 一、for循环的基本语法 for循环的基本语法如下&#xff1a; …...

Kubernetes那点事儿——暴露服务之Ingress

Kubernetes那点事儿——暴露服务之Ingress 前言一、ingress负载均衡器Ingress Controller路由规则Ingress 二、Ingress Controller三、案例 前言 在 k8s 集群中&#xff0c;如果我们将服务暴露出来&#xff0c;提供访问&#xff0c;可以使用Nodeport方式&#xff0c;但是Nodepo…...

八股文总结

文章目录 项目介绍1.不动产项目项目难点机器学习算法调研图像提取算法调研数据集-ImageNetXceptionVGGInceptionDensenetMobilenet 系统流程图 2.图书项目技术栈ShiroMybatisMyBatis:Mybatis Plus: 面试问题 Java基础基本数据类型反射接口和抽象类异常代理模式1. 静态代理2. 动…...

【浅学 MyBatis 】

MyBatis 笔记记录 一、MyBatis基础1. MyBatis介绍及快速入门2. 相关API介绍2.1 Resources2.2 SqlSessionFactory&&SqlSessionFactoryBuilder2.3 SqlSession 3. 映射配置文件4. 核心配置文件4.1 规范写法4.2 参数和返回类型_起别名 5. 引入Log4j 二、MyBatis进阶1. 接口…...

Windows版Redis安装

最近电脑重装了系统&#xff0c;很多常用的软件、应用都没有了&#xff0c;所以需要重新装&#xff0c;所以想借此机会把一些安装比较复杂的应用的安装过程&#xff0c;重新记录一下&#xff0c;方便后续&#xff0c; 安装 Redis默认只有Linux的版本&#xff0c;但是微软为了更…...

Flume面试题二十道

什么是Apache Flume&#xff1f; 参考答案&#xff1a;Apache Flume是一个可靠、分布式、可扩展的日志收集和聚合系统。它用于将大量的日志数据从不同的源&#xff08;如Web服务器、应用程序日志&#xff09;传输到目标&#xff08;如Hadoop、HBase&#xff09;中进行存储和分析…...

单链表OJ题:LeetCode--138.复制带随即指针的链表

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下LeetCode中第138道单链表OJ题&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; 数据结构与算法专栏&#xff1a;数据结构与算法 个 人…...

Chapter7: SpringBoot与数据访问

尚硅谷SpringBoot顶尖教程 1. JDBC 1.1 依赖及配置 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency><groupId>mysql</groupId…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明

AI 领域的快速发展正在催生一个新时代&#xff0c;智能代理&#xff08;agents&#xff09;不再是孤立的个体&#xff0c;而是能够像一个数字团队一样协作。然而&#xff0c;当前 AI 生态系统的碎片化阻碍了这一愿景的实现&#xff0c;导致了“AI 巴别塔问题”——不同代理之间…...

sqlserver 根据指定字符 解析拼接字符串

DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

中医有效性探讨

文章目录 西医是如何发展到以生物化学为药理基础的现代医学&#xff1f;传统医学奠基期&#xff08;远古 - 17 世纪&#xff09;近代医学转型期&#xff08;17 世纪 - 19 世纪末&#xff09;​现代医学成熟期&#xff08;20世纪至今&#xff09; 中医的源远流长和一脉相承远古至…...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?

在工业自动化持续演进的今天&#xff0c;通信网络的角色正变得愈发关键。 2025年6月6日&#xff0c;为期三天的华南国际工业博览会在深圳国际会展中心&#xff08;宝安&#xff09;圆满落幕。作为国内工业通信领域的技术型企业&#xff0c;光路科技&#xff08;Fiberroad&…...

提升移动端网页调试效率:WebDebugX 与常见工具组合实践

在日常移动端开发中&#xff0c;网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时&#xff0c;开发者迫切需要一套高效、可靠且跨平台的调试方案。过去&#xff0c;我们或多或少使用过 Chrome DevTools、Remote Debug…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...