介绍

微前端

微前端的概念是由 ThoughtWorks 在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。

它主要解决了两个问题:

  • 1、随着项目迭代应用越来越庞大,难以维护。
  • 2、跨团队或跨部门协作开发项目导致效率低下的问题。

image.png

开源框架

字节跳动: Garfish

京东: micro-app

蚂蚁金服: qiankun (qiankun是基于 single-spa 的一层封装)

比较

Garfish

优势

  • 自实现
  • 提供缓存
  • 沙盒功能

劣势

  • 需要配置的配置项较多
  • 基座应用必须为react
  • 懒加载、刷新有时有奇怪的问题
  • 不支持嵌套(子应用不能既是主又是子)

micro-app

优势

  • 基于webComponent技术
  • 嵌入无需新增依赖
  • 应用无限制
  • 沙盒功能
  • 预加载

劣势

  • 静态资源有时会有问题
  • angular子应用无法使用懒加载
  • 不支持嵌套(子应用不能既是主又是子)

鉴于实际上手难度以及使用场景我们决定基于micro-app来实现我们的微前端方案

实操

主应用(预算planning项目)

1、安装依赖

npm i @micro-zoe/micro-app --save

2、在入口处引入

// 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

// app/app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
 schemas: [CUSTOM_ELEMENTS_SCHEMA],
})

4、分配路由给子应用

// 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、在页面中嵌入子应用

<!-- app/my-angular/my-angular.component.html -->

<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、关闭热更新
"scripts": {
 "start": "ng serve --live-reload false",
},
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
// 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的文件,并添加如下内容

// __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

// entry
import './public-path'
4、监听卸载

子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。

// 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路由,这一步可以省略)
// 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>
 )
}

或者

// index.js
const router = createBrowserRouter([
 {
 path: '/',
 element: <App />
 }
],
 {
 // @ts-ignore
 basename: (window['__MICRO_APP_BASE_ROUTE__']) || '/'
 })
2、设置publicPath

步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内容

// __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

// entry
import './public-path'
3、监听卸载
子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。

window.addEventListener('unmount', function () {
 root.unmount() // react 18
 // ReactDOM.unmountComponentAtNode(document.getElementById('root'))
})

Vue

1、设置跨域支持
在vue.config.js中添加配置

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
 transpileDependencies: true,
 devServer: {
 headers: {
 'Access-Control-Allow-Origin': '*',
 }
 }
})
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
// 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的文件,并添加如下内容

// __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

// entry
import './public-path'
4、监听卸载
子应用被卸载时会接受到一个名为unmount的事件,在此可以进行卸载相关操作。

// main.js
const app = createApp(App)
app.mount('#app')
// 监听卸载操作
window.addEventListener('unmount', function () {
 app.unmount()
})

数据通信

一、子应用获取来自基座应用的数据

micro-app会向子应用注入名称为microApp的全局对象,子应用通过这个对象和基座应用进行数据交互。

有两种方式获取来自基座应用的数据:

方式1:直接获取数据

const data = window.microApp.getData() // 返回基座下发的data数据

方式2:绑定监听函数

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()

二、子应用向基座应用发送数据

// dispatch只接受对象作为参数
window.microApp.dispatch({type: '子应用发送的数据'})

三、基座应用向子应用发送数据

基座应用向子应用发送数据有两种方式:

方式1: 通过data属性发送数据
Vue(Angular类似)
<template>
  <micro-app
    name='my-app'
    url='xx'
    :data='dataForChild' // data只接受对象类型,数据变化时会重新发送
  />
</template>
<script>
export default {
  data () {
    return {
      dataForChild: {type: '发送给子应用的数据'}
    }
  }
}
</script>
React

在React中我们需要引入一个polyfill。

在<micro-app>元素所在的文件顶部添加polyfill(注释也要复制)。

/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'

开始使用

<micro-app
 name='my-app'
 url='xx'
 data={this.state.dataForChild} // data只接受对象类型,采用严格对比(===),当传入新的data对象时会重新发送
/>
方式2: 手动发送数据

手动发送数据需要通过name指定接受数据的子应用,此值和<micro-app>元素中的name一致。

import microApp from '@micro-zoe/micro-app'
// 发送数据给子应用 my-app,setData第二个参数只接受对象类型
microApp.setData('my-app', {type: '新的数据'})

四、基座应用获取来自子应用的数据

基座应用获取来自子应用的数据有三种方式:

方式1:直接获取数据
import microApp from '@micro-zoe/micro-app'
const childData = microApp.getData(appName) // 返回子应用的data数据
方式2: 监听自定义事件 (datachange)
Vue(Angular同理)
<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。

在<micro-app>元素所在的文件顶部添加polyfill(注释也要复制)。

/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'

开始使用

<micro-app
 name='my-app'
 url='xx'
 // 数据在event.detail.data字段中,子应用每次发送数据都会触发datachange
 onDataChange={(e) => console.log('来自子应用的数据:', e.detail.data)}
/>
方式3: 绑定监听函数
绑定监听函数需要通过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子应用

Last modification:January 31, 2023
如果觉得文章对你有所收获,可以请笔者喝杯咖啡