微前端很好,为什么我却不使用?

语言: CN / TW / HK

本文记录我对微前端原理的探索与思考,以及微前端框架qiankun项目实践。

当下前端所存在的一些问题

  1. 在技术浪潮的推动下,由vue、react所主导的单页面应用已成为主流,但在开发中,随着业务的深入和项目的复杂,带来了逻辑定位问题、打包速度问题部署上线等等问题,往往我们可能只是更改了一行JS代码,到最后发布的时候,整个项目却要整个重新打包编译发布。

  2. 公司可能存在旧系统框架开发维护的项目,我们需要对以前的项目进行迭代或维护的时候,就不得不适应之前项目的开发环境,如果想要使用新技术,将会遇到阻碍。

  3. 单页面应用在应对大型项目的场景下,不可避免的会造成用户在首次进入的时候加载时间较长,因为几乎所有JS都在打包在一起,即使采用路由懒加载的技术进行优化,也依然无法避免这其中产生的DNS解析、三次握手、网络传输、代码解析等耗费的时间代价。

我们所希望的

  1. 能够使各个子模块或者子系统进行隔离。我们在开发或更新一个子模块的时候,只需要对这个子模块单独进行打包,发布上线,而不会影响到其他模块。各个子系统由于相互隔离,不会受限于技术栈的影响,更加轻量化,打包速度,前端性能等也会上去。
  2. 能够使各个子系统之间进行数据共享,例如用户信息,状态。
  3. 能够对JS,CSS等进行相互隔离,防止出现方法或样式污染问题。

微前端

微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于前端,即 Web 应用由单一的单页面应用转变为多个小型前端应用聚合为一的应用。然后各个前端应用还可以独立运行、独立开发、独立部署。

实现微前端的方案

1. 路由转发

我们知道单页面应用的路由控制都是在前端进行,当我们存在多个项目的时候,就算技术栈一样,A项目也指挥不了B里面的路由跳转。但是如果我们将路由跳转交给服务端,当我们访问一个路由的时候,后端进行重定向等操作,这样就会将我们的应用隔离开。 由于不存在跨域,可通过cookie、localstorage等技术进行信息共享。 因为每次路由匹配到的话,都会进行刷新,因此JS,CSS不会互相污染。

缺点:每次跳转都相当于重新刷新了一次页面,不是页面内跳转,体验较差。 优点:配置简单,可快速部署。

2. 嵌套 iframe

通过创建一个父程序,在父程序中监听路由的变化,卸载或加载相应的子程序iframe。因每一个iframe就相当于一个单独的页面,所以iframe具有天然的JS和css隔离。在信息共享方面,可以使用postMessage或者contentWindow的方式进行。

缺点: iframe样式兼容问题。分别为功能性兼容性以及业务性兼容性的问题。并且可能会存在一些安全问题。 - 主应用劫持快捷键操作 - 事件无法冒泡到顶层,针对整个应用统一处理时效 - iframe 内元素会被限制在文档树中,视窗宽高限制问题 - 无法共享基础库进一步减少包体积 - 事件通信繁琐且限制多 事件整理参考

优点:实现简单,自带沙盒特性

codesandbox

3. 纯 Web Components 开发

将每个子应用采用 Web Components 进行开发。纯 web-components 相当于自定义了一个html标签,我们就可以在任何的框架中进行使用此标签。例如:

html <template id='userInfo'> <div class='user-box'> <p class='user-name'>byeL</p> <p class='user-sex'>男</p> </div> </template> javascript class UserInfo extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userInfo'); var content = templateElem.content.cloneNode(true); this.appendChild(content); } } window.customElements.define('user-info', UserCard);Ï

```html // 直接使用

```

// 在vue中使用(需要在入口的main中引入userInfo <template> <user-info></user-info> </template>

// 在react中使用(需在入口引入userInfo class Hello extends React.Component { render() { return <div><user-info></user-info></div>; } }

缺点:之前的子系统都要进行改造,工作量大,并且通信方面较为复杂。原生组件,普及度不高,未来充满不确定性,容易翻车。

