Appearance
前言
模块联邦提供了一种运行时的加载其他服务下模块的能力,通过下面几个问题来解决生产中模块联邦的问题
- 模块联邦在项目源码中并不会体现,如果packageA引入了packageB,如何保证自动补全、ts类型等功能
- 项目工程化如何设计,单仓库还是多仓库,项目如何启动
- 开发环境和线上环境如何设计
- 和微服务框架如何结合
webpack中的模块联邦
webpack中通过MF插件提供了简单配置容器和加载容器的功能
js
// webpack.config.js
export default {
.....
plugins: [new webpack.ModuleFederationPlugin({
name: "main_app", // 本身对外暴露出的入口
filename: "remoteEntry.js", // 入口文件
exposes: {
'./functionA': './src/xxx/a.js' // 暴露出的文件
},
share: ['vue'], // 共享的模块, 不会重复打包
remote: { // 需要加载的远程package
'packageB': 'packageB@http://localhost:3001/remoteEntry.js'
}
})]
}在packageA中使用packageB的方法
js
import functionA from "packageB/functionA"
console.log(functionA)模块联邦
模块联邦提供了加载其他模块的能力,可以用于跨项目共享chunk。
- 本地模块指的是入口文件构建的模块,远程模块是MF定义出的。相当于共享配置的两个入口文件。
- 每个构建都是一个容器,容器可以加载远程模块。
- 远程模块需要使用异步的方式加载。
- 远程模块也可以作为容器加载其他远程模块。
- 远程模块中,因为publicPath是简单的拼接,如果设置了publicPath会出现加载宿主域的问题。需要将publicPath设置为auto或则动态设置。
- 远程模块至少会生成runtime、remote和module 3种chunk文件
js
const MF = webpack.container.ModuleFederationPlugin;
module.exports = {
plugins: [
// 打包逻辑 runtime -> remote.js -> ./componentA
new MF({
name: 'remote', // 远程模块名称
filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
exposes: [{
'./componentA': './src/componentA.vue' // 需要使用./前缀,host 通过 import('remote/componentA')
'.': 'src/componentA/index.js' // import('remote')
}]
})
]
}动态设置publicPath
因为runtime.js 需要加载remote.js和module.js 所以需要提前设置publicPath。MF在打包时如果container chunk和入口chunk同名,会合并。所以需要将name设置为一个入口name相同。
js
module.exports = {
entry: {
main: './index.js',
remote: './public-path.js' // 在入口文件中配置动态的js。
},
plugins: [
new MF({
name: 'remote', // 远程模块名称
filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
exposes: [{
'./componentA': './src/componentA.vue'
'.': 'src/componentA/index.js'
}]
})
]
}动态加载远程模块
正常情况下远程模块是在webpack配置中制定的,可以手动实现远程模块的加载。
js
const loadModule = async (url, containerName, module) => {
await loadScript(url); // 加载remoteEntry js 脚本。
const container = window[containerName]; // 默认情况下remoteEntry 按照var 导出。 通过window containerName 可以获取到container。
const factory = await container.get(module) // get方法是一个异步的方法,获取的是modules里的value。
return factory() // 执行一次相当于执行了module文件。
}上面代码不够完整,缺少共享模块的加载以及重复引用下module不是单例问题。
js
const remoteModuleCache = new set();
let loadModule = async (url, containerName, module) => {
if(hasCache(container, module)) {
return getCache()
}
await loadScript(url); // 加载remoteEntry js 脚本
await __webpack_init_sharing__('default') // 全局初始化共享模块
const container = window[containerName]; // 默认情况下remoteEntry library 按照var 导出。 通过window containerName 可以获取到container。
container.init(__webpack_share_scoped__.default)
const factory = await container.get(module) // get方法是一个异步的方法,获取的是modules里的value。
return factory() // 执行一次相当于执行了module文件。
}远程模块加载流程
- webpack_modules 声明了remoteEntry入口文件。
- 通过__webpack_require__.e 调用 __webpack__require.f.remote 中间件加载入口js文件。
- 入口js文件加载好后,在全局暴露remote变量
- host中加载远程模块的module会固定编译成 一个chunkid,
- host 会调用remote去获取这个chunkId对应的js文件,
- 加载完成后回调在remote中,然后host执行这个remote中的回调函数得到module。
远程模块踩坑
- 远程模块的异步chunk加载方式务必不要和host应用雷同,会出现覆盖host modules问题。大坑,使用 output.chunkLoadingGlobal、output.uniquename修改
- 目前自动加载远程模块的方式仅默认方式能生效,即把远程模块绑定到全局变量上。其他的方式均需要自行加载远程模块
在vite中使用模块联邦
vite社区已经实现@originjs/vite-plugin-federation, 配置方式和webpack差不多
如何保证packageB的自动补全
有如下两种场景,一是packageB和packageA使用了pnpm的monorepo的方案,一种是packageB在另一个地方。第一种场景比较简单,直接在pnpm add packageB作为packageA为依赖,编辑器中就能获得所有补全,但是需要引入的packageB和源码结构类似。虽然在packageA中引用了packageB,但是并不会真正打包,这种情况存在路径映射的问题(通过package新特性支持,通过exports指明对应文件的type,这种方式需要在tsconfig中指定moduleRelution为我为Node16 或者nodenext)。第二种情况是packageB不和packageA在同一个仓库,需要对packageB进行申明文件构建,申明文件选择一个新的目录,在packageA的tsconfig中正常添加这个申明文件的引用,通过typeRoot添加一个。通过设置rootDir还能实现对第一种方案中路径映射的调整
单仓库还是多仓库
新项目无脑单仓库,自带补全功能美滋滋。
但是模块联邦目前不能很好的和monorepo结合,比如monorepo引用的是packageB的dist后的代码,但是模块联邦需要的是源码方便调试(也可以只把申明文件输出到dist目录中),两者存在冲突。模块联邦的这种黑科技方式对项目也是破坏性,所以还是推荐自行添加types到packageA的依赖中(生成声明文件时,声明文件也可以添加resouremap选项也能重定向到源代码,这个特性重要重要重要,需要珍藏)
开发环境和线上环境的结合
模块联邦主要的组件和复用,非应用的复用。在monorepo开发环境中,删除对模块联邦的使用,使用packageA打包packageB依赖(因为已经申明了packageB,可以放心大胆的删除),这个好处是不用启用多个服务,一个服务解决。
和微服务框架如何结合
微服务框架主要是对应用的复用和嵌套,模块联邦目前未能做到样式隔离等功能,只做到了加载器的功能。同时微服务对monorepo本地线上双模式支持也较弱。目前不考虑强行将微服务转换为模块联邦(一定不要)
模块联邦如何动态设置publicPath
模块联邦中,如果子应用简单的设置了publicPath,不管是相对还是绝对路径都是对于主应用而言,这种情况下需要动态的配置publicPath使子应用正常。第一个方法是设置子应用的publicPath为auto。或者通过__webpack_public_path 动态设置,具体看webpack那篇博客
