应用:组件跨项目实时共享,页面运行时按需加载另外一个应用的chunk,开箱即用,直接执行
区别于npm编译引入,类似<script src="http://cdn/jquery.js" />,只不过共享的模块只会有业务上的代码,不会有底层的框架和公用库代码
项目如何配置
app1应用:webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js", //共享模块的入口控制文件,共享的代码不一定会在此文件中
library: { type: 'var', name: 'app1Global' }, // 共享模块的全局引用
remotes: {
app2: "app2@http://localhost:3002/remoteEntry.js", // 远程其他共享模块的加载地址
},
exposes: {
"./Content": "./src/components/Content", // 共享组件定义
"./Button": "./src/components/Button",
},
shared: ['vue'] // 全局模块剥离
}),
]
app2应用:webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: "app2",
filename: "remoteEntry.js", //共享模块的入口控制文件,共享的代码不一定会在此文件中
library: { type: 'var', name: 'app2Global' }, // 共享模块的全局引用
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js", // 远程其他共享模块的加载地址
},
exposes: {
"./Form": "./src/components/Form", // 共享组件定义,
},
shared: ['vue'] // 全局模块剥离
}),
]
如果exposes 为空对象,则webpack不会打出remoteEntry.js包,逻辑上讲没有共享的组件自然也不会打包共享模块
项目中如何使用共享模块
这里以app2应用使用app1里面的组件举例:
// layout.vue
<template>
<div class="layout-app">
<content-element />
<button-element />
</div>
</template>
<script>
export default {};
</script>
// main.js
import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";
const Content = defineAsyncComponent(() => import("app1/Content"));
const Button = defineAsyncComponent(() => import("app1/Button"));
const app = createApp(Layout);
app.component("content-element", Content);
app.component("button-element", Button);
app.mount("#app");
import("app1/Content") 会被解析大致为:
loadScript("http://localhost:3001/remoteEntry.js").then(() => require('Content'))
核心逻辑 & 源码解析
1: 首先从共享模块打出的包开始:
比如Button.vue组件,和正常的打出的包结构是一致的
(self["webpackChunk_vue3_demo_home"] = self["webpackChunk_vue3_demo_home"] || []).push([[488,347],{
// __unused_webpack_module:模块对象,__webpack_exports__: 导出对象, __webpack_require__:模块加载器
347: ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
// 定义导出对象中的default属性即默认导出值 : export default button
__webpack_require__.d(__webpack_exports__, {
"default": () => __WEBPACK_DEFAULT_EXPORT__
});
// 252 是vue文件,返回的对象里面有 h渲染器
var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(252);
// button 组件业务代码
const button = {
name: "btn-component",
render() {
return (0,vue__WEBPACK_IMPORTED_MODULE_0__.h)(
"button",
{
id: "btn-primary",
},
"Hello World!!!"
);
},
};
const __WEBPACK_DEFAULT_EXPORT__ = (button);
})
}]);
(self["webpackChunk_vue3_demo_home"] = self["webpackChunk_vue3_demo_home"] || []).push 的作用是执行后,会将该模块代码执行,并将返回的模块值,注册到全局的模块池中缓存下来
2:从上面可以看到共享模块的包跟平时的包是一样的结构,这里webpack为了让其他应用方便访问,提供了一个统一的访问入口remoteEntry.js ,remoteEntry.js负责维护管理加载一系列共享模块,本身不涉及具体的业务代码
var app1;app1=(() => { // webpackBootstrap
// 该模块定义了各种共享模块的导出方法
var __webpack_modules__ = {270: function(module,export,require){
Button: () => import(Button.vue)
Form: () => import(Form.vue)
Title: () => import(Title.vue)
export.get = (module) {return maps[module]()}
}}
/********************************* webpack的基础方法 ***************************************/
***********
********
/********************************* 加载270这个模块,这个模块里面属性是各种export的对象,这些对象就是共享模块对象 ***************************************/
return __webpack_require__(270); // { get(module){} }
})()
那么app2应用是如何根据remoteEntry这个模块加载指定的组件的呢,比如
import(app1/button)
app2应用会载入270这个模块姑且叫remote,然后引入button组件:remote.get(button)会从webpack_modules里面拿到加载组件的方法,执行webpack_require.e,并返回promise,
具体源码如下:
var __webpack_modules__ = ({
270: ((__unused_webpack_module, exports, __webpack_require__) => {
// 具体业务组件以及对应组件的加载策略,252代表vue公共库,347代表button.vue组件
var moduleMap = {
"./Content": () => {
return Promise.all([__webpack_require__.e(252), __webpack_require__.e(561)]).then(() => () => (__webpack_require__(561)));
},
"./Button": () => {
return Promise.all([__webpack_require__.e(252), __webpack_require__.e(347)]).then(() => () => (__webpack_require__(347)));
}
};
// 外部应用调用get方法加载具体业务组件
var get = (module, getScope) => {
.........
getScope = moduleMap[module]()
.........
return getScope;
};
// 处理全局依赖共享的问题
var init = (shareScope, initScope) => {
// 处理共享模块的全局依赖比如Vue模块的逻辑
};
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
get: () => get,
init: () => init
});
})
3: app2应用 是如何载入app1应用的remoteEntry.js的呢
// main.js
import("app1/Button")
会编译成如下代码:
__webpack_require__.e(/* import() */ 190).then(__webpack_require__.t.bind(__webpack_require__, 190, 23))
webpack_require.e作为加载远程chunk的方法,可与webpack_require加载本地脚本进行对比
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
这里遍历执行webpack_require.f里面的方法并收集返回的promise,当所有的promise都完成时意味着这个远程的模块已经加载到内存里面了,注册进webpack_modules这个全局模块缓冲池里面了
webpack_require.f 有3个属性:
{
remotes:(chunkId, promises) => {},
jsonp: (chunkId, promises) => {
installedChunks[chunkId] = 0;
};
}
jsonp这个方法的作用如下:
1: 存储已加载或者正在加载的chunk
2: 根据chunkId 查找到的值为undefined 代表:chunk not loaded, 为null 代表 chunk preloaded/prefetched
3: 根据chunkId 查找到的值为Promise = chunk loading, 0 = chunk loaded
remotes这个方法的作用是:
1:载入远程模块
2:获取远程模块里面指定的业务组件模块
在main.js 编译后的chunk文件最开始的地方对webpack_modules进行如下初始化
var __webpack_modules__ = ({
293: ((module, __unused_webpack_exports, __webpack_require__) => {
var error = new Error();
module.exports = new Promise((resolve, reject) => {
if(typeof home !== "undefined") return resolve();
__webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {
if(typeof home !== "undefined") return resolve();
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ScriptExternalLoadError';
error.type = errorType;
error.request = realSrc;
reject(error);
}, "home");
}).then(() => home);
})
});
<strong>里面293这个模块在初始化的时候定义,当webpack_require(293)执行这个模块时,会去载入app1的远程remoteEntry.js 并返回一个promise</strong>
回到最开始引入远程组件的地方:webpack_require.e(/* import() */ 190)
根据上面所说里面会执行webpack_require.f.remotes(190, promises<type: []>), 这个方法主要做如下事情:
1:找到远程组件对应的远程加载配置,这个配置里面包含如下信息:
{
远程模块的全局作用域标识,通过这个标识找到模块对应的全局依赖列表,这个作用是解决全局依赖的问题
远程模块id(远程应用的入口控制模块,包含多个组件),
需要加载的组件名称,
加载远程模块脚本的promise(控制只加载一次)
}
2:执行加载远程脚本,并将promise存到promises里面
3:当promise完成后,执行webpack_require.I,给远程模块注入全局依赖,这个过程比较复杂,是一个递归的过程,详细会在下面讲到
4:webpack_require.I 会返回一个promise,当全局依赖注入完成后,远程模块external会调用get方法
external.get(远程业务组件名称) 返回组件模块对象
未完待续。。。
具体代码解释如下:
/********************************* webpack的基础方法 ***************************************/
// The module cache
var __webpack_module_cache__ = {};
// The require function
__webpack_require__(){...}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
// define getter functions for harmony exports
__webpack_require__.d() {...}
// 用于加载远程共享模块,__webpack_require__.f对象里面定义了一组处理和加载共享模块的方法,
// 这些方法执行返回promise,等所有promise都完成后,如下方法返回共享模块处理加载完成的最终的promise
__webpack_require__.e() {...}
// 作用是根据chunkId,返回对应的chunk地址
__webpack_require__.u() {return "" + chunkId + ".js"}
// 和上面作用类似,返回对应 chunk.css
__webpack_require__.miniCssF() {...}
// 全局变量
__webpack_require__.g
// hasOwnProperty
__webpack_require__.o(){...}
// load script
__webpack_require__.l(){...}
// define __esModule on exports
__webpack_require__.r(){...}
// 加载共享模块以及导出共享模块里面的指定导出对象
__webpack_require__.f.remotes(){...}
// 全局依赖处理,并同步到共享模块里面,使得远程模块能共享全局的Vue之类的
__webpack_require__.S = {};
__webpack_require__.I(){...}
// 本地chunk的publicPath 设置
__webpack_require__.p
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
__webpack_require__.f.j
// 注册并执行异步加载的本地chunk, 与import('./abc.vue') 有关
webpackJsonpCallback(){...}
参考相关代码:
https://zhuanlan.zhihu.com/p/115403616
https://juejin.cn/post/6844904187147321352
https://segmentfault.com/a/1190000022373938
https://cloud.tencent.com/developer/article/1703407
https://blog.csdn.net/sendudu/article/details/110232830