乐趣区

关于webpack:webpack-5-模块联邦实现微前端

webpack 5 模块联邦实现微前端

微前端:将 微小 单体 前端零碎拆分成 多个独立 的小型零碎,最初集成为一个零碎的架构思路,分而治之 ,让零碎
更加容易保护、更易扩大,施行微前端是一个 先拆分 后合并 的过程。

这里的前端利用指的是 前后端拆散 单页利用,在这根底上议论微前端才有意义。

微前端和微服务都是为了解决大我的项目和大团队的难题:大我的项目实现 模块解耦 ,大团队实现 人员解耦,这两大难题是《软件工程
》这门课程钻研的次要问题。

康威定律:软件结构体现人员构造,人员构造决定软件结构造。

阮一峰软件工程介绍

微服务架构的实践根底 – 康威定律

康威定律

为何须要微前端

巨石单体零碎随着业务的减少,变得越来越臃肿,多个团队一起开发,沟通胜利高,编译、部署、测试、保护艰难,微前端可解决这些问题。

  1. 利用自治:各个利用互相独立,规模更小,更容易扩大、测试、构建、保护、排错、降级依赖等;
  2. 团队自治:利用独立后,团队也会独立,缩小很多人在一个巨石利用中同时开发,相互影响,进步开发效率;
  3. 技术无关:各个利用 抉择不同的框架开发,尽量放弃对立 ,否则利用之间交互会可能遇到麻烦,也不利于组件复用,比方无奈
    共享组件级别的代码;
  4. 尝试新技术:利用拆分后,很容易在零碎里尝试新技术。
  5. 老零碎增量重构。

毛病:

  1. 代码标准对立比拟艰难(人员多、我的项目多),容易克服
  2. 开发时可能须要同时运行多个我的项目,容易克服
  3. 集成测试比拟艰难
  4. UI、交互等容易不对立,容易克服

相比微前端的长处,毛病根本可忽略不计。

施行倡议:

  1. 统一的工作办法:团队成员要达成统一的工作办法,尤其是宿主利用和近程利用之间的 交互协定,须要提前约定好;
  2. 联合业务:在应用为微前端架构之前,思考 业务划分 和微前端给团队给来的价值;
  3. 听从统一的代码格调,不便前期保护;
  4. 不要适度应用:心愿能实现拆分人员或者技术的指标,或者有必要拆分人员或者技术时才应用。

微前端介绍

对于微前端最早的探讨文章

如何集成 — 聚合

次要三种集成形式:

集成形式 具体形容 长处 毛病 其余
构建时集成 把微利用打包进入主利用,微利用通常以 npm 包公布 施行容易且很好了解 有依赖关系的利用之间,其中一个更新,另一个也要更新,而后部署,实际上无奈独立部署 实际上这种形式并没有实现微前端的指标
运行时构建 主利用在浏览器加载后,再去获取微利用代码 独立部署,主利用能决定应用哪个微利用的版本,灵便,性能好 设置简单,不好了解 实现微前端的指标,目前比拟通用的形式
服务端集成 主利用向服务器申请微利用,服务器决定是否给代码 重大依赖后盾代码 实现简单,个别不应用

集成时须要思考哪些问题?

  1. 防止款式抵触
  2. 利用之间通信简略
  3. 不同利用之间导航丝滑
  4. 能集成特定版本
  5. 版本控制不互相烦扰
  6. 能不便实现依赖共享

常见的微前端实施方案

微前端的架构形式呈现后,业界呈现一些框架和解决方案,常见的:

框架:

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

如何实现款式隔离?

常见的款式作用域解决方案

  1. 自定义 css

① css in js:会从新编译选择器。不同的我的项目应用雷同的 css in js 库,生产环境下可能导致款式抵触。

起因:生产环境下,css in js 生成的类名短小,导致不同微前端利用之间类名雷同,规定不同,导致抵触。

解决:应用不同的 css-in-js 库,或者查问其本地,配置自定义前缀。

比方 @material-uicreateGenerateClassName 可自定义类名前缀。

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: 设置非凡的选择器

  1. css 库

css 款式库,自行构建,比拟麻烦。

  1. 雷同的款式库不同的版本导致款式规定不同

① 类名雷同规定不同导致款式不统一;

② 规定雷同,类名不同导致款式生效。

解决办法:不同款式库不共享。

相比之下,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",应用最新的遇到困难。

有应用最新的库集成胜利的敌人可通知我一下。

为何应用函数实现数据共享

函数调用,函数调用的特点:

  1. 定义处 接管到 内部数据(通过参数),在 调用处 获取到 返回值 参数 返回值 将定义处和调用处分割起来。
  2. 依赖少,函数设计适当,可无效升高依赖 — 最好的状况,只依赖参数,也非常容易扩大。
  3. 函数是 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 还有还手之力吗?

退出移动版