微前端遇到的问题
微前端拆分子应用会面临多主应用子应用重复加载模块、公共模块跨应用维护等问题。
所以我们要满足以下几个能力:
- 远程应用模块动态加载
- 子应用复用主应用的库
- 跨应用第三方库版本共存
模块联邦
- 是Webpack 5 的新特性之一,允许在多个 webpack 编译产物之间共享模块、依赖、页面甚至应用
- 提供了一种轻量级的、在运行时,通过全局变量组合,在不同模块之前进行数据的获取
- 提供了一种解决应用集的官方方案。 每个构建都充当一个容器,也可将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。
下面是大概的流程图
下面是完整流程图
host
和remote
属于container
,container
作为提供(provided)和消费(consumed)的载体remote
暴露(expose)模块提供给host
异步加载remote
和host
分别提供(provided)模块维护在share scope
host
在加载共享模块时对share scope
与自身模块决定最终版本,最后加载消费(consumed)模块
模块联邦的应用场景有很多
- 在运行时直接加载其他项目的指定模块/组件/库,比如我们提供一个带业务功能的独立组件嵌入在其他部门的项目中,并不需要在他们项目中加载我们整个项目,只需要加载这个组件及相关的依赖即可。
- 多项目共存时的第三方模块多版本共存
- 抽离多项目之间重复维护构建的代码或npm包,比如UI组件库从npm包的形式改为独立项目提供模块联邦的形式共享组件,这样就可以实时更新不需要经历npm包更新重新部署
- 微前端主应用共享给子应用的组件或代码片段,主应用提前加载,子应用不需要异步加载
微前端里的联邦模块
代码实现
本文章的代码实现基于我自己搭建的一个vite+webpack+react+ts的项目,项目地址:lipten/ice-vitepack-project: icejs2.0 + antd + dva.js 定制entry和工程配置的初始项目 (github.com)
如果想直接体验最终效果可以白嫖:lipten/ice-vitepack-micro-frontend: 基于ice-vitepack-project项目 + qiankun.js 改造的微前端架构demo (github.com)
父应用改造
config/build.config.ts
moduleFederation: {
name: 'baseApp',
filename: 'remoteEntry.js',
library: { type: 'window', name: 'baseApp' },
exposes: {
'./ShareInfo': './src/ShareInfo.tsx', // 暴露的模块
},
shared: {
// 指定react与其他应用共存或共享
react: {
singleton: true,
},
},
},
src/app.tsx
import('./bootstrap')
src/bootstrap.tsx(原src/app.tsx的内容)
子应用改造
config/build.config.js
moduleFederation: {
name: 'app1',
remotes: {
// 子应用需要动态加载父应用地址,所以需要用一个Promise在运行时加载remoteEntry
baseApp: `promise new Promise(resolve => {
// This part depends on how you plan on hosting and versioning your federated modules
const remoteUrlWithVersion = window.location.origin + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.baseApp.get(request),
init: (arg) => {
try {
console.log(arg)
return window.baseApp.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
shared: {
// 将react库与父应用的react库复用或共存
react: {
singleton: true,
},
},
},
src/app.tsx
let mountModule
export async function bootstrap() {
console.log('bootstrap App1')
}
export async function mount(props) {
mountModule = await import('./bootstrap')
mountModule.render(props)
}
export async function unmount(props) {
mountModule.destroy(props.container)
}
if (!window.__POWERED_BY_QIANKUN__) {
// 独立启动时调用获取环境变量
import('./bootstrap').then((module) => {
module.render({})
})
}
src/bootstrap.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './global.less'
import { APP_MODE } from 'ice'
// 加载远程模块
import('baseApp/ShareInfo')
export const render = (props) => {
// ...
ReactDOM.render(<App />, document.querySelector('#root-slaved'))
}
export const destroy = (container) => {
ReactDOM.unmountComponentAtNode(
container ? container.querySelector('#root-slaved') : document.querySelector('#root-slaved'),
)
}
参考资料: