微前端

330 阅读6分钟

项目中遇到的问题

需要在react框架中使用一些vue的组件。是重构?还是用其他方式?

几种微前端方案的列举和思考

重构过程比较长,所以重构是一个持续过程,完成一块功能,就迁移到智能版。所以采用微前端的方式集成。主项目不发布,功能热替换。

方案

方案核心实现

优点

缺点

iframe

iframe内嵌页面

最简单的实现

内外独立的2个系统,

系统间数据共享,通信等很多问题都需要解决

微前端

  1. 主项目创建dom

  2. 主项目请求子项目描述文件

  3. 主项目加载子项目静态资源

  4. 子项目渲染在同id的dom

  5. 结束

主项目动态加载子项目

实现了微前端

实现简单,好理解

父子项目数据无法直接共享

父项目无法控制子项目渲染的细节。

父子项目有很多约定。

子组件单独无法使用。(如果依赖外层提供的数据的话)

远程模块(组件)

  1. 主项目请求子项目描述文件

  2. 主项目动态获取子模块入口 js 和 css,

  3. 主项目在promise中获取子模块

  4. 主项目传入props 和 context 给子模块。(也可以基于子模块再次进行上层封装), 此步因具体业务而定,可省略。

  5. 主项目渲染子模块。

  6. 结束

主项目动态加载子项目

实现了微前端

父子项目数据可以直接通过 和 context传递,不需要额外技术

父页面可以控制子页面的渲染(位置,时机)。甚至可以通用组件再次包装。

可以实现不同系统,页面的多态

实现稍微复杂,需要一定理解成本

远程模块(组件)一些质疑

  1. 为啥不封装npm的业务组件,再彼此系统中使用。

    业务组件变化频繁,一般不抽象组件使用。另外npm组件更新,需要使用此组件的系统分别发布升级,会造成线上系统频繁发布,无关的功能也会被频繁发布。

  2. 远程模块的改造成本多大,对业务入侵?

    使用远程模块方案,只对编译阶段有影响,会额外编译一个目录存放本次业务组件,发布后供其他系统远程获取。自身系统的开发形式和发布形式不约束。但要保证组件是纯组件(尽量不在组件内部依赖全局变量或者数据,保证组件在提供正确 props和context 下即可正常使用),如果有依赖需求,通过 props 或者 context 从外部进行传递。事实上,这点也是我们正常形式开发时要遵守的一点。

  3. 为啥不用时老师的方式

    会员portm的方式也可以实现。但是正如缺点描述那样,对父子有约定,比如组件渲染的dom id 要约定,子组件的全局数据怎么获取和更新。子组件在开发时路由怎么控制?还有一个问题,子页面的多态无法实现。比如我们组件有2套主题。需要根据父主题而定。通过远程模块,子组件在开发时可以直接根据上下文中的主题内容来渲染自身样式。父组件在渲染时只要包装上context即可。在我们会员系统里,全局的用户信息,菜单权限等,子组件开发时不需要管怎么来的,只需要控制怎么渲染即可。

  4. 组件重复的问题?父子应用重复?子应用多个会重复吗?

    1. 本着一个原则:微前端尽量减少父子应用之间的耦合。换句话说,单拿出来,应用是个独立的应用,集成后,又可以做为子应用。这是我们设计的理念。在开发时候,开发同学同样不感知我应用最终如何呈现,统一当做一个独立应用开发。所以,父子应用的公共组件不贡献,也不应该共享。(不然会有版本耦合)

    2. 子应用独立打包,webpack会抽取子应用组件的公共组件,在我们引入子应用的main脚本时,相关vender会同时被加载。而且只会加载一次。(这点webpack会帮我们实现)

方案设计

整个方案的核心思想就是将webpack 基于组件 code split, 将传统的同项目异步加载,改为异项目异步加载。

图例:

CDN业务bundle组件bundle本地组件远程组件A项目B项目本地组件编译

前置要求:

✅A, B 项目依赖react 版本保持一致。

✅打包后包名要不能重复,可以通过编译控制。

简单的实现方案:

  1. A业务正常开发自身项目。

  2. 编译时,除了A正常打包外,在build中打包一个目录。里面是A组件对外提供的业务组件。

    打包方式,使用webpack的 code split 即可:

    代码块
    JavaScript
    // 将组件输出到window
    window.remoteModule = (name: string) => {
      if (name === "home") {
        return import(/* webpackChunkName: "smarts_home" */ './pages/Home');
      }
      if (name === "test") {
        return import(/* webpackChunkName: "smarts_test" */ './pages/Test');
      }
    }

  3. 如下图:

    其中,main是入口文件,给B项目依赖。smarts_x.bundle.js 为组件。

  4. B项目中加载后,在入口处动态引入A入口文件(此时,A种的组件还未加载):

    代码块
    JavaScript
    function loadRemoteModule = (src) => {
      var script = document.createElement('script');
      script.charset = 'utf-8';
      script.timeout = 2000;
      script.setAttribute('crossorigin', 'anonymous')
      script.setAttribute('type', 'text/javascript')
      script.crossorigin="anonymous"
      document.head.appendChild(script);
      script.src = src;
    }

  5. B项目异步引入远程模块A中的组件(webpack将动态加载该组件的js文件和它的依赖):

    代码块
    JavaScript
    getRemoteComponent('home').then((Home) => {
      this.setState({
        el: Home.default || Home,
      })
    })
    getRemoteComponent = (name: string) => {
       return Promise.all([
          window.remoteModule(name),
        ])
    }
     render() {
        const Com = this.state.el;
        const Context = this.state.context;
        return <div>
            <hr/>
            <Context value={{user: 'yanggaofei'}}> //指定异步组件的上下文
               <Com id={1}/> // 传递属性
            </Context>
        </div>
    }

重构的技术价值和产出

重构不是简单的新瓶装旧药,也不是无意义的重复建设。基于当前版本现有问题的解决,同时激励自我成长。我们在改造完成后要达到以下目标:

序号

1

推行新的react 版本以及新特性在项目落地,沉淀和积累经验。系列文章: 经验积累 (每个人都要有)

2

梳理业务。优化代码中的多处冗余。

3

积累大型项目微前端整合的经验, 产出通用技术方案/技术博客

4

对团队代码检查sonar配置的更新