阅读 2112

带你解析微前端新方案-Module Federation

这是个撒

webpack5模块联邦为我们提供了一种在应用程序之间共享代码的新方法。用白话来说,模块联邦提供了一种模块间共享代码的能力,为前端代码组织结构提供新的思路。

用它能干哈

那么谈谈能用他做啥

  1. 模块拆解。庞大的应用会面临代码编译速度慢,各个模块迭代快速上线排队等,拆解模块解决问题
  2. 能力共享 。a应用写了某种能力,b应用也需要,常规做法再贴一份做迁移,有了模块联邦直接共享。
  3. 资源共享。传统的npm共享面临资源同步问题,使用模块联邦一键更新。
  4. ...more

咋用捏

引入ModuleFederationPlugin,w5自带,无需安装 include-1.png

宿主配置也类似,就不贴了。

如何引用暴露出的Search呢,import('search/Search')

原理

基础概念

常见的import

先从前置基础开始聊聊,我们的import变成了啥,大概手撸一段伪代码就是酱紫,通过webpack_require来取我们的import的东西,其实是被存进了一个缓存对象,用到时先检查模块是否已经加载过了,如果加载过了直接返回,没有加载过就调加载的方法的加载。

import React from 'react';

export default function MyCom () {
  return <div> hello world </div>;
}
复制代码
(() => {
function webpack_require(dep) {
  return webpack_module[dep]
}
function define(key, obj) {
 	webpack_module[key] = obj;
}
  
  ((webpack_require, define) => {
    const namespace = 'src_myCom_index_jsx';
    const myDep = {
      React: 'node_modules_react_index_js'
    };
    function MyCom() {
    	return webpack_require(myDep.React).creatElement(div, 'hello,world')
  	}
    define(namespace, {
      default: MyCom,
      xx: xxx
    });
  })(webpack_require)
})()
复制代码

moduleMap从何而来

能够根据我们传入的模块id返回模块的内容,那么必然有映射的对象,那么这个nb的moudleMap是从何而来,在我们打出来的bundle.js,大概有酱婶儿的东东~

(function (modules) {})({
    "./src/main.js": (function (module, __webpack_exports__, __webpack_require__) {}),
})();
复制代码

动态的import被解析成啥样呢

大概解析成酱紫

__webpack_require__.e( /*! import() */ 0).then(__webpack_require__.bind(null, /*! ./chunka*/ "./src/chunka.js")).then(module => {});
复制代码

大概就是用__webpack_require__.e,用类似于jsonp的方法去加载chunk,等加载完了再把这堆塞到自己的map里面。

///////

开始我们的正题,神奇的MF

MF的文件结构

简单写一下,大致拥有这点东西,表达的意思就是app1可以引入app2的Button,app2可以引入app1的AppOneCom,他们共享依赖react、react-dom,如果不使用devserver,还需要在app1的html里加 ,app2的同理。

app1
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
---AppOneCom.jsx 用于暴露的组件
 
app2
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
---Button.jsx 用于暴露的组件
复制代码

MF都打出来了啥

和以前不一样的大致就是这些,其他chunk和bundle我就不列举了。

内容.png

考究的加载顺序

loadorder.png

依次在加载远程模块时,它知道自己依赖的共享模块,然后去检测是否存在,不存在依次去加载,所以依赖就位后才开始执行自己。

加载顺序先加载main.js是因为是要知晓我们的远程主机,然后拉取远程主机的入口文件remoteEntry,拉取共享的依赖,加载远程主机的chunk。

流程分析、梳理源码

大致瞅一眼打出来的东西,webpack那些工具方法.s .l .e等等之类的都有两套在main和RemoteEntry里,好家伙,啥都有两套呢,各处理各的。

diff.png

异步加载文件的__webpack_require__.e,和以前不太一样,调用了__webpack_require__.f,把f上的方法挨个执行了遍,f上的remote,cosumes,j这仨。remote是处理远程需要引入的文件的,cosumes处理共享文件的,j是jsonp是真正执行拉取的地方。这块代码比较多也比较易懂,就不贴了。

remoteEntry第一行

firstline.png

image-20210404191426431.png

这也解释了window上为啥有app_two,这些初始化加载的,后面也会通过window在调。

crucial point!

既然上面的分析都看到了,main.js和remoteEntry里的啥啥都不是一套,对于共享的文件咋整呢,我总不能拉取两遍吧,分别给modulemap赋值吧,那还叫啥子共享,必须保持单例在store里面。关键点来咯,我简写下就是酱紫

在main.js里面,被共享的react...

register('react', '17.0.0', () => __webpack_require__.e('node_modules_react_index_js').then(() => () => __webpack_require__('./node_modules/react/index.js')));
// ....这堆register执行完了 执行了initExternal
initExternal("webpack/container/reference/app_two");

// initExternal方法大概就是酱紫
function initExternal() {
  var module = __webpack_require__(id);
	if (!module) return;
	var initFn = module => module && module.init && module.init(__webpack_require__.S[name], initScope);
  if (module.then) return promises.push(module.then(initFn, handleError));
}
复制代码

initExternal方法看清没看清没,__webpack_require__虽然这家伙是自己的,但是返回的是module,这家伙可是个对象上面还挂着方法,假想如果这家伙是app2的,那就是他们通讯的方式了,他把自己的webpack_require.s 传过去了。那么就去康康module是个啥。

function __webpack_require__(moduleId) {
  ...omit
  // 其他的先不看,这段话是执行我们__webpack_modules__['webpack/container/reference/app_two'],沿着寻找我们穿进去的"webpack/container/reference/app_two"对应的是啥
__webpack_modules__[moduleId](module, module.exports, __webpack_require__); // Return the exports of the module
}
// 找到这段
'webpack/container/reference/app_two': /***/ (module, __unused_webpack_exports, __webpack_require__) => {
            module.exports = new Promise((resolve, reject) => {
                __webpack_require__.l(
                    'http://localhost:3002/remoteEntry.js',
                    event => {
                       // 异常处理,omit
                    },
                    'app_two'
                );
            }).then(() => app_two);
}
  // 这段执行完了取module.exports就是Promise,PromiseValue是app_two,我在这里打log
复制代码

shichui.png

实锤了这个module的promiseValue就是window.app_two,印证了上面的猜测,调了2的init,init里将传进来的赋值给自己,2的init如下。

var init = (shareScope, initScope) => {
	if (!__webpack_require__.S) return;
	var oldScope = __webpack_require__.S["default"];
	var name = "default"
  // crucial point
	__webpack_require__.S[name] = shareScope;
	return __webpack_require__.I(name, initScope);
};

复制代码

大概上面的问题也解释通了,如何实现的共享,main拉完了资源通知了remoteEntry的去赋值,至于版本不一致问题啥的不再细说,有兴趣可以深入探讨。

总结

优势还是很明显的,几乎无需二次开发成本的代码模块拆分,丝滑的共享,开箱即用...(我在吹啥)

模块联邦为微前端提供的新思路还是很惊艳的,期待更多发现~

文章分类
前端
文章标签