前言
这是 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);
})();
}, []);
最后效果:
到这一步,我们实现了公共方法的共享,现在来试试公共组件吧:
// 主应用 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>
);
}
然而,总是要好事多磨:
大概意思是react、react-dom的版本不是一个,这里翻看了一下 webpack 官方文档,是需要我们配置 shared属性的,于是赶紧加上吧:
// 主、子应用都加上
{
...
shared: [
'antd',
{
react: {
singleton: true,
eager: true,
},
'react-dom': {
singleton: true,
eager: true,
},
},
],
}
再来:
oh,要崩溃了 ~~
再来看webpack官网的说法:
看来是需要我们把启动程序单独拎出来,然后在程序的入口文件中去异步引用。问题的关键来了,我们这是umi项目啊,入口文件是 src/.umi/umi.ts,umi底层是不让修改 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'],
}
结果是这样的:
看来是与 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',
},
最后见证奇迹了:
总结
这一章节我们主要集成了 webpack5 的新特性--模块联邦,实现了在各应用间实时共享组件和公共方法;虽然过程曲折,但最终还是完成了。如果各位看官能有更好的办法,欢迎交流。码字不易,求点赞收藏,更欢迎批评指正。下一个章节是本系列的终章:应用的部署。