优点:每个子应用拥有独立的script和css,也可单独部署。

4. 组合式应用路由

每个子应用单独打包,部署和运行。不过需要基于父应用进行路由管理。例如:有子应用A的路由是/testA,子应用B的路由是/testB,那么父应用在监听到/testA的时候,如果此时处于/testB,那么首先会直接卸载子应用B,在去加载子应用A。

缺点:需要解决样式冲突,JS污染问题,需要有通信技术等。

优点:纯前端改造,相比于路由式,无刷新,体验更好。

5. 模块联邦

基于 Webpack 5 的一种微前端方案,使用其 Module federation 插件。

它的主要功能是可以将项目中的部分组件或全部组件暴露给外部使用,我们也可以引用其他项目中暴露的组件,从而实现模块的复用。这和发布 npm 包然后在其他项目中引用听起来有些相似,本质区别在于,npm的形式如果a包依赖b包,那么b更改发布后需要在a当中install新b包,重新发布a才能应用b的更改,而模块联邦只需要更新b即可。

缺点:依赖webpack,对老项目不友好,不支持js沙盒环境,需要自行实现,第一次需要将引用的依赖前置,可能会导致加载时间变长。

优点:未来可期。

其他方案:EMP微前端方案

方案选择与实践

目前较多的微前端方案采用的技术方案是组合式应用路由分发,那么需要解决的问题是:JS的沙盒环境、css的样式重叠或冲突、以及通信技术问题。

css冲突解决方案:

  1. 类似于vue的scoped。在打包的时候,对css选择器加上响应的属性,属性的key值是一些不重复的hash值,然后在选择的时候,使用属性选择器进行选择。
  2. 可以自定义前缀。在开发子模块之前,需要确定一个全局唯一的css前缀,然后在书写的过程中同一添加此前缀,或在根root上添加此前缀,使用less或sass作用域嵌套即可。例如:

```javascript

```

JS的沙盒环境:

首先我们需要明确的是,如果采用组合式应用路由开发,对于JS上下文有什么影响?我们做个例子:

假如我有个a子应用,会给window上挂在一个函数,函数名是hello,然后我父应用上也有一个函数名是hello,那么在子应用进行加载的时候,就会覆盖父类上的方法。

javascript // 子应用A window.hello = () => { alert('我是子应用A'); }; // 父应用 window.hello = () => { alert('我是父应用'); };

基于上面的例子,我们大致就可以看出,沙盒环境最主要做的就是一个js作用域、属性等的隔离。那么在实际的应用中,基本采用以下原理进行隔离:

1. diff方法。

当我们的子页面加载到父类的基座中的时候,我们可以生成一个map的散列表。在页面渲染之前,我们先把当前的window上的变量等都存储在这个map中。当页面卸载的时候,我们在遍历这个map,将其数据在替换回去。

```javascript class Sandbox { constructor() { this.cacheMy = {} // 存放修改的属性 this.cacheBeforeWindow = {} } showPage() { this.cacheBeforeWindow = {} for (const item in window) { this.cacheBeforeWindow[item] = window[item] }

Object.keys(this.cacheMy).forEach((p) => {
  window[p] = this.cacheMy[p]
})

}

hidePage() { for (const item in window) { if (this.cacheBeforeWindow[item] !== window[item]) { // 记录变更 this.cacheMy[item] = window[item] // 还原window window[item] = this.cacheBeforeWindow[item] } } } }

const diffSandbox = new Sandbox() // 模拟页面激活 diffSandbox.showPage() // 激活沙箱 window.info = '我是子应用' console.log('页面激活,子应用对应的值', window.info) // 模拟页面卸载 diffSandbox.hidePage() // 模拟页面激活 console.log('页面卸载后,子应用的对应的值', window.info) diffSandbox.showPage() // 重新激活 console.log('页面激活,子应用对应的值', window.info) ```

2. 使用代理

