前言
官方文档:https://www.webpackjs.com/
通过阅读文档,使用webpack 模块联邦有两个问题需要解决:
- 异步导入
- react实例唯一,依赖共享
如果单纯在umi中使用模块联邦,可以使用umi插件的方式异步导入入口,依赖也可以使用shared来共享,但是这种方式在qiankun下使用会不兼容,在此我们主要说下umi+qiankun的思路, 对umi插件有兴趣可以翻下之前发的一个umi的文章
思考:
1、异步导入如何解决?
如果了解qiankun会知道,qiankun的HTMl Entry 是用import-html-entry实现的,其本质就是异步加载远端文件的方式,所以子应用使用模块联邦其实不需要关注异步导入这个问题
2、react 实例唯一
这个我们也可以用shared来解决,同样我们也可以换个思路,既然所有的子应用要使用同一个react实例,那我们直接引用用一个react不就可以了
方案:
1、技术栈
egg + umi + qiankun + webpack5
2、如何实现
1、 架构设计
项目整体分为项目入口、基座(主应用)和子应用三大部分
1.1 项目入口
项目入口使用egg项目启动,配置路由如下:
router.get('/*', controller.home.index);
然后设置home的colltroller
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<script>
window.routerBase = "/";
</script>
<script>
window.publicPath = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || "/";
</script>
<script>
//! umi version: 3.5.23
</script>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
</body>
</html>
入口文件引用react和react-dom供整个项目使用,这样保证项目整体的react实例是唯一的
1.2 基座
配置umi项目配置
export default defineConfig({
base:'/',
alias:{
'@':'../src'
},
hase:false,
title:false,
qiankun:{
master:{
apps: [
{
name: 'app1', // 唯一 id
entry: 'http://localhost:2001/index.html', // html entry
},
{
name: 'app2', // 唯一 id
entry: 'http://localhost:2002/index.html', // html entry
},
],
}
},
routes: [
{
path: 'app1',
microApp: 'app1',
},
{
path: 'app2',
microApp: 'app2',
},
],
fastRefresh: {},
externals: {
react:'var window.React',
'react-dom':'var window.ReactDOM'
},
mountElementId:'root',
publicPath:'./',
});
注意 externals 配置要结合scripts一起使用,scripts会导入远程地址,但这里我们不需要每个项目导入,所以不用scripts
1.3子应用-组件提供方
使用umi启动项目之后配置webpack5模块联邦
const { ModuleFederationPlugin } = require("webpack").container;
....
webpack5:{},
externals: {
react:'var window.React',
'react-dom':'var window.ReactDOM'
},
chainWebpack(memo) {
memo.output.publicPath('auto');
memo
.plugin('mf')
.use(ModuleFederationPlugin, [{
name: "layout",
library: { type: 'umd', name: 'layout' },
filename: 'remoteLayout.js',
exposes: {
"./Layout": './src/pages/index',
},
}])
},
.....
注意: webpack5需要配置,这样umi才会使用webpack5 .其他模块联邦配置可参考官网
1.4 子应用-组件使用方
....
webpack5:{},
externals: {
react:'var window.React',
'react-dom':'var window.ReactDOM'
},
chainWebpack(memo) {
memo.output.publicPath('auto');
memo
.plugin('mf')
.use(ModuleFederationPlugin, [{
name: "qiankun1",
filename: 'remoteEntry.js',
remotes: {
mfLayout: 'layout@http://localhost:3000/remoteLayout.js',
},
}])
},
...
至此umi+ qiankun+ wepack5模块联邦 已经配置完成,那模块联邦都是写在umi配置里面,如果要动态导入该怎么处理呢?
1.5 模块联邦动态导入
a.将我们打包好的组件文件添加在入口文件,以script的方式加载js
<script src="http://localhost:3000/remoteLayout.js" ></script>
c. 子应用使用
import React from 'react'
// 动态导入联邦组件
function loadComponent(scope: string, module: string) {
return async () => {
await __webpack_init_sharing__('default');
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
// 懒加载组件
const componentCache = new Map();
export const getFederatedComponent = (remoteUrl: string, scope: string, module: string) => {
const key = `${remoteUrl}-${scope}-${module}`;
const Comp = React.lazy(loadComponent(scope, module));
componentCache.set(key, Comp);
return {Comp}
};
export default function IndexPage() {
const { Comp: FederatedComponent } = getFederatedComponent('http://localhost:3000/remoteLayout.js', 'layout', './Layout');
return (
<div>
<React.Suspense fallback="">
<FederatedComponent/>
</React.Suspense>
<h1>Page index</h1>
</div>
);
}
为什么这么设置可以实现导入呢?这篇文章可以为我们解惑:https://github.com/Vincent0700/learning-webpack/blob/master/docs/Webpack%E6%A8%A1%E5%9D%97%E8%81%94%E9%82%A6%E5%8E%9F%E7%90%86.md
文章描述了远程模块的加载步骤:
- 下载并执行
remoteEntry.js
,挂载入口点对象到window.app1
,他有两个函数属性,init
和get
。init
方法用于初始化作用域对象 initScope,get
方法用于下载moduleMap
中导出的远程模块。 - 加载
app1
到本地模块 - 创建
app1.init
的执行环境,收集依赖到共享作用域对象shareScope
- 执行
app1.init
,初始化initScope
- 用户
import
远程模块时调用app1.get(moduleName)
通过Jsonp
懒加载远程模块,然后缓存在全局对象 window[‘webpackChunk’ + appName] - 通过 webpack_require 读取缓存中的模块,执行用户回调
其实我们刚才的loadComponent 函数就是手动实现了这些步骤,加载、创建、执行
结语:
至此 umi + qiankun + webpack5模块联邦的实现已经写完了,技术有限,不当之处还望多指教…