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