这里需要用到es6的新特性:proxy查看MDN Proxy介绍。原理是监听get和set方法,针对当前路由进行window的属性或方法的存取

```javascript const windowMap = new Map(); const resertWindow = {};

let routerUrl = ''; const handler = { get: function(obj, prop) { const tempWindow = windowMap.get(routerUrl); console.log(windowMap, routerUrl); return tempWindow[prop]; }, set: function(obj, prop, value) { if (!windowMap.has(routerUrl)) { windowMap.set(routerUrl, JSON.parse(JSON.stringify(resertWindow))); } const tempWindow = windowMap.get(routerUrl); tempWindow[prop] = value; // console.log(obj, prop, value); }, };

let proxyWindow = new Proxy(resertWindow, handler); // 首先是父类的a属性. proxyWindow.a = '我是父类的a属性的值';

// 改变路由到子类 routerUrl = 'routeA'; proxyWindow.a = '我是routerA的a属性的值'

// 改变路由到父类 routerUrl = ''; console.log(proxyWindow.a);

// 改变路由到子类 routerUrl = 'routeA'; console.log(proxyWindow.a); ```

3. iframe自带css和js沙盒隔离。

single-spa 框架

通过上面的了解,我们知道要实现组合式应用路由开发,首先需要设计一套前端基座,用于调控子项目,single-spa就是其中最广为人知的框架,先以Vue为例看下如何使用它。

1. 在子项目中引入single-spa-vue

npm install single-spa-vue --save

2. 在子类的main.js中加入single-spa-vue相应的生命周期

```javascript import Vue from 'vue' import App from './App.vue' import router from './router' import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = { el: '#microApp', router, render: h => h(App) }

// 支持应用独立运行、部署,不依赖于基座应用 if (!process.env.isMicro) { delete appOptions.el new Vue(appOptions).$mount('#app') } // 基于基座应用,导出生命周期函数 const vueLifecycle = singleSpaVue({ Vue, appOptions }) // 启动生命周期 export function bootstrap (props) { console.log('app2 bootstrap') return vueLifecycle.bootstrap(() => {}) } // 挂载生命周期 export function mount (props) { console.log('app2 mount') return vueLifecycle.mount(() => {}) } // 卸载生命周期 export function unmount (props) { console.log('app2 unmount') return vueLifecycle.unmount(() => {}) }

```

3. 新建vue.config.js文件,并设置导出格式为umd

