html5做網(wǎng)站鏈接范例網(wǎng)站推廣100種方法
module federation是什么
webpack5新增了module federation,module federation的作用,將每個構(gòu)建(build)作為容器(這是一個概念),構(gòu)建后的資源可以正常部署,同時還具備在運行時對外暴露其中的模塊,這就意味著多個構(gòu)建可以獨立完成,獨立部署,所需的依賴可以在運行時加載。對于多個構(gòu)建公共的依賴,可以通過shared來指定,這些依賴也可以在運行時加載,并且只加載一次。
事實上,公共依賴模塊也可以通過npm包的形式來實現(xiàn)共享,這種方式的共享不得不依賴于app shell這種容器預先加載共享包。module federation只在第一次加載模塊A時加載共享包,加載模塊B時共享包已被緩存。
模塊與容器的概念有點重疊,容器實際上是一些模塊的集合,與構(gòu)建相關(guān),是我們傳統(tǒng)意義上的bundle,只是在加入module federation能力后,容器可以對外導出成員,這一點跟模塊比較接近而已。
這種能力剛好與微前端架構(gòu)所需的前端集成能力一致。前端集成技術(shù),就是應用A將應用B、C等應用的某些頁面、組件、片段等等,集成到自己頁面里。
加入module federation的構(gòu)建
傳統(tǒng)的構(gòu)建過程,模塊之間如果存在依賴關(guān)系,這些模塊會在一個構(gòu)建過程中打包成一個bundle。
而module federation讓這種存在依賴關(guān)系的模塊,各自有各自的構(gòu)建過程,并各自實現(xiàn)自己的bundle和部署,最終在運行時異步獲取依賴模塊。這種方式提高了模塊的自主性,但可能因為異步的原因,降低了首屏渲染性能、運行時的用戶交互體驗等等。
有對外導出的容器示例
這里展示一個module federation的示例使用。products項目展示一些產(chǎn)品名稱,其工程目錄如下
- products/- public/- index.html // 模板- src/- bootstrap.js // 導入faker生成假數(shù)據(jù),導出一個mount方法,將html內(nèi)容掛載到某個DOM節(jié)點上- index.js // 入口文件,導入bootstrap.js模塊- package.json - webpack.config.js // 配置module federation的配置文件
bootstrap.js的內(nèi)容如下
// 聲明為shared的模塊,會被拆分為異步模塊,所以需要異步加載
const faker = await import("faker");function mount(el) {let products = "";for (let i = 1; i <= 5; i++) {products += `<div>${faker.commerce.productName()}</div>`;}el.innerHTML = products;
}if (process.env.NODE_ENV === "development") {// 依賴該模塊的容器,必須提供一個id屬性為'dev-products'的DOM元素const el = document.querySelector("#dev-products");if (el) mount(el);
}export { mount };
構(gòu)建產(chǎn)物
webpack的module federation相關(guān)配置示例
devServer: {port: 8081,},// 省略其他傳統(tǒng)的配置plugins: [new ModuleFederationPlugin({name: "products", // 當前容器的名稱,其他容器導入該容器時的標識filename: "remoteEntry.js", // 當前容器的入口文件,與output.filename不是一回事exposes: {// 導出成員對應的模塊會被拆離為異步模塊"./Index": "./src/bootstrap.js", // 指定對外暴露的模塊列表,標識符: 模塊地址},shared: { // 公共的共享模塊,其他容器也會使用faker這個模塊,共享模塊在構(gòu)建產(chǎn)物會被作為單獨的包存在,由容器異步加載faker: {singleton: true,},},}),
],
在加入module federation的webpack配置下,構(gòu)建產(chǎn)物發(fā)生了一定的變化,除了傳統(tǒng)的bundle外,還會有如下產(chǎn)物
- module federation插件產(chǎn)生的容器入口文件,如上面配置的
remoteEntry.js
; - 對外暴露的模塊,如上面配置的"./Index": "./src/bootstrap.js"的產(chǎn)物
src_bootstrap_js.js
; - 共享模塊,如上面配置的faker,產(chǎn)物是
vendors-node_modules_faker_index_js.js
;
而通過模板生成的index.html中,不僅有傳統(tǒng)的bundle產(chǎn)物main.js
,也會有remoteEntry.js
。對于傳統(tǒng)的部署而言,remoteEntry.js
是沒必要的。main.js與remoteEntry.js有很多重復的webpack膠水代碼。
容器對外的入口文件remoteEntry.js
查看由module federation生成的容器入口文件,可以看到與傳統(tǒng)的bundle不一樣的地方在于,容器入口文件包含一個變量聲明var products
, 與配置name: "products"
一致。
remoteEntry.js會包含一個moduleMap,包含模塊標識符’./Index’與src_bootstrap_js.js
;
remoteEntry.js包含本地容器的初始化init方法和獲取導出成員get方法,被加載后導出了products變量,攜帶init和get方法。
/***/ // "webpack/container/entry/products":
/*!***********************!*\!*** container entry ***!\***********************/
/*** remoteEntry.js中 "webpack/container/entry/products" 對應的函數(shù)內(nèi)部的eval代碼整理如下*/var moduleMap = {"./Index": () => {return __webpack_require__.e("src_bootstrap_js").then(() => () =>__webpack_require__(/*! ./src/bootstrap.js */ "./src/bootstrap.js"));},
};
// container的getter,將container中的module加載,并返回加載后的module
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;
};
// 初始化容器,通過shareScope來提供對外共享的module,如果聲明了shared,每個build都會有shared的module,即便有重復
// 如果共享module已經(jīng)被使用了,那么該容器的共享module會被忽略,但會作為fallback
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);
};// This exports getters to disallow modifications
__webpack_require__.d(exports, {get: () => get,init: () => init,
});//# sourceURL=webpack://products/container_entry?;
有導入其他容器的容器示例
通常導入別的容器的容器會作為一個app shell,加載其他容器的模塊,這正是微前端的客戶端集成方案。這里的container項目,加載products的bootstrap模塊,使用mount方法掛載HTML內(nèi)容。目錄示例如下
- container/- public/- index.html // 模板- src/- bootstrap.js // 導入products的bootstrap模塊,使用mount方法,將html內(nèi)容掛載到某個DOM節(jié)點上- index.js // 入口文件,導入bootstrap.js模塊- package.json - webpack.config.js // 配置module federation的配置文件
其中,bootstrap.js的內(nèi)容如下
// 這里不使用const { mount: mountProducts } = await import("products/Index")的語法
// 是因為模塊本身不導入導出任何成員,webpack不認為是ESM,只有ESM才能使用頂層的await語法
// 在這種情況下,使用import ESM語法,那么index.js必須使用import('./bootstrap.js')的動態(tài)導入語法,因為遠程模塊的導入必須是異步的
import { mount as mountProducts } from "products/Index"
// 模板index.html中已經(jīng)有<div id="prod-products"></div>
mountProducts(document.querySelector("#prod-products"));
相關(guān)的module federation配置如下
devServer: {port: 8080},plugins: [new ModuleFederationPlugin({name: "container", // 容器名稱remotes: { // 指定遠程模塊依賴// 模塊別名: '模塊別名@模塊入口地址',模塊別名是要在代碼里導入該模塊成員時使用products: "products@http://localhost:8081/remoteEntry.js", // 模塊名稱: 模塊地址}}),
container容器應用可以拿到products容器應用的bootstrap模塊,使用mount方法來掛載HTML內(nèi)容了??梢試L試一下,啟動8080端口,可以看到頁面有一些由products容器應用的bootstrap模塊掛載的HTML內(nèi)容。
構(gòu)建產(chǎn)物
由于container容器沒有對外暴露的模塊,因此沒有remoteEntry.js這樣的入口文件,也沒有共享模塊,所以container容器的構(gòu)建產(chǎn)物與傳統(tǒng)的構(gòu)建一致,只有bundle。bundle文件中,有包含products容器的引用和模塊加載代碼
/***/ "webpack/container/reference/products":
/*!****************************************************************!*\!*** external "products@http://localhost:8081/remoteEntry.js" ***!\****************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {if(typeof products !== "undefined") return resolve();__webpack_require__.l("http://localhost:8081/remoteEntry.js", (event) => {if(typeof products !== "undefined") return resolve();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__);}, "products");
}).then(() => (products));/***/ })
模塊成員標識符規(guī)則
模塊對外導出的成員應該如何標識?例如,products對外導出的./Index
能不能寫成’Index’或者’Hello’?
在導入成員時,使用import xxx from 'products/Index'
,webpack會轉(zhuǎn)換為’./Index’作為模塊標識符,因此products對外導出成員時的標識符不能隨意寫,要按照規(guī)則./[name]
的形式來書寫;
webpack內(nèi)部使用了一系列映射關(guān)系來確定導出成員,如下面代碼所示
var chunkMapping = {/******/ // src/bootstrap.js中導入了products/Index和products/World"src_bootstrap_js": [/******/ "webpack/container/remote/products/Index", /******/"webpack/container/remote/products/World"/******/]/******/};/******/var idToExternalAndNameMapping = {/******/"webpack/container/remote/products/Index": [/******/"default", /******/"./Index", /******/ 導入成員的標識符"webpack/container/reference/products"/******/],/******/"webpack/container/remote/products/World": [/******/"default", /******/"./World", /******/ 導入成員的標識符"webpack/container/reference/products"/******/]/******/};
異步模塊有異步依賴時使用異步導入還是同步導入
module federation導致的模塊拆分,如果是異步模塊A依賴了異步模塊B,在A中可以同步導入模塊Bimport xxx from 'module-B'
,因為webpack會使用Promise.all來加載模塊A和被A依賴的模塊B。所以只要使用動態(tài)導入`import(‘module-A’)即可,不需要在A中使用動態(tài)導入B了。當然,動態(tài)導入B模塊也是可以的。
總結(jié)
module federation是一種支持當前應用在運行時加載其他運行時應用內(nèi)部模塊的技術(shù),在webpack配置時,當前應用需要用remote指定要加載的應用名稱, 其他應用使用exposes指定對外暴露的內(nèi)部模塊,使用shared指定公共的共享模塊。應用可以各自獨立構(gòu)建,獨立部署,只在運行時產(chǎn)生耦合(加載)。各個應用在開發(fā)構(gòu)建時都是獨立的,降低了開發(fā)構(gòu)建時的耦合性。