webpack 5 模块联邦实现微前端
微前端:将 微小 的单体 前端零碎拆分成 多个独立 的小型零碎,最初集成为一个零碎的架构思路,分而治之 ,让零碎
更加容易保护、更易扩大,施行微前端是一个 先拆分 , 后合并 的过程。
这里的前端利用指的是 前后端拆散 的单页利用,在这根底上议论微前端才有意义。
微前端和微服务都是为了解决大我的项目和大团队的难题:大我的项目实现 模块解耦 ,大团队实现 人员解耦,这两大难题是《软件工程
》这门课程钻研的次要问题。康威定律:软件结构体现人员构造,人员构造决定软件结构造。
阮一峰软件工程介绍
微服务架构的实践根底 – 康威定律
康威定律
为何须要微前端
巨石单体零碎随着业务的减少,变得越来越臃肿,多个团队一起开发,沟通胜利高,编译、部署、测试、保护艰难,微前端可解决这些问题。
- 利用自治:各个利用互相独立,规模更小,更容易扩大、测试、构建、保护、排错、降级依赖等;
- 团队自治:利用独立后,团队也会独立,缩小很多人在一个巨石利用中同时开发,相互影响,进步开发效率;
- 技术无关:各个利用 可抉择不同的框架开发,尽量放弃对立 ,否则利用之间交互会可能遇到麻烦,也不利于组件复用,比方无奈
共享组件级别的代码; - 尝试新技术:利用拆分后,很容易在零碎里尝试新技术。
- 老零碎增量重构。
毛病:
- 代码标准对立比拟艰难(人员多、我的项目多),容易克服
- 开发时可能须要同时运行多个我的项目,容易克服
- 集成测试比拟艰难
- UI、交互等容易不对立,容易克服
相比微前端的长处,毛病根本可忽略不计。
施行倡议:
- 统一的工作办法:团队成员要达成统一的工作办法,尤其是宿主利用和近程利用之间的 交互协定,须要提前约定好;
- 联合业务:在应用为微前端架构之前,思考 业务划分 和微前端给团队给来的价值;
- 听从统一的代码格调,不便前期保护;
- 不要适度应用:心愿能实现拆分人员或者技术的指标,或者有必要拆分人员或者技术时才应用。
微前端介绍
对于微前端最早的探讨文章
如何集成 — 聚合
次要三种集成形式:
集成形式 | 具体形容 | 长处 | 毛病 | 其余 |
---|---|---|---|---|
构建时集成 | 把微利用打包进入主利用,微利用通常以 npm 包公布 | 施行容易且很好了解 | 有依赖关系的利用之间,其中一个更新,另一个也要更新,而后部署,实际上无奈独立部署 | 实际上这种形式并没有实现微前端的指标 |
运行时构建 | 主利用在浏览器加载后,再去获取微利用代码 | 独立部署,主利用能决定应用哪个微利用的版本,灵便,性能好 | 设置简单,不好了解 | 实现微前端的指标,目前比拟通用的形式 |
服务端集成 | 主利用向服务器申请微利用,服务器决定是否给代码 | 重大依赖后盾代码 | 实现简单,个别不应用 | |
集成时须要思考哪些问题?
- 防止款式抵触
- 利用之间通信简略
- 不同利用之间导航丝滑
- 能集成特定版本
- 版本控制不互相烦扰
- 能不便实现依赖共享
常见的微前端实施方案
微前端的架构形式呈现后,业界呈现一些框架和解决方案,常见的:
框架:
MicroApp、single-spa、qiankuan
无裤架的解决方案:
web components
不举荐,无奈提供模块化性能,我的项目越大,越容易失控,web component 实质是封装自定义的 html, 会很快会到 jQuery 时代。
iframe
webpack5 module federation
模块联邦,webpack5 的一个新个性,可实现跨利用共享代码。
如何拆分微利用?
在拆分时,心愿不同利用之间的 交互最小 为准则,最大水平解耦,以不便不同利用之间的通信,否则导致利用难以测试、问题难以定
位。
依照业务划分,雷同的业务拆分到同一个微利用中。
依照权限划分,依据用户的权限,不同用户应用应用的性能划分到一起。
依照后盾微服务划分,有的团队,后盾应用微服务架构,可思考依照微服务划分。
模块联邦 — Module Federation
模块联邦是 webpack5 引入的个性,能 轻易实现 在两个应用 webpack 构建的我的项目之间 共享代码 ,甚至 组合不同的利用为一个利用。
Module Federation 官网
模块联邦的配置
模块联邦可实现跨利用共享代码,以两个利用为例子阐明其配置。
一个dashboard
vue3 利用,心愿提供代码给其余利用:
// const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin') // 导出模块联邦
const {ModuleFederationPlugin} = require('webpack').container // 还能够这样
// webpack 插件配置
{
plugins: [
new ModuleFederationPlugin({
name: 'dashboard', // exposeRemoteName 共享的模块名字,在生产该模块的利用中会用到 不能应用 -
filename: 'remoteEntry.js', // 近程加载的文件名字,在浏览器申请面板可看到,默认名字就是 remoteEntry.js
exposes: {'./DashboardApp': './src/bootstrap', // 从本利用裸露的共享模块, 可共享多个模块 key 以 ./ 结尾,value 指向本地的一个文件},
shared: packageJson.dependencies, // 心愿共享的依赖
}),
],
}
对于名字的设置
name 的值会被导出成一个全局变量,不要应用-
,不要和页面里的 id 雷同,否则可能报错。
container
react 利用生产 dashboard
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
{
plugins: [
new ModuleFederationPlugin({
name: 'container',// 心愿共享的模块,尽管 container 没有被其余利用生产,申明名字是一个好的做法
remotes: {
// marketing: 'marketing@http://localhost:8081/remoteEntry.js',
// auth: 'auth@http://localhost:8082/remoteEntry.js',
dashboard: 'dashboard@http://localhost:8083/remoteEntry.js',// 指定近程模块
//NOTE remoteName:exposeRemoteName@fileOnRemoteServer
},
shared: packageJson.dependencies,// 心愿共享的模块
}),
],
}
如何在 container 应用 dashboard 呢?
在 container 中新建一个 DashboardApp.jsx
组件来引入 dashboard:
dashboard 是 container 里 remotes 的字段 DashboardApp 是 dashboard 在 exposes 里裸露而 key 是 dashboard 里导出的一个挂
载函数,可在 container 将该利用挂载到任何中央
// exposeRemoteName/expose
import {mount} from 'dashboard/DashboardApp' // NOTE 留神这里的写法和配置的对应关系
import React, {useRef, useEffect} from 'react'
import {useHistory} from 'react-router-dom'
export default ({isSignedIn, user}) => {const ref = useRef(null)
const history = useHistory()
useEffect(() => {
// NOTE 通过 mount 把 dashboard 挂载到 div 上,这些参数和返回值是实现数据共享的形式,稍后细说
const {onParentNavigate} = mount(ref.current, {
isMemoryHistory: true,
basePath: '/dashboard',
currentPath: history.location.pathname,
onNavigate: nextPathname => {const { pathname} = history.location
if (pathname !== nextPathname) {console.log('vue 子利用跳转', nextPathname)
history.push(nextPathname)
}
},
sharedData: {isSignedIn, user},
})
console.log('container dashboard navigate')
history.listen(onParentNavigate)
}, [])
return <div ref={ref} />
}
而后将该组件导出,放在 /dashboard
门路下,就可导航到 dashboard 了:
import React, {lazy, Suspense, useState, useEffect} from 'react'
import {Router, Route, Switch, Redirect} from 'react-router-dom'
import {StylesProvider, createGenerateClassName} from '@material-ui/core/styles'
import {createBrowserHistory} from 'history'
import {Progress, Header} from './components'
const MarketingLazy = lazy(() => import('./components/MarketingApp'))
const AuthLazy = lazy(() => import('./components/AuthApp'))
const DashboardLazy = lazy(() => import('./components/DashboardApp'))
const generateClassName = createGenerateClassName({productionPrefix: 'co',})
const history = createBrowserHistory()
export default () => {const [isSignedIn, setIsSignedIn] = useState(window.localStorage.getItem('isSignedIn') === 'true')
const [user, setUser] = useState(JSON.parse(window.localStorage.getItem('user')))
useEffect(() => {if (isSignedIn) {history.push('/dashboard')
}
}, [isSignedIn])
return (<Router history={history}>
<StylesProvider generateClassName={generateClassName}>
<div>
<Header
onSignOut={() => {window.localStorage.removeItem('isSignedIn')
window.localStorage.removeItem('user')
window.sessionStorage.removeItem('user')
setIsSignedIn(false)
}}
isSignedIn={isSignedIn}
/>
<Suspense fallback={<Progress />}>
<Switch>
<Route path='/auth'>
<AuthLazy
onSignIn={user => {setUser(user)
// 应用本地存储
window.sessionStorage.setItem('user', JSON.stringify(user))
window.localStorage.setItem('user', JSON.stringify(user))
window.localStorage.setItem('isSignedIn', JSON.stringify(true))
setIsSignedIn(true)
}}
/>
</Route>
<Route path='/dashboard'>
{/* {!isSignedIn && <Redirect to='/' />} */}
<DashboardLazy user={user} isSignedIn={isSignedIn} />
</Route>
<Route path='/' component={MarketingLazy} />
</Switch>
</Suspense>
</div>
</StylesProvider>
</Router>
)
}
dashboard 裸露进去的模块是怎么的呢?
dashboard 里 bootstrap.js
import {createApp} from 'vue'
import App from './App.vue'
import {setupRouter} from './route'
import {createWebHistory, createMemoryHistory} from 'vue-router'
function mount(el, { isMemoryHistory, basePath, currentPath, onNavigate, sharedData = {} }) {const app = createApp(App, { basePath, currentPath, isMemoryHistory, onNavigate, sharedData})
const history = isMemoryHistory ? createMemoryHistory(basePath) : createWebHistory()
setupRouter(app, { history})
app.mount(el)
return {onParentNavigate({ pathname: nextPathname}) {console.log('dashboard vue onParentNavigate', nextPathname)
history.listen(currentPath => {if (currentPath !== nextPathname) {history.push(nextPathname)
}
})
},
}
}
// If we are in development and in isolation,
// call mount immediately
const devRoot = document.querySelector('#dashboard-dev-root')
if (devRoot) {mount(devRoot, { isMemoryHistory: false})
}
// We are running through container
// and we should export the mount function
export {mount}
关键点:
① 提供一个 mount
函数,导出该函数,让生产该模块的利用能任意挂载它,在独立部署和开发环境下,调用该函数实现利用挂载到页面
上。
② 正当设计 mount 的 参数 和返回值 ,以实现利用之间 共享数据 、 路由切换 等。
③ 在 webpack 入口文件 index.js
引入 bootstrap.js
:
import('./bootstrap.js')
在入口文件 index.js 间接应用 bootstrap.js 的内容,会如何?
独自运行微利用时,报错:
Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react
history.listen(onNavigate) 不起作用,不晓得为啥。
在 mount 函数中也无奈应用 useRoute 等函数。
引入的近程模块的执行程序是怎么的?
如何防止 container 和 dashboard 路由导航抵触问题?
为了实现独立部署能切换页面,各个微应要有本人的导航,container 也有本人的导航,container 加载 dashboard 之后,如何解决导
航抵触?
为何会有导航抵触?
一个门路只能对应一个页面 :同一个时刻,浏览器只有一个门路,该门路对应一个页面或者组件,切换到该门路时,渲染该页面或者
组件。
vue-router 提供了 web 路由和内存路由,应用 web 路由时,切换利用的门路,浏览器地址栏会变动,浏览器地址变动,利用里渲染的
组件的也会变动;应用内存导航时,地址栏和利用内的组件渲染脱离关系。
为何会有内存路由?为了实用非浏览器环境。
vue 和 react 有这两种路由。
微利用独自部署时,应用 web 路由,集成到 container 时,应用内存路由,web 路由由 container 接管,浏览器地址栏变动时,告
诉集成进来的微利用,而后微利用再跳转到相应的页面。
dashboard 的 路由配置:
import {createRouter, createWebHistory} from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
},
{
path: '/upload',
name: 'upload',
component: () => import('../views/Upload.vue'),
},
]
// 导出路由配置函数,默认应用 web 路由
export function setupRouter(app, { history = createWebHistory() } = {}) {
const router = createRouter({
history,
routes,
})
app.use(router)
}
如何在 mount 中应用 setupRouter 呢?
要害代码:
// 传递一个 isMemoryHistory 标识,阐明应用的路由
function mount(el, { isMemoryHistory}) {
// el 是利用挂载的元素
const history = isMemoryHistory ? createMemoryHistory(basePath) : createWebHistory()
setupRouter(app, { history}) // app 是 createApp 的返回值
}
独自部署和开发时,应用 web 路由:
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {const devRoot = document.querySelector('#dashboard-dev-root')
if (devRoot) {
// NOTE 应用 web 路由
mount(devRoot, { isMemoryHistory: false})
}
}
集成时应用内存路由,同时还须要检测浏览器门路是否变动,在变动时切换路由,否则 container 导航时,dashboard 不会变动。
在 App.vue
中实现跳转过程:
<script>
import {onMounted, watch} from '@vue/runtime-core'
import {useRouter, useRoute} from 'vue-router'
export default {
name: 'App',
props: {
onNavigate: {type: Function,},
basePath: {
type: String,
default: '/',
},
currentPath: {
type: String,
default: '/',
},
isMemoryHistory: {
type: Boolean,
default: false,
},
},
setup(props) {const { basePath, currentPath, isMemoryHistory, onNavigate} = props
const router = useRouter()
const route = useRoute()
// NOTE 路由有变动且提供了跳转函数,才跳转
function onRouteChange(newPath) {onNavigate && onNavigate(basePath + newPath)
}
watch(() => route.path, onRouteChange)
onMounted(() => {console.log('App vue mounted', basePath, currentPath)
let nextPath = currentPath
if (currentPath.startsWith(basePath)) {
//NOTE 默认去到首页
nextPath = currentPath.replace(basePath, '') ??'/'
}
// NOTE 如果是 memoryHistory,挂载时跳转到相应的组件,解决浏览器刷新,页面无奈维持的问题
isMemoryHistory && router.push(nextPath)
})
return {}},
}
</script>
App 接管以后浏览器门路、根底门路(dashboard 在 container 中的门路)、是否时内存路由和跳转的具体方法,这些参数都是从
container 传递进来。
function mount(el, { isMemoryHistory, basePath, currentPath, onNavigate, sharedData = {} }) {
//NOTE basePath, currentPath, isMemoryHistory, onNavigate, 是 container 传递到 App.vue 的数据
// onNavigate 是跳转函数,dashboard 路由变动是,通过该函数告知 container 跳转到哪儿
const app = createApp(App, { basePath, currentPath, isMemoryHistory, onNavigate, sharedData})
const history = isMemoryHistory ? createMemoryHistory(basePath) : createWebHistory()
setupRouter(app, { history})
app.mount(el)
}
这些参数时如何传递到 App 的呢?
在 container 利用中通过 mount 传递
export default () => {const ref = useRef(null)
const history = useHistory()
useEffect(() => {
mount(ref.current, {
isMemoryHistory: true,
basePath: '/dashboard',
currentPath: history.location.pathname,
onNavigate: nextPathname => {const { pathname} = history.location
if (pathname !== nextPathname) {console.log('vue 子利用跳转', nextPathname)
history.push(nextPathname)
}
},
})
console.log('container dashboard navigate')
}, [])
return <div ref={ref} />
}
以上方法实现了 dashboard 内路由跳转,告诉 container 切换门路,当 container 切换门路时,须要告诉 dashboard 跳转门路,如何
找到这一点?
可通过 mount 返回一个 函数 ,该函数解决具体的跳转逻辑,因为 mount 在 container 外部调用,能够获取返回值,在 container
路由变动时,就调用该函数。
function mount(el, { isMemoryHistory, basePath, currentPath, onNavigate, sharedData = {} }) {const history = isMemoryHistory ? createMemoryHistory(basePath) : createWebHistory()
return {
// pathname 时 container 传递过去的浏览器门路
onParentNavigate({pathname: nextPathname}) {console.log('dashboard vue onParentNavigate', nextPathname)
// history.listen 时 vue-router 提供的函数,可监听门路变动,参数是一个回调函数,回调的时以后利用门路
history.listen(currentPath => {if (currentPath !== nextPathname) {history.push(nextPathname)
}
})
},
}
}
在 DashboardApp.jsx
中调用 onParentNavigate:
import {mount} from 'dashboard/DashboardApp'
import React, {useRef, useEffect} from 'react'
import {useHistory} from 'react-router-dom'
export default () => {const ref = useRef(null)
const history = useHistory()
useEffect(() => {const { onParentNavigate} = mount(ref.current, {})
console.log('container dashboard navigate')
history.listen(onParentNavigate) // container 的门路变动,而后调用 onParentNavigate
// history.listen 是 react-router-dom 提供的函数,在地址栏变动时触发回调函数执行,回调函数的尝试时 history 对象
}, [])
return <div ref={ref} />
}
如何共享数据?
解决了路由抵触问题,利用如何共享数据呢?
还是通过 mount 传递。
mount 在 container 中调用,在 dashboard 中定义,那么就能在调用时传递 container 的数据到 dashboard 中。
mount 有一个 sharedData 字段,承受从 container 传递过去的参数,再通过 createApp 的第二个参数,传递到 App.vue
中。
function mount(el, { sharedData = {} }) {const app = createApp(App, { sharedData})
}
App.vue
通过 props 接管 sharedData:
<template>
<div id="app">
<div id="nav">
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/upload">Upload Dropzone</RouterLink>
</div>
<!-- NOTE 给路由进口传递数据 -->
<RouterView :sharedData="sharedData" />
</div>
</template>
<script>
import {onMounted} from '@vue/runtime-core'
export default {
name: 'App',
props: {
sharedData: {
type: Object,
default: () => ({}),
},
},
setup(props) {const { sharedData} = props
onMounted(() => {console.log('App vue mounted', sharedData)
})
return {}},
}
</script>
特地提醒,RouterView 可传递数据,而后在相应的路由组件中接管该数据。
在 container 的 DashboardApp.jsx
调用 mount
// isSignedIn 和 user 时组件的 props
export default ({isSignedIn, user}) => {const ref = useRef(null)
useEffect(() => {const { onParentNavigate} = mount(ref.current, {sharedData: { isSignedIn, user},
})
}, [])
return <div ref={ref} />
}
在 container 的路由配置中传递数据:
<DashboardLazy user={user} isSignedIn={isSignedIn} />
如何实现款式隔离?
常见的款式作用域解决方案
- 自定义 css
① css in js:会从新编译选择器。不同的我的项目应用雷同的 css in js 库,生产环境下可能导致款式抵触。
起因:生产环境下,css in js 生成的类名短小,导致不同微前端利用之间类名雷同,规定不同,导致抵触。
解决:应用不同的 css-in-js 库,或者查问其本地,配置自定义前缀。
比方 @material-ui
的 createGenerateClassName
可自定义类名前缀。
import React from 'react'
import {Switch, Route, Router} from 'react-router-dom'
import {StylesProvider, createGenerateClassName} from '@material-ui/core/styles'
import {Landing, Pricing} from './components'
const generateClassName = createGenerateClassName({productionPrefix: 'ma',})
export default ({history}) => {
return (
<div>
<StylesProvider generateClassName={generateClassName}>
<Router history={history}>
<Switch>
<Route exact path='/pricing' component={Pricing} />
<Route path='/' component={Landing} />
</Switch>
</Router>
</StylesProvider>
</div>
)
}
② vue 内置的 scoped 款式:给标签增加自定义属性
其余:给 css 增加 namespace: 设置非凡的选择器
- css 库
css 款式库,自行构建,比拟麻烦。
- 雷同的款式库不同的版本导致款式规定不同
① 类名雷同规定不同导致款式不统一;
② 规定雷同,类名不同导致款式生效。
解决办法:不同款式库不共享。
相比之下,vue scoped 和设置类名前缀是最不便的解决办法。
还有其余计划吗?
到此,解决了 路由导航 、 数据共享 和款式抵触 问题,集成遇到的遇到的问题根本解决了,微前端架构施行是一个 ** 先分后
合 ** 的过程,上面聊聊如何分。
微利用用 react 开发,如何集成到 container?
以上和 vue3 集成到 container,react 利用如何集成到 container 呢?
思路和后面的相似,从 react 微利用导出 mount
,同时也要解决路由到底、款式抵触等问题。
当初有一个 marketing 的 react 利用,心愿集成到 container 到。
入口文件 dashboard.js
import React from 'react'
import ReactDOM from 'react-dom'
import {createMemoryHistory, createBrowserHistory} from 'history'
import App from './App'
// Mount function to start up the app
function mount(el, { onChildNavigate, defaultHistory, currentPathParent}) {
const history =
defaultHistory ||
createMemoryHistory({initialEntries: [currentPathParent],
})
const {pathname: currentPathChild} = history.location
// NOTE 浏览器刷新,利用会从新挂载,此时要放弃门路和以后门路统一
if (currentPathParent && currentPathChild && currentPathParent !== currentPathChild) {console.log('child history.push', currentPathParent)
history.push(currentPathParent)
}
onChildNavigate && history.listen(onChildNavigate)
ReactDOM.render(<App history={history} />, el)
return {onParentNavigate({ pathname: nextPathname}) {const { pathname} = history.location
nextPathname && pathname !== nextPathname && history.push(nextPathname)
},
}
}
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {const el = document.getElementById('_marketing-dev-root')
const history = createBrowserHistory()
el && mount(el, { defaultHistory: history})
}
// We are running through container
// and we should export the mount function
export {mount}
在 index.js
引入 bootstrap.js
import('./bootstrap')
webpack.dev.js
配置
const {merge} = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
const commonConfig = require('./webpack.common')
const packageJson = require('../package.json')
const devConfig = {
mode: 'development',
output: {
publicPath: 'http://localhost:8081/',
clean: true, // build 之前清空 dist 目录
},
devServer: {
port: 8081,
historyApiFallback: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'marketing',
filename: 'remoteEntry.js',
exposes: {'./MarketingApp': './src/bootstrap',},
shared: packageJson.dependencies,
}),
new HtmlWebpackPlugin({template: './public/index.html',}),
],
}
module.exports = merge(commonConfig, devConfig)
在 container 的MarketingApp.jsx
引入 marketing
import {mount} from 'marketing/MarketingApp'
import React, {useRef, useEffect} from 'react'
import {useHistory} from 'react-router-dom'
export default () => {const ref = useRef(null)
const history = useHistory()
useEffect(() => {const { onParentNavigate} = mount(ref.current, {
currentPathParent: history.location.pathname,
onChildNavigate: ({pathname: nextPathname}) => {console.log('marketing react:', nextPathname)
const {pathname} = history.location
nextPathname && pathname !== nextPathname && history.push(nextPathname)
},
})
history.listen(onParentNavigate)
}, [])
return <div ref={ref} />
}
在 container 路由中配置 MarketingApp
import React, {lazy, Suspense, useState, useEffect} from 'react'
import {Router, Route, Switch, Redirect} from 'react-router-dom'
import {StylesProvider, createGenerateClassName} from '@material-ui/core/styles'
import {createBrowserHistory} from 'history'
import {Progress, Header} from './components'
const MarketingLazy = lazy(() => import('./components/MarketingApp'))
const generateClassName = createGenerateClassName({productionPrefix: 'co',})
const history = createBrowserHistory()
export default () => {const [isSignedIn, setIsSignedIn] = useState(window.localStorage.getItem('isSignedIn') === 'true')
const [user, setUser] = useState(JSON.parse(window.localStorage.getItem('user')))
useEffect(() => {if (isSignedIn) {history.push('/dashboard')
}
}, [isSignedIn])
return (<Router history={history}>
<StylesProvider generateClassName={generateClassName}>
<div>
<Header
onSignOut={() => {window.localStorage.removeItem('isSignedIn')
window.localStorage.removeItem('user')
window.sessionStorage.removeItem('user')
setIsSignedIn(false)
}}
isSignedIn={isSignedIn}
/>
<Suspense fallback={<Progress />}>
<Switch>
<Route path='/' component={MarketingLazy} />
</Switch>
</Suspense>
</div>
</StylesProvider>
</Router>
)
}
marketing 利用应用的路由库版本
"react-router-dom": "^5.2.0"
,应用最新的遇到困难。
有应用最新的库集成胜利的敌人可通知我一下。
为何应用函数实现数据共享
函数调用
,函数调用的特点:
- 在 定义处 接管到 内部数据(通过参数),在 调用处 获取到 返回值 , 参数 和返回值 将定义处和调用处分割起来。
- 依赖少,函数设计适当,可无效升高依赖 — 最好的状况,只依赖参数,也非常容易扩大。
- 函数是 js 代码,能在任意框架中调用。
为何不导出组件,以实现在 container 中集成?
组件难以实现跨框架应用。
为何不应用状态治理库实现数据共享?
状态治理库要求同一个框架之间,难以实现技术无关。
即便应用同一种框架搭建微前端,也要防止应用状态治理库在不同利用之间共享数据,这减少了耦合。
后果展现和源码
应用 gh-pages 部署这些利用,生产环境的模联邦须要批改,详见仓库源码。
container --- react
auth --- react
dashboard --- vue3
marketing --- react
micro-front-end-mfd 源码
集成后的利用
独立部署的利用
marketing-react 微利用
vue3-dashboard 微利用
react-auth 微利用
举荐教程
本文依据视频教
程应用 React 的微前端,残缺的开发指南 Microfrontends with React A Complete Developer’s Guide 整顿,强烈推荐,p43 — p66 解说 CICD 可跳过。
参考
Webpack 5 and Module Federation – A Microfrontend Revolution
Webpack 5 Module Federation: A game-changer in JavaScript architecture
webpack 5 模块联邦实现微前端疑难问题解决
面对 ESM 的开发模式,webpack 还有还手之力吗?