javascript const package = require('./package.json') module.exports = { // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载 publicPath: '//localhost:8082', // 开发服务器 devServer: { port: 8082, }, configureWebpack: { // 导出 umd 格式的包,在全局对象上挂载属性 package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数 output: { // library 的值在所有子应用中需要唯一 library: package.name, libraryTarget: 'umd', }, }, }

经过以上几步操作,一个微前端的子项目基本就创建完成了,我们可以看出single-spa在这里只做了一件事,那就是提供生命周期的概念。

4. 创建父类基座。

4.1. 父类基座引入single-spa

javascript npm install single-spa --save

4.2. 改造父类main.js

```javascript import Vue from 'vue' import App from './App.vue' import router from './router' import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

// 远程加载子应用 function createScript(url) { return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = url script.onload = resolve script.onerror = reject const firstScript = document.getElementsByTagName('script')[0] firstScript.parentNode.insertBefore(script, firstScript) }) }

// 记载函数,返回一个 promise function loadApp(url, globalVar) { // 支持远程加载子应用 return async () => { // await createScript(url + '/js/chunk-vendors.js') await createScript(url + '/js/app.js') // 这里的return很重要,需要从这个全局对象中拿到子应用暴露出来的生命周期函数 return window[globalVar] } }

// 子应用列表 const apps = [ { // 子应用名称 name: 'app1', // 子应用加载函数,是一个promise app: loadApp('http://localhost:8083', 'app1'), // 当路由满足条件时(返回true),激活(挂载)子应用 activeWhen: (location) => location.pathname.startsWith('/app1'), // 传递给子应用的对象 customProps: {}, }, { name: 'app2', app: loadApp('http://localhost:8082', 'app2'), activeWhen: (location) => location.pathname.startsWith('/app2'), customProps: {}, }, ]

// 注册子应用 for (let i = apps.length - 1; i >= 0; i--) { registerApplication(apps[i]) }

new Vue({ router, mounted() { // 启动 start() }, render: (h) => h(App), }).$mount('#app') ```

经过上面改造完成之后,一个父类基座应用也就创建完毕了,看看它主要做了哪些事:

  1. loadApp主要是根据资源地址去请求资源
  2. createScript主要是将请求回来的script添加到页面中。
  3. apps就是一些子类应用的描述。
  4. registerApplication就是将这些子应用注册到single-spa中。

现在一个由single-spa改造完成的微前端项目就完成了,我们再回头看看其原理流程图

2022/04/1651041217064.png

从中我们可以看出浏览器首次打开父类应用的时候,第一步就是先调用registerApplication注册app,接下来判断当前的路由是属于哪一个子应用的,他的判断依据就是apps中的activeWhen,接下来就会将当前的子应用划分状态,appToLoad,appToUnmounted, appToMounted。接下来根据子应用的状态,先去执行需要卸载的子应用,卸载完成之后,就会去执行状态为appToLoad, appToMounted的子应用,那么在最后在执行相应的回调函数,也就是我们在子应用中注册的那些生命周期。

前面铺垫了这么多,我们发现single-spa就只做了两件事:提供生命周期概念与负责调度子应用的生命周期,对于组合式应用路由模式还有其它尚未解决的问题怎么办呢,这时候就要讲下一个框架:qiankun

qiankun 框架

qiankun 是一个基于 single-spa 的微前端实现库,样式隔离、JS沙盒环境、空闲预加载这些qiankun都帮我们做了,不需要自己去处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。

说到资源加载方案,可分为两种:

1. JS Entry

该方式通常是子应用将资源打成一份 script,这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。

2. HTML Entry (qiankun所采用的)

这种方式更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。

qiankun微前端实践

项目架构简述:1. 基座采用:vue,2. 子项目:vue 2/3 、 react

说明:vue 通过 vue cli 进行创建, react 通过 create-react-app 进行项目创建。

完整示例 Demo -> Micro-Frontends-Practice,以下篇幅为过程实现,代码片段经过反复修改,有可能存在部分错误,具体实现参考demo。

在创建完父类项目以及子类项目之后,安装好 qiankun 依赖库。

javascript npm install qiankun --save

在安装完成之后,我们需要对其做相应的处理。分为三部分:

1. 在父类中创建微前端基座

首先在 src 下创建一个子应用的配置文件,我们叫做 micro_app.js

```javascript // src/micro_app.js

const microApps = [ { name: 'micro_vue', entry: '//localhost:8081/', activeRule: '/micro_vue', container: '#subapp-viewport', // 子应用挂载的div props: { routerBase: '/micro_vue', // 下发路由给子应用,子应用根据该值去定义qiankun环境下的路由 }, }, { name: 'micro_react', entry: '//localhost:10100/', activeRule: '/micro_react', container: '#subapp-viewport', // 子应用挂载的div props: { routerBase: '/micro_react', }, }, ]

export default microApps ```

在设置完子应用的配置文件之后,我们需要改写 App.vue

```javascript // 为了简单,只是改写了模板文件,
```

接下来是更改 main.js。在 main.js 中最主要做的事情就是将之前配置的子应用进行注册并且启动 qiankun 框架服务

```javascript import Vue from 'vue' import App from './App.vue' import store from './store' import router from './router' import { registerMicroApps, start } from 'qiankun' import appConfig from './micro_app' Vue.config.productionTip = false

new Vue({ router, store, render: (h) => h(App), }).$mount('#app')

// 注册子应用 registerMicroApps(appConfig, { beforeLoad: (app) => { console.log('before load app.name====>>>>>', app.name) }, beforeMount: [ (app) => { console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name) }, ], afterMount: [ (app) => { console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name) }, ], afterUnmount: [ (app) => { console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name) }, ], }) // 启动qiankun start()
```

父类的基座创建完成之后,接下来就是子应用的注册了。

2. vue 接入 qiankun

首先需要在项目根目录下建立 vue.config.js 用来改写 webpack 的相应配置。在 src 下创建 public-path.js。public-path 用来改写 webpack 打包好之后,通过那个路径可以访问到打包后的文件。

```javascript // vue.config.js const { name } = require('./package.json')

module.exports = { configureWebpack: { output: { library: ${name}-[name], libraryTarget: 'umd', jsonpFunction: webpackJsonp_${name}, }, }, devServer: { port: 8081, // 在.env中VUE_APP_PORT=7788,与父应用的配置一致 headers: { 'Access-Control-Allow-Origin': '*', // 主应用获取子应用时跨域响应头 }, }, } javascript // public-path.js

;(function () { // 用来判断是否运行在乾坤的框架下 if (window.POWERED_BY_QIANKUN) { // eslint-disable-next-line no-undef webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN } })() ```

修改 router 下的 Index,我们只需要导出对应的路径定义即可,不需要导出对应的 vue-router 实例,如果直接导出 vue-router 实例的话,会与我们在父类 micro_app.js 中设置的 routerBase 匹配不上。导致渲染子应用失败。

javascript import Home from '../views/Home.vue' const routes = [ { path: '/', name: 'Home', component: Home, }, { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), }, ] export default routes

