LiuL0703 / blog

https://liul0703.github.io
38 stars 11 forks source link

基于React搭配 React Router的微前端实践 #43

Open LiuL0703 opened 2 years ago

LiuL0703 commented 2 years ago

什么是微前端

众所周知,微前端是一种架构方式,与后端的微服务可以说是一脉相承。其核心思想就是"分而治之"。简单来说就是讲某单体应用解耦拆分为若干个子应用,这些子应用可以独立运行、开发、部署、和维护、且互不影响。且各个子应用可以选择不同的技术栈开发等等。目前在项目中有所尝试,下面内容就以实际业务为基础的微前端实践

需求背景

说之前先来分析一下为什么需要微前端?

背景分析: 项目中的子应用越来越多,其中部分功能越来越复杂,加之系统开发中碰到的问题越来越多

开发过程中的痛点:

预期结果

技术方案

可以实现微前端的方案有很多,比较常见的一些微前端实现方案比如:iframe、Module Federation、动态路由等

iframe : 实现简单;可同时挂载多个应用;天然支持隔离,但是存在路由不同步,刷新页面状态丢失,通信困难只能通过postMessage等方式比较麻烦。

动态路由: 常见的实现方式比如single-spa 、qiankun等,通过统一维护所有应用的注册加载,挂载和卸载,针对不同技术栈的应用做聚合。但是通讯难,接入成本高。

Module Federation: 依赖webpack5,存在加载第三方依赖、版本控制等问题,会增加试错成本。

考虑到若选用上述方式,则会对项目本身带来较大影响,并且改造成本太高。 结合项目现状和改造成本,决定通过借鉴动态路由的思想实现通过以React为基座的特定动态路由的方案。对于动态路由的方式来说,关键部分就是注册、加载、挂载与卸载,由于我们的技术栈都是React,而且react-router自身就具备对组件挂载和卸载的能力。利用这一点优势,将所有子应用作为一个component,将挂载卸载的工作交由react-router来完成,也不用每次切换路由都重新初始化整个子应用,用户体验和单页应用保持一致。

架构设计

todo

实现

基座:从配置文件中读取配置信息,在路由注册层注册信息。当路由匹配到子应用后,请求获取到子应用的打包文件,由react-router进行加载和卸载的控制 子应用: 导出umd资源,上传到服务器,更新配置文件中对应的子应用信息

整个执行流程如图所示

todo


配置子应用信息

在基座项目里创建子应用配置文件subApp.js

// container/scr/subApp.js
const app = [
  {
    name: 'subApp', // 子应用名称
    host: process.env.REACT_APP_SUBAPP_HOST, // 子应用的静态资源地址
    appName: 'App', // 导出的应用名 默认为'App'
  },
  ...
]

管理子应用

引入subApp.js配置信息,通过AppManager进行子应用管理

// container/src/index.js

function renderApp () {
  apps.forEach(AppManager.registerApp)
  ReactDOM.render(<App />,document.getElementById('root'));
}

在AppManager中维护子应用信息,及加载子应用的loadSubApp函数,loadSubApp函数获取到子应用的资源文件地址后,通过添加script标签的方式加载资源,并将返回的资源组价挂载到window上

// container/src/AppManager.js
class AppManager {
  subApps = {}
  subAppModules = {}
  registerApp = (subApp) => {
    this.subApps[subApp.name] = subApp
  }
  loadSubApp = (subAppInfo) => {
    const { name, host } = typeof subAppInfo === 'string'? this.subApps[subAppInfo] : subAppInfo
    if (!this.subAppModules[name]) {
      this.subAppModules[name] = new Promise((resolve, reject)=> {
        fetch(`${host}/${name}/asset-manifest.json`)
        .then(res => res.json())
        .then(manifest => {
          const script = document.createElement('script');
          script.src = `${host}${manifest.files['main.js']}`;
          const timeout = setTimeout(()=>{
            console.error(`MicroApp ${name} timeout`);
            reject(new Error(`MicroApp ${name} timeout`));
          }, 10000)
          script.onload = () => {
            clearTimeout(timeout)
            const app = window[name]
            console.log(`MicroApp ${name} loaded success`);
            resolve(app)
          }
          script.onerror = (e) => {
            clearTimeout(timeout);
            console.error(`MicroApp ${name} loaded error`, e);
            reject(e)
          }
          document.body.appendChild(script);
          // 加载css
          const link = document.createElement('link')
          link.rel = 'stylesheet'
          link.href = `${host}${manifest.files['main.css']}`
          document.head.appendChild(link)
        })
      })
    }
    return this.subAppModules[name]
  }
}

总结

todo