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

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

前言

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

现有的系统情况分析

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

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

第一个落地版本的架构

image.png

引入微前端要解决的问题

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

改造正式开始

微前端框架用qiankun, 因为咱们用的框架是umi, 对应提供了插件 @umijs/plugin-qiankun。webpack

主应用

  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插件仍是比较方便的运行起来。下面咱们一块儿看下上面的问题怎么解决。nginx

解决实际遇到的一些问题

组件库升级,支持antd4.x

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

主子应用通讯

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

在主应用入口导出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>;
}
复制代码

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

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

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

  • 定义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结合作资源隔离
  • 随着子应用的增多,脚手架加强

结束语

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

参考

相关文章
相关标签/搜索