接下来就是需要改写 main.js。同样是生命周期的引入。并且注册 vue-vouter 实例。

```javascript import './public-path' // 注意需要引入public-path import Vue from 'vue' import App from './App.vue' import routes from './router' import store from './store' import VueRouter from 'vue-router' Vue.use(VueRouter) Vue.config.productionTip = false let instance = null window.projectName = 'micro_vue' function render(props = {}) { // 这个是我们在父类注册的时候定义的那些参数。 const { container, routerBase } = props const router = new VueRouter({ base: window.POWERED_BY_QIANKUN ? routerBase : process.env.BASE_URL, mode: 'history', routes, }) instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app') }

if (!window.POWERED_BY_QIANKUN) { render() }

export async function bootstrap() { console.log('[vue] vue app bootstraped') }

export async function mount(props) { console.log('[vue] props from main framework', props)

render(props) }

export async function unmount() { instance.$destroy() instance.$el.innerHTML = '' instance = null } ```

最后需要在根目录下创建.env 文件。 javascript VUE_APP_PORT = 8081

以上步骤完成之后,我们只需要启动项目我们既可以通过对应的路由进行访问了http://localhost:xxxx/micro_vue

完整示例 Demo -> Micro-Frontends-Practice

3. react 项目的接入

在 src 下创建 public-path.js 文件

javascript if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }

接下来修改index.js

首先我们需要引入react-router-dom ```javascript // npm install react-router-dom --save import { BrowserRouter as Router } from 'react-router-dom' // 接下来我们需要对render进行改写 function render(props) { const { container } = props ReactDOM.render( , container ? container.querySelector('#root') : document.querySelector('#root'), ) } // 然后加入生命周期即可 export async function bootstrap() { console.log('ReactMicroApp bootstraped') }

/* * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 / export async function mount(props) { console.log('ReactMicroApp mount', props) render(props) }

/* * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 / export async function unmount() { console.log('ReactMicroApp unmount') ReactDOM.unmountComponentAtNode(document.getElementById('root')) } ```

修改 webpack 配置
方式 1:

运行 npm run eject

修改config/webpackDevServer.config.js

