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