likes
comments
collection
share

微前端落地,悬着的心终于放下了

作者站长头像
站长
· 阅读数 18

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

产品提出系统要模块化,通过不同的模块组合构建我们的应用。这不是最近很火的微前端吗,和组内的小伙伴经过一番沟通,带着压力开始搞起来了,经过三个月的迭代开发,在业务开发的同时,把两个模块迁移成了子应用。系统上线了,悬着的心终于落下了。

现有的系统状况分析

现有的系统是一个独立的项目,采用umi框架开发,通过文件夹区分各大业务模块。公共组通过npm包的方式去管理。由于系统迭代时间比较久,架构上遗留的几个问题需要解决

  • 子应用采用ant4.x (antd用的3.x版本,升级antd4.x难度比较大,主要是Form变动比较大)
  • hooks替换redux部分state (redux stroe中的state差不多有一百多个,一些本不应该放在Redux里的状态也放到了里面)
  • 统一封装requst(拆分成独立http和socket请求库,支持hooks)

第一个落地版本的架构

微前端落地,悬着的心终于放下了

引入微前端要解决的问题

  • 封装脚手架
  • 组件库升级,支持antd4.x
  • 主子应用通信
  • 主应用共享一些实例供子应用使用
  • Redux Store得部分数据需要共享,怎么做限制
  • 子应用支持国际化,国际化内容动态加载
  • 子应用跳转父应用
  • 样式隔离

改造正式开始

微前端框架用qiankun, 由于我们用的框架是umi, 对应提供了插件 @umijs/plugin-qiankun。

主应用

  1. 在umi配置文件开启qiankun配置
  qiankun: {
    master: {},
  },
  1. 加载子应用配置

function microApp(entryPrefix) {
  return [
    {
      name: 'app1', //唯一 id
      entry: entryPrefix ? `${entryPrefix}/app1` : '//localhost:3000',
    },
    {
      name: 'app2', //唯一 id
      entry: entryPrefix ? `${entryPrefix}/app2` : '//localhost:3001',
    },
    ... //其它子应用
  ];
}
export default microApp;
  1. 配置激活子应用路由
const router = [
  {
    path: '/main/app/rouer/app1', // (/main/app/rouer/)主应用部分路由
    microApp: 'app1',
    microAppProps: {
      autoSetLoading: true,
      className: 'appClassName',
      wrapperClassName: 'wrapperClass',

    },
  },
  {
    path: '/main/app/rouer/app2',
    microApp: 'app2',
    microAppProps: {
      autoSetLoading: true,
      className: 'appClassName',
      wrapperClassName: 'wrapperClass',

    },
  }
  ... //其它子应用
  ];

export default router;
  1. 在入口文件app.js导出qiankun对象
import microApp from './microApp';
import router from './router';

export const qiankun = new Promise((resolve) => {
  const entryPrefix = process.env.NODE_ENV === 'production' ? window.location.origin : null;
  const res = microApp(entryPrefix);
  resolve(res);
}).then(apps => {
  return {
    apps,
    routes: router,
  };
});

子应用

  1. 在umi中开启qiankun配置
  qiankun: {
    slave: {},
  },
  1. 在入口文件app.js导出qiankun的生命周期

export const qiankun = {
    // 应用加载之前
    async bootstrap(props) {
     init(props);
    },
    // 应用 render 之前触发
    async mount(props) {
    },
    // 应用卸载时触发
    async unmount(props) {
    },
};

基于qiankun和umi提供的qiankun插件还是比较方便的运行起来。下面我们一起看下上面的问题怎么解决。

解决实际遇到的一些问题

组件库升级,支持antd4.x

我们采用施渐进式重构,我们需要一种增量升级的能力,先让新旧代码和谐共存,再逐步转化旧代码,直到整个重构完成。目前我们把基于antd3.x版本的组件库升级到4.x。两个版本同时更新一段时间,两个版本可以共存,迁出来的子应用使用antd 4.x,保证新的特性我们能使用

主子应用通信

umi qiankun plugin提供了主子应用通信的方式

在主应用入口导出useQiankunStateForSlave方法

export function useQiankunStateForSlave() {
  const [masterState, setMasterState] = useState({});

  return {
    SlaveSDK,
    getStore,
    // masterState,
    setMasterState,
  };
}
子应用中会自动生成一个全局 model,可以在任意组件中获取主应用透传的 props 的值。
import { useModel } from 'umi';

