因为一些交付场景, 最近遇到了2个项目需要使用远程组件.

第一个在疫情居家期间, 使用了module federation实现了一下.

最近的项目尝试了手动加载远程组件, 所以产生了一些对比.

对比的方式是mf和手动.

我会从使用方式, 原理比较, 和各方面综合比较来进行对比.

使用方式

mf

mf的使用方式只需要照着webpack文档操作就行了, 分为主应用和子应用, 子应用比较简单先说

remote

ModuleFederationPlugin插件里配置:

  • 子应用名字
  • 输出的文件名
  • 文件入口
  • 共享模块

于是webpack打包结果就会多出一个作为远程组件的文件.

master

ModuleFederationPlugin插件里配置:

  • 远程组件的url和名字
  • 共享模块

然后配置ExternalTemplateRemotesPlugin, 并把应用相关入口改成dynamic import.

使用的时候mf和手动都使用了React.Suspense和React.lazy, mf使用的时候lazy里dynamic import设置好的共享模块名字就可以了.

手动

remote

在webpack配置里增加一个entry指向组件, 并设置output.library.

output.library我尝试过2种设置, umd和window, 都可以正常运行, 打包体积也基本一致(umd会稍微多一点), window的缺点是需要设置name, 让人有些不舒服. (即使可以随便写, 但在使用组件的时候得写额外的逻辑, 想名字也比较花时间)

master

主应用只要做一件事: 手动请求remote组件资源并解析. 然后作为promise的结果返回给lazy方法.

对应着umd打包结果方式是: 准备module, exports, 和一个对应着远程组件externals的require()`方法. 然后eval资源, 远程组件就在module.exports里了. 代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
.then((res) => {
const require = p => ({
React,
ReactDom
}[p]);
const exports = {};
const module = {
exports
};
eval(res);
resolve(module.exports);
});

对应window打包结果只需要准备1个变量, 名字随意, 把共享模块放上, 并留一个目标模块的placeholder键, 然后替换资源的window.成自己的变量, eval变量后, 就得到了模块.

如果觉得麻烦的话, eval以后直接取window.xxx就行了, 也能走通. 流程化是另外一个话题了.

原理比较

mf

我们这里其实指的是webpack的mf, 所以原理会和webpack比较相关. 之前有文章比较详细的讲了, 这里总结下.

  1. 当主应用引入了子应用, 会通过网络请求获取到子应用的资源.
  2. 子应用通过window.compName把入口暴露给主应用. 但拥有自己独立的运行闭包.
  3. 子应用加载的时候主子应用会共享一个变量(webpack__.s), 变量里存着需要被共享的模块, 并指向子包的资源. (比如使用子包的react和react-dom)
  4. 主/子应用配置过的模块都会去共享变量(webpack__.s)里取.

手动

手动加载就简明很多. 主应引用子应用时, 通过网络请求获取子应用的资源, 并根据子应用打包方式最终获取react组件. (其实就是个js方法)

在加载子应用的时候, 通过中间变量将主应用的模块直接传给子应用, 以达到共享模块的目的.

总结

经过了比较以后, 发现这两个方案不可以比较.

因为mf是一套完整的解决方案, 而手动加载只是简单的poc.

但和所有框架的对比一样, 功能完备就会有大多场景都不需要的功能, 我的看法是, 如果场景确定切重复, 推荐手动加载远程组件.

简单的在几个场景下比较一下:

  • 配置复杂度.

    相比手动加载只需要把子应用externals的包都注入, mf的配置就比较复杂, 需要配置子应用名字也是一个不舒服的地方.

  • 代码复杂度.

    手动加载只有加载的时候有特殊的代码, 抽象一下调用会更简单.

    而mf会要求远程组件入口前要dynamic import, 并且引入远程组件的名字也要和配置中的一样.

  • 打包体积.

    最明显的是, mf的包必须把依赖都打到包里, 而手动打可以设置externals.

  • 开发体验.

    mf在开发的时候要将主应用配置子应用的地址是localhost, 再用monorepo工具同时起2个应用.

    手动加载的情况, 开发时主应用直接引用子应用, 但最后要改成加载远程组件的方法.

    或者子应用单独作为组件开发, 写个模拟入口.

聊聊mf复杂的原因和手动加载的更大灵活性

其实上面几个mf复杂的原因基本来自于对共享模块的处理, 而我们的场景固定在react组件, 整个应用里不会有多个react版本, 所以就可以少处理很多事.

当然, 有一些地方手动加载比mf更复杂的, 但根据我们自己的业务来开发工具和流程, 手动加载的可扩展性更强, 天花板更高.