【实战】umi3.x+qiankun+模块联邦 搭建微前端应用(二)— 集成qiankun

1,000 阅读4分钟

前言

这是 umi3.x+qiankun+模块联邦 搭建微前端应用 系列的第二章,我们先来回顾一下:上一章节我们主要是搭建了一个微前端的基础框架,并实现了一个类似浏览器页签切换的功能,使得我们打开的每一个页面都能保持状态。那么这一章节我们主要利用 webpack5 ModuleFederationPlugin 实现各应用之间组件和方法的复用;

module-federation 作为webpack5的新特性,个人觉得将它称之为史诗级更新也不为过,它使得我们在不同项目间实时共享组件和方法的需求成为可能。

系列文章传送门:

【实战】umi3.x+qiankun+模块联邦 搭建微前端应用(一)— 项目初始化

【实战】umi3.x+qiankun+模块联邦 搭建微前端应用(三)— 项目部署

项目配置

主应用

const { normalizePath } = require('./compose');

// 主应用 config/config.ts
chainWebpack(memo) {
    memo
      .plugin('module-feaderation-plugin')
      .use(require('webpack').container.ModuleFederationPlugin, [
        {
          name: 'providerComponets',
          library: { type: 'umd', name: 'providerComponets' },
          filename: 'remoteComponetsEntry.js',
          exposes: {
            './remoteUtils': './src/utils/index',
            ...normalizePath,
          },
          shared: [
            'antd',
            {
              react: {
                singleton: true,
                eager: true,
              },
              'react-dom': {
                singleton: true,
                eager: true,
              },
            },
          ],
        },
      ]);
  },

这里需要注意 normalizePath 这个属性,初衷是想把 src/components 这个文件夹下的所有组件都导出,假设我们的components文件夹有如下文件:

components
├── Comp1
│   ├── NewIndex.tsx
│   ├── components
│   │   └── Tag
│   │       └── index.tsx
│   └── index.tsx
├── Comp2
│   └── index.tsx
├── Layout
    └── Content.tsx

根据模块联邦的expose规则,我们期望生成如下数据结构,方便我们在后面的应用引用:

{
  './Comp1/NewIndex': './src/components/Comp1/NewIndex.tsx',
  './Comp1': './src/components/Comp1/index.tsx',
  './Comp2': './src/components/Comp2/index.tsx',
  './Layout/Content': './src/components/Layout/Content.tsx',
  './Comp1/components/Tag': './src/components/Comp1/components/Tag/index.tsx'
}

开始编写node.js脚本

// config/compose.js
const glob = require('fast-glob');
const path = require('path');

const { sep } = path;

const rootPath = path.resolve(__dirname, '..');

const componentsPath = path.resolve(rootPath, 'src/components');

const subProjectDirs = glob.sync(`${componentsPath}/**/*.{js,jsx,tsx}`, {
  stats: true,
});

const normalizePath = subProjectDirs.reduce((prev, { name, path: p }) => {
  const [fileName, ext] = p.split('.');
  const compRelativePath = fileName.replace(`${componentsPath}${sep}`, '');
  const indexNameIdx = compRelativePath.lastIndexOf('/index');
  const compoentName =
    indexNameIdx < 0
      ? compRelativePath
      : compRelativePath.substr(0, indexNameIdx);
  return {
    ...prev,
    [`./${compoentName}`]: p.replace(rootPath, '.'),
  };
}, {});

module.exports = {
  normalizePath,
};

子应用 app2

// src/config/config
chainWebpack(memo) {
    memo
      .plugin('module-feaderation-plugin')
      .use(require('webpack').container.ModuleFederationPlugin, [
        {
          name: 'comsumerComps',
          remotes: {
            comsumerComps: 'providerComponets@http://localhost:8000/remoteComponetsEntry.js',
          },
          shared: [
            'antd',
            {
              react: {
                singleton: true,
                eager: true,
              },
              'react-dom': {
                singleton: true,
                eager: true,
              },
            },
          ],
        },
      ]);
  },

这里app1项目是vite搭建的,其实它也可以实现模块联邦,需要借助 @originjs/vite-plugin-federation 插件,这里略...

ok! 让我们来试试公共方法的引用吧:

