介绍微前端微前端的概念是由 ThoughtWorks 在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独…
介绍
微前端
微前端的概念是由 ThoughtWorks 在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
它主要解决了两个问题:
- 1、随着项目迭代应用越来越庞大,难以维护。
- 2、跨团队或跨部门协作开发项目导致效率低下的问题。

开源框架
字节跳动: Garfish
京东: micro-app
蚂蚁金服: qiankun (qiankun是基于 single-spa 的一层封装)
比较
Garfish
优势
劣势
- 需要配置的配置项较多
- 基座应用必须为react
- 懒加载、刷新有时有奇怪的问题
- 不支持嵌套(子应用不能既是主又是子)
micro-app
优势
- 基于webComponent技术
- 嵌入无需新增依赖
- 应用无限制
- 沙盒功能
- 预加载
劣势
- 静态资源有时会有问题
- angular子应用无法使用懒加载
- 不支持嵌套(子应用不能既是主又是子)
鉴于实际上手难度以及使用场景我们决定基于micro-app来实现我们的微前端方案
实操
主应用(预算planning项目)
1、安装依赖
1
| npm i @micro-zoe/micro-app --save
|
2、在入口处引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // index.js import microApp from '@micro-zoe/micro-app' microApp.start()
// 实际代码(main.js)
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; // entry import microApp from '@micro-zoe/micro-app' microApp.start() if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch((err) => console.error(err));
|
3、增加对WebComponent的支持
在app/app.module.ts中添加CUSTOM_ELEMENTS_SCHEMA到@NgModule.schemas
1 2 3 4 5
| // app/app.module.ts import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], })
|
4、分配路由给子应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| // app/app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { MyAngularComponent } from "./my-angular/my-angular.component"; import { MyReactComponent } from "./my-react/my-react.component"; import { MyVueComponent } from "./my-vue/my-vue.component"; const routes: Routes = [ { // 👇 非严格匹配,/examples/angular/* 都指向 my-angular 页面 path: 'examples/angular', children: [{ path: '**', component: MyAngular }] }, { path: 'examples/react', children: [{ path: '**', component: MyReactComponent }] }, { path: 'examples/vue', children: [{ path: '**', component: MyVueComponent }] }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule { }
|
5、在页面中嵌入子应用
1 2 3 4 5 6 7 8 9
| <micro-app disableScopecss name='app-angular' url='http://localhost:3000/' baseroute='/examples/angular'></micro-app>
<!-- app/my-vue/my-vue.component.html -->
<micro-app disableScopecss name='app-vue' url='http://localhost:8080/' baseroute='/examples/vue'></micro-app>
<!-- app/my-react/my-react.component.html -->
<micro-app disableScopecss name='app-react' url='http://localhost:3001/' baseroute='/examples/react'></micro-app>
|
子应用
Angular
1、关闭热更新
1 2 3
| "scripts": { "start": "ng serve --live-reload false", },
|
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // app/app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { APP_BASE_HREF } from '@angular/common'; const routes: Routes = [...];
@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], // 👇 设置基础路由 providers: [{ provide: APP_BASE_HREF, // angular子应用实测需要此中方式获取路径,可能是文档更新不齐全 // @ts-ignore __MICRO_APP_BASE_ROUTE__ 为micro-app传入的基础路由 useValue: (window["__MICRO_APP_PROXY_WINDOW__"] && window["__MICRO_APP_PROXY_WINDOW__"]["__MICRO_APP_BASE_ROUTE__"]) || '/', }] }) export class AppRoutingModule { }
|
3、设置publicPath
步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内容
1 2 3 4 5
| // __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量 if (window["__MICRO_APP_PROXY_WINDOW__"] && window["__MICRO_APP_PROXY_WINDOW__"]["__MICRO_APP_ENVIRONMENT__"]) { // eslint-disable-next-line __webpack_public_path__ = window["__MICRO_APP_PROXY_WINDOW__"]["__MICRO_APP_PUBLIC_PATH__"] }
|
步骤2: 在子应用入口文件的最顶部引入public-path.js
1 2
| // entry import './public-path'
|
4、监听卸载
子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。
1 2 3 4 5 6 7 8 9 10 11 12
| // main.ts let app = null; platformBrowserDynamic() .bootstrapModule(AppModule) .then((res: NgModuleRef<AppModule>) => { app = res }) // 监听卸载操作 window.addEventListener('unmount', function () { app.destroy(); app = null; })
|
React
1、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
1 2 3 4 5 6 7 8
| // router.js import { BrowserRouter, Switch, Route } from 'react-router-dom' export default function AppRoute () { return ( // 👇 设置基础路由,如果没有设置baseroute属性,则window.__MICRO_APP_BASE_ROUTE__为空字符串 <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}> ... </BrowserRouter> ) }
|
或者
1 2 3 4 5 6 7 8 9 10 11
| // index.js const router = createBrowserRouter([ { path: '/', element: <App /> } ], { // @ts-ignore basename: (window['__MICRO_APP_BASE_ROUTE__']) || '/' })
|
2、设置publicPath
步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内容
1 2 3 4 5
| // __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量 if (window.__MICRO_APP_ENVIRONMENT__) { // eslint-disable-next-line __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ }
|
步骤2: 在子应用入口文件的最顶部引入public-path.js
1 2
| // entry import './public-path'
|
3、监听卸载
1 2 3 4 5 6
| 子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。
window.addEventListener('unmount', function () { root.unmount() // react 18 // ReactDOM.unmountComponentAtNode(document.getElementById('root')) })
|
Vue
1、设置跨域支持
1 2 3 4 5 6 7 8 9 10 11
| 在vue.config.js中添加配置
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, devServer: { headers: { 'Access-Control-Allow-Origin': '*', } } })
|
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
1 2 3 4 5 6 7 8
| // main.js import { createRouter, createWebHistory } from 'vue-router' import routes from './router' const router = createRouter({ // 👇 __MICRO_APP_BASE_ROUTE__ 为micro-app传入的基础路由 history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL), routes, })
|
3、设置publicPath
步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内容
1 2 3 4 5
| // __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量 if (window.__MICRO_APP_ENVIRONMENT__) { // eslint-disable-next-line __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ }
|
步骤2: 在子应用入口文件的最顶部引入public-path.js
1 2
| // entry import './public-path'
|
4、监听卸载
1 2 3 4 5 6 7 8 9
| 子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。
// main.js const app = createApp(App) app.mount('#app') // 监听卸载操作 window.addEventListener('unmount', function () { app.unmount() })
|
数据通信
一、子应用获取来自基座应用的数据
micro-app会向子应用注入名称为microApp的全局对象,子应用通过这个对象和基座应用进行数据交互。
有两种方式获取来自基座应用的数据:
方式1:直接获取数据
1
| const data = window.microApp.getData() // 返回基座下发的data数据
|
方式2:绑定监听函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function dataListener (data) { console.log('来自基座应用的数据', data) } /**
- 绑定监听函数,监听函数只有在数据变化时才会触发 - dataListener: 绑定函数 - autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false - !!!重要说明: 因为子应用是异步渲染的,而基座发送数据是同步的, - 如果在子应用渲染结束前基座应用发送数据,则在绑定监听函数前数据已经发送,在初始化后不会触发绑定函数, - 但这个数据会放入缓存中,此时可以设置autoTrigger为true主动触发一次监听函数来获取数据。 */ window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean) // 解绑监听函数 window.microApp.removeDataListener(dataListener: Function) // 清空当前子应用的所有绑定函数(全局数据函数除外) window.microApp.clearDataListener()
|
二、子应用向基座应用发送数据
1 2
| // dispatch只接受对象作为参数 window.microApp.dispatch({type: '子应用发送的数据'})
|
三、基座应用向子应用发送数据
基座应用向子应用发送数据有两种方式:
方式1: 通过data属性发送数据
Vue(Angular类似)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <micro-app name='my-app' url='xx' :data='dataForChild' // data只接受对象类型,数据变化时会重新发送 /> </template> <script> export default { data () { return { dataForChild: {type: '发送给子应用的数据'} } } } </script>
|
React
在React中我们需要引入一个polyfill。
在元素所在的文件顶部添加polyfill(注释也要复制)。
1 2 3
| /** @jsxRuntime classic */ /** @jsx jsxCustomEvent */ import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
|
开始使用
1 2 3 4 5
| <micro-app name='my-app' url='xx' data={this.state.dataForChild} // data只接受对象类型,采用严格对比(===),当传入新的data对象时会重新发送 />
|
方式2: 手动发送数据
手动发送数据需要通过name指定接受数据的子应用,此值和元素中的name一致。
1 2 3
| import microApp from '@micro-zoe/micro-app' // 发送数据给子应用 my-app,setData第二个参数只接受对象类型 microApp.setData('my-app', {type: '新的数据'})
|
四、基座应用获取来自子应用的数据
基座应用获取来自子应用的数据有三种方式:
方式1:直接获取数据
1 2
| import microApp from '@micro-zoe/micro-app' const childData = microApp.getData(appName) // 返回子应用的data数据
|
方式2: 监听自定义事件 (datachange)
Vue(Angular同理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <micro-app name='my-app' url='xx' // 数据在事件对象的detail.data字段中,子应用每次发送数据都会触发datachange @datachange='handleDataChange' /> </template> <script> export default { methods: { handleDataChange (e) { console.log('来自子应用的数据:', e.detail.data) } } } </script>
|
React
在React中我们需要引入一个polyfill。
在元素所在的文件顶部添加polyfill(注释也要复制)。
1 2 3
| /** @jsxRuntime classic */ /** @jsx jsxCustomEvent */ import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
|
开始使用
1 2 3 4 5 6
| <micro-app name='my-app' url='xx' // 数据在event.detail.data字段中,子应用每次发送数据都会触发datachange onDataChange={(e) => console.log('来自子应用的数据:', e.detail.data)} />
|
方式3: 绑定监听函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 绑定监听函数需要通过name指定子应用,此值和<micro-app>元素中的name一致。
import microApp from '@micro-zoe/micro-app' function dataListener (data) { console.log('来自子应用my-app的数据', data) } /**
- 绑定监听函数 - appName: 应用名称 - dataListener: 绑定函数 - autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false */ microApp.addDataListener(appName: string, dataListener: Function, autoTrigger?: boolean) // 解绑监听my-app子应用的函数 microApp.removeDataListener(appName: string, dataListener: Function) // 清空所有监听appName子应用的函数 microApp.clearDataListener(appName: string)
|
DEMO
依赖版本: “@micro-zoe/micro-app”: “^0.8.10”
内部项目就不展示了
Angular主应用(预算)
React子应用
Vue子应用
Angular子应用