从bundle中分析webpack5 Module Federation
背景
部门启动了新的项目,技术选型使用到了webpack5 Module Federation,因此我对该新特性做了一番研究,本文从bundle中来分析webpack是如何实现remote component调用以及package的共享。 带着这两个问题开启咱们探索之旅吧
案例配置
展示配置之前先解释一下host和remote的关系,从微前端角度来解释,host就是基座。remote就是子应用
Remote
new ModuleFederationPlugin({
// 子应用名称
name: 'home',
// 获取子应用入口
filename: 'remoteEntry.js',
exposes: {
// 将这两个组件共享出去
'./Content': './src/components/Content',
'./Button': './src/components/Button',
},
shared: {
// 与基座共享一个package
vue: {
singleton: true,
},
},
});
Host
new ModuleFederationPlugin({
// 基座名称
name: 'layout',
remotes: {
// 连接子应用
home: 'home@http://localhost:3002/remoteEntry.js',
},
shared: {
// 与子应用共享一个package
vue: {
singleton: true,
},
},
});
Host的./src/main.js
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));
const app = createApp(Layout);
app.component('content-element', Content);
app.component('button-element', Button);
app.mount('#app');
bundle分析
看构建后的产物,需要做一些公共方法和变量的解释,这边会更利于你的阅读
- webpack_modules, 存储的是module,例如webpack配置的entry
- webpack_require,从__webpack_modules__中加载模块
- webpack_require.e,动态加载模块,比如,import('./Button')
- webpack_require.o,判断对象中是否有该元素,用的是hasOwnProperty该方法
- webpack_require.d,在对象上定义getter方法,用的是 defineProperty
- webpack_require.S,存储的共享package get方法
- webpack_require.l 加载共享package 并将 package 注入到远程组件内
remote的共享组件的入口 remoteEntry.js
// ....
// module入口
var __webpack_modules__ = {
'webpack/container/entry/home': (__unused_webpack_module, exports, __webpack_require__) => {
// remote 向外提供的组件
var moduleMap = {
'./Content': () => {
return Promise.all([
__webpack_require__.e('webpack_sharing_consume_default_vue_vue'),
__webpack_require__.e('src_components_Content_vue-_b1070'),
]).then(() => () => __webpack_require__('./src/components/Content.vue'));
},
'./Button': () => {
return Promise.all([
__webpack_require__.e('webpack_sharing_consume_default_vue_vue'),
__webpack_require__.e('src_components_Button_js-_e56a0'),
]).then(() => () => __webpack_require__('./src/components/Button.js'));
},
};
// host 通过该方法获取 ‘./Content’ ‘./Button’ 组件
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = __webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
});
__webpack_require__.R = undefined;
return getScope;
};
// host 将共享的package注入到remote,便于 ‘./Content’ ‘./Button’获取 package
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = 'default';
var oldScope = __webpack_require__.S[name];
if (oldScope && oldScope !== shareScope)
throw new Error(
'Container initialization failed as it has already been initialized with a different share scope',
);
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
// 给 webpack/container/entry/home 模块添加 getter,get 和 init 方法
// getter 是 __webpack_require__.d 实现的
__webpack_require__.d(exports, {
get: () => get,
init: () => init,
});
},
};
// ...
// 加载 上面的 webpack/container/entry/home module
var __webpack_exports__ = __webpack_require__('webpack/container/entry/home');
// 将 webpack/container/entry/home module导出的两个方法 get 和 init 挂载到 home 上
home = __webpack_exports__;
上面是remote的入口文件 remoteEntry.js,它主要干了这些事
- window 挂在 home 变量,home是 remote和host沟通的桥梁
- 加载 webpack/container/entry/home,将 remote的Content、Button 组件get和共享package注入方法暴露出去
先简单介绍上面两个,其实已经将咱们的问题解释了三分之一了(如何实现remote component调用以及package的共享)。
咱们接着看看,Host干了什么
Host 入口 main
Host这边做的比较多,咱们从入口文件一段段代码分析
// host 模块入口
var __webpack_modules__ = {
// 入口module 入口
'./src/index.js': (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
__webpack_require__
.e(/* import() */ 'src_main_js')
.then(__webpack_require__.bind(__webpack_require__, './src/main.js'));
},
// remote module 的入口 其实就是 remoteEntry
'webpack/container/reference/home': (module, __unused_webpack_exports, __webpack_require__) => {
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
// 防止重复加载
if (typeof home !== 'undefined') return resolve();
// 使用jsonp加载 remoteEntry.js 并执行 remoteEntry.js
__webpack_require__.l(
'http://localhost:3002/remoteEntry.js',
event => {
if (typeof home !== 'undefined') return resolve();
// remote加载或者执行失败回调异常抛出
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
__webpack_error__.message =
'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
__webpack_error__.name = 'ScriptExternalLoadError';
__webpack_error__.type = errorType;
__webpack_error__.request = realSrc;
reject(__webpack_error__);
},
'home',
);
// 返回 remote export === home
}).then(() => home);
},
};
// ....
var __webpack_exports__ = __webpack_require__('./src/index.js');
上面代码块加载入口模块 ./src/index.js
,加载这个模块主要干了这些事
- 执行__webpack_require__.e(/* import() */ 'src_main_js') a. 主要去加载共享package vue、remote remoteEntry.js和src_main_js ,并将 vue 的get方式注入到 remote 中,并将 Button和Content 组件的get方法暴露到 host中,具体看代码实现 i.加载共享package vue
register('vue', '3.2.41', () =>
__webpack_require__
// 远程加载vue组件,并将 vue内的模块通过 ./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js 挂在到 _webpack_module_ 中
.e('vendors-node_modules_vue_runtime-dom_dist_runtime-dom_esm-bundler_js')
.then(
() => () =>
// 从 _webpack_module_ 中获取 vue
__webpack_require__('./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js'),
),
);
ii. 加载 remoteEntry.js & 将 vue 的get方式注入到 remote 中
initExternal('webpack/container/reference/home');
var initExternal = id => {
// id: webpack/container/reference/home
// ...
try {
// __webpack_require__(id) 直接从 __webpack_modules__ 中获取,可以看看 上 __webpack_modules__['webpack/container/reference/home']加载
var module = __webpack_require__(id);
if (!module) return;
//
var initFn = (
module, // module: remoteEntry.js 抛出来的全局home
) =>
// module.init === home.init, home.init 将 共享package的vue注入到remoteEntry中
module && module.init && module.init(__webpack_require__.S[name], initScope);
// remoteEntry加载完成之后,执行注入
if (module.then) return promises.push(module.then(initFn, handleError));
var initResult = initFn(module);
if (initResult && initResult.then) return promises.push(initResult['catch'](handleError));
} catch (err) {
handleError(err);
}
};
iii. 加载 host的 ./src/main.js
// jsonp形式加载 ./src/main.js
__webpack_require__.f.j = (chunkId, promises) => {
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
__webpack_require__.l(url, loadingEnded, 'chunk-' + chunkId, chunkId);
};
// 加载完成之后,将 ./src/main.js 放到 __webpack_module_中
// webpackChunk_vue3_demo_layout.push 实现了将 ./src/main.js 放到 __webpack_module_中
(self['webpackChunk_vue3_demo_layout'] = self['webpackChunk_vue3_demo_layout'] || []).push([
['src_main_js'],
{
'./src/main.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {},
},
]);
iv. Button和Content 组件的get方法暴露到 host中 其实这个方式分析 remote remoteEntry.js 已经提到了,就是home.get 这个方法,如何去获取的,咱们继续往下面分析
-
执行完
__webpack_require__.e(/* import() */ 'src_main_js')
之后,继续执行后面的.then(__webpack_require__.bind(__webpack_require__, './src/main.js'))
a. 这其实就是去加载./src/main.js
模块并执行,也就是去执行vue那部分代码了,里面就涉及到了 加载 remote的Button 和Content -
加载 remote 的 Button
const Button = (0, runtime_dom_esm_bundler_js_.defineAsyncComponent)(() =>
// 下载远程的组件 Button,这里实际执行的是 __webpack_require__.f.remotes 方法
__webpack_require__.e(/* import() */ 'webpack_container_remote_home_Button').then(
// 加载 Button 组件
__webpack_require__.t.bind(__webpack_require__, 'webpack/container/remote/home/Button', 23),
),
);
const app = (0, runtime_dom_esm_bundler_js_.createApp)(Layout);
app.component('content-element', Content);
a.看看 webpack_require.f.remotes 如何下载 remote Button i. 先通过 webpack/container/reference/home module查找到 remoteEntry.js export,也就是上面说的 home,包含get和init方法 ii.然后通过 home.get('./Button') 获取 对应的remote Button,并将它缓存下下来
/* webpack/runtime/remotes loading */
(() => {
var chunkMapping = {
webpack_container_remote_home_Button: ['webpack/container/remote/home/Button'],
};
var idToExternalAndNameMapping = {
'webpack/container/remote/home/Button': [
'default',
'./Button',
'webpack/container/reference/home',
],
};
// chunkId: webpack_container_remote_home_Button
__webpack_require__.f.remotes = (chunkId, promises) => {
if (__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach(id => {
// id: webpack/container/remote/home/Button
var getScope = __webpack_require__.R;
if (!getScope) getScope = [];
// data: ['default', './Button', 'webpack/container/reference/home'];
var data = idToExternalAndNameMapping[id];
if (getScope.indexOf(data) >= 0) return;
getScope.push(data);
if (data.p) return promises.push(data.p);
var handleFunction = (fn, arg1, arg2, d, next, first) => {
try {
var promise = fn(arg1, arg2);
if (promise && promise.then) {
var p = promise.then(result => next(result, d), onError);
if (first) promises.push((data.p = p));
else return p;
} else {
return next(promise, d, first);
}
} catch (error) {}
};
var onExternal = (external, _, first) =>
external
? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first)
: onError();
var onInitialized = (_, external, first) =>
handleFunction(external.get, data[1], getScope, 0, onFactory, first);
var onFactory = factory => {
data.p = 1;
__webpack_require__.m[id] = module => {
module.exports = factory();
};
};
handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
});
}
};
})();
总结
用一张图来解释一下,package如何共享、remote component 如何加载的
转载自:https://juejin.cn/post/7158873605059641374