function MyPage() {
  const masterProps = useModel('@@qiankunStateFromMaster');
  return <div>{JSON.stringify(masterProps)}</div>;
}

该方式主子应用通信没问题,嵌套多层子应用使用不是很方便,后期我们会对它进行改造

主应用共享一些实例供子应用使用

在微前端架构当中,不推荐应用共享实例,因为这样子应该很难做到独立开发,独立部署,应用会有耦合。但由于我们是对系统做拆分,一些公用的功能,比如弹框查看日志,数据预览等功能会先用父应用的功能,需要共享。这里我们封装了SDK供子应用使用。后期如果要改造这块成独立开发,独立部署。我们实现对应的接口,动态注入,就可以实现。

  • 定义log要共享的方法
class Log {
  show = (params) => 
      // 具体业务逻辑
  }

  showOther = (params) => {
      // 具体业务逻辑
  }
}

export default new Log();

  • 在主应用中创建SlaveSDK实例
import intl from 'utils/intl';
import { history } from 'umi';
import { getParams } from 'utils/util';
import Log from './log';

class SlaveSDK {
  constructor() {
    this.log = Log;
    this.intl = intl;
    this.router = {
      history, // 主应用的history对象
      getParams, // 获取主应用的参数
    };
    ... 其它实例共享
  }
}

export default new SlaveSDK();

  • 通过数据通信传递给子应用
import SlaveSDK from './SlaveSDK';
export function useQiankunStateForSlave() {
  return {
    SlaveSDK,
  };
}
  • 子应用在生命周期里获取sdk并初始化保存
export const qiankun = {
    // 应用加载之前
    async bootstrap(props) {
         props?.SlaveSDK && setSDK(props.SlaveSDK);
    },
};

Redux Store部分数据需要共享,怎么做限制

子应用业务有可能需要Redux Store的部分数据,但我们不能把整个Store都暴露给子应用,在应用使用的时候做些限制,这里我们用到了代理对象去实现

    const allowGet = { auth: '', user: '' }; // 需要共享的key
    const state = getReduxStore();
    const proxy = new Proxy(state, {
      get(target, property) {
        if (property in allowGet) {
          return target[property];
        }
        return undefined;
      },
    });
    return proxy;
  };

子应用支持国际化,国际化内容动态加载

我们通过共享实例,通过SDK的方式后去获取国际化实例intl,在子应用初始化的时候去加载国际化。

/**
 * 国际化初始化
 */
const intlInit = async () => {
  // load语言 文件
  const result = await import('./locales');
  const intl = await import('./utils/intl');
  try {
    moment.locale(intl?.default?.getIntlLang?.() || 'zh_CN');
    intl?.default?.load(result.default);
  } catch (error) {
    console.error('intl error', error);
  }
};

子应用跳转父应用

在子应用中跳转父应用的路由,也是通过把父应用的router的history对象传递给子应用

import { history } from 'umi';
class SlaveSDK {
  constructor() {
    this.router = {
      history, // 主应用的history对象
      getParams, // 获取主应用的参数
    };

  }
}

样式隔离

样式隔里子应用我们用的是cssModule,编译的时候会自动生成唯一的key。主要问题是antd,因为我们既加载了antd3,又加载了antd4,导致样式会有冲突。我在antd4的编译的时候改前缀。配置如下

import { ConfigProvider } from 'antd';

// 弹框的前缀配置
 ConfigProvider.config({
    prefixCls: 'my-ant',
 });

// 组件的配置
<ConfigProvider prefixCls="my-ant">
</ConfigProvider>

部署

系统采用nginx部署,是在同一个Server下通过不同的path来处理。配置如下

server {
  listen       8080;
  server_name  localhost;


  location / {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }


  location /app1 {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /app1/index.html;
  }


  location /app2 {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /app2/index.html;
  }
}

思考

目前第一版微前端已经上线,下个版本有可能会有嵌套多层子应用。以下问题需要解决

  • 资源隔离
  • 嵌套多层应用共享状态
  • webpack5 Module Federation和qiankun结合做资源隔离
  • 随着子应用的增多,脚手架增强

结束语

这个版本,脚手架完成了基础的功能,能根据模块去生成我们的子应用。后期增强之后,能生成主应用和子应用,会分享这块。以上是微前端落地这块总结的一些问题,如有问题,欢迎指正。

参考

如果你觉得该文章不错,不妨

1、点赞,让更多的人也能看到这篇内容

2、关注我,让我们成为长期关系

3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章