javascript module.exports = function (proxy, allowedHost) { return { headers: { 'Access-Control-Allow-Origin': '*', // 表示允许跨域 }, ... }

接下来修改config/webpack.config.js

javascript return { ... output: { // The build folder. ... // 微应用配置 library: `${appPackageJson.name}-[name]`, libraryTarget: 'umd' }, ... }

方式2:

安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired dash npm i -D @rescripts/cli 根目录新增 .rescriptsrc.js

```javascript const { name } = require('./package')

module.exports = { webpack: (config) => { config.output.library = ${name}-[name] config.output.libraryTarget = 'umd' config.output.jsonpFunction = webpackJsonp_${name} config.output.globalObject = 'window'

return config

},

devServer: (_) => { const config = _

config.headers = {
  'Access-Control-Allow-Origin': '*',
}
config.historyApiFallback = true
config.hot = false
config.watchContentBase = false
config.liveReload = false

return config

}, } ```

修改 package.json

json - "start": "react-scripts start", + "start": "rescripts start", - "build": "react-scripts build", + "build": "rescripts build", - "test": "react-scripts test", + "test": "rescripts test",

最后,同理在根目录下创建一个.env 文件,

javascript PORT = 10100 BROWSER = none

然后启动 react 项目,之后就可以通过http://localhost:xxx/micro_react进行访问了

完整示例 Demo -> Micro-Frontends-Practice

数据交互

qiankun 内部使用 initGlobalState(state)定义全局状态,该方法执行后返回一个 MicroAppStateActions 实例,实例中包含三个方法,分别是 onGlobalStateChange、setGlobalState、offGlobalStateChange。

默认会通过 props 将通信方法传递给子应用。

  • onGlobalStateChange: 当 global 有数据发生更改的时候,就会触发回调函数,回调函数传入的是更改后的 globalState
  • setGlobalState: 更改 globalstate 的方法
  • offGlobalStateChange: 移除监听,微应用执行 umount 时会默认调用。

如何使用:

首先我们在父类基座的 src 下建一个文件为action.js文件。

```javascript import { initGlobalState } from 'qiankun' import store from './store'

const initialState = { //这里写初始化数据 userName: 'testInfo', userId: '1232', }

// 初始化 state const actions = initGlobalState(initialState) actions.onGlobalStateChange((state, prev) => { //监听公共状态的变化 console.log('主应用: 变更前') console.log(prev) console.log('主应用: 变更后') console.log(state) }) export default actions ```

在父类如何获取初始的GlobalState:

javascript // 当onGlobalStateChange的最后一个属性为true的时候,表示立即获取,不需要等待数据发生变更 action.onGlobalStateChange((state) => { console.log('全局状态发生改变') // this.mes = state this.userInfo.userName = state.userName console.log(state) }, true) 在父类中更改GlobalState:

javascript action.setGlobalState({ userName: '更改之后的名称', userId: 'new Id', })

如何在子类中进行使用:

首先在子类中 src 下创建一个 action.js

```javascript function emptyAction() { //设置一个actions实例 // 提示当前使用的是空 Action console.warn('Current execute action is empty!') }

class Actions { // 默认值为空 Action actions = { onGlobalStateChange: emptyAction, setGlobalState: emptyAction, }

/* * 设置 actions / setActions(actions) { this.actions = actions }

/* * 映射 / onGlobalStateChange(...args) { return this.actions.onGlobalStateChange(...args) }

/* * 映射 / setGlobalState(...args) { return this.actions.setGlobalState(...args) } }

const actions = new Actions() export default actions ```

之后我们在子类的 main.js 中接受 props 上传递过来的方法

javascript export async function mount(props) { console.log('[vue] props from main framework', props) actions.setActions(props) render(props) }

之后的获取数据,以及更新与父类的相同。

结尾

总结我对微前端的思考,我认为它或许会成为主流,但未来不一定完全属于微前端。因为我们希望的一直都是——可以使用熟悉的技术、或是新潮的技术去完成开发工作,微前端既是在这种情况下诞生的。而解决大型系统各方各面的问题,可以是统一治理,也可以是分而治之,微前端则是后者。 所以我们当下既要全面了解各种微前端的技术,也不用急于去追随潮流,强行微前端化。如果争取在实践中获得更多经验与思考,不论什么技术都会有派上用场的时候。