// 主应用 src/utils/index.ts
import round from 'lodash/round';

export const hello = () => {
  console.log('hello world');
};

export const couter = (num: number) => {
  return round(num, 2);
};

// app2/src/pages/index.tsx
  useEffect(() => {
    (async () => {
      const res = await import('comsumerComps/remoteUtils');
      console.log(res);
    })();
  }, []);

最后效果:

image.png

到这一步,我们实现了公共方法的共享,现在来试试公共组件吧:

// 主应用 src/components/Comp2/index.tsx
import React, { useEffect } from 'react';
import { Button } from 'antd';

const Comp2 = () => {
  useEffect(() => {
    console.log('sssssss');
  }, []);
  return <Button type="primary">this is Com2</Button>;
};

export default Comp2;
import { useEffect, lazy, Suspense, useState } from 'react';
import { loadComponent } from '../utils';
import styles from './index.less';

const Comp1 = lazy(() => import('comsumerComps/Comp2'));

export default function IndexPage() {
  let [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(++count);
  };

  useEffect(() => {
    (async () => {
      const res = await import('comsumerComps/remoteUtils');
      console.log(res);
    })();
  }, []);
  return (
    <div>
      <h1 className={styles.title}>这是app2项目</h1>
      <button onClick={handleClick}>点击({count})</button>
      <Suspense fallback="loading...">
        <Comp1 count={12234}></Comp1>
      </Suspense>
    </div>
  );
}

然而,总是要好事多磨:

image.png

大概意思是react、react-dom的版本不是一个,这里翻看了一下 webpack 官方文档,是需要我们配置 shared属性的,于是赶紧加上吧:

// 主、子应用都加上
{
...
shared: [
    'antd',
    {
      react: {
        singleton: true,
        eager: true,
      },
      'react-dom': {
        singleton: true,
        eager: true,
      },
    },
  ],
}

再来:

image.png

oh,要崩溃了 ~~

再来看webpack官网的说法:

image.png

看来是需要我们把启动程序单独拎出来,然后在程序的入口文件中去异步引用。问题的关键来了,我们这是umi项目啊,入口文件是 src/.umi/umi.tsumi底层是不让修改 webpack entry 属性的。试试只有试试写一个umi插件了:

// 新建 app2/plugins/bootstrap.js
import { resolve } from 'path';
import { readFileSync } from 'fs';

export default (api) => {
  api.onGenerateFiles(() => {
    const path =
      api.env === 'production' ? './src/.umi/index.ts' : './src/.umi/umi.ts';
    const buffer = readFileSync(resolve(path));
    const c = String(buffer);
    api.writeTmpFile({
      path: 'index.ts',
      content: c,
    });
    api.writeTmpFile({
      path: 'umi.ts',
      content: 'import("./index")',
    });
  });
};
// app2/config/config.ts 中引入刚刚写的插件
{
...
plugins: ['./plugins/bootstrap.js'],
}

结果是这样的:

image.png 看来是与 umi-plugin-qiankun 插件的不兼容;

到这里我们不妨换个思路,究其上述所有问题的原因就是 react 版本的不一致嘛,那我们向外暴露一个固定的 react 版本不就好了?

// 主应用 src/config/config.ts
  externals: {
    react: 'var window.React',
    'react-dom': 'var window.ReactDOM',
  },
  scripts: [
  // 这里可以通过环境变量来区分不同的版本哈
    'https://unpkg.com/react@17.0.1/umd/react.production.min.js',
    'https://unpkg.com/react-dom@17.0.1/umd/react-dom.production.min.js',
  ],
// app2/src/config/config.ts 不用再次定义scripts属性了,实质上最后主、子应用都是在一个html文件中
  externals: {
    react: 'var window.React',
    'react-dom': 'var window.ReactDOM',
  },

最后见证奇迹了:

image.png

总结

这一章节我们主要集成了 webpack5 的新特性--模块联邦,实现了在各应用间实时共享组件和公共方法;虽然过程曲折,但最终还是完成了。如果各位看官能有更好的办法,欢迎交流。码字不易,求点赞收藏,更欢迎批评指正。下一个章节是本系列的终章:应用的部署。