微前端架構(gòu)ModuleFederationPlugin源碼解析_第1頁(yè)
微前端架構(gòu)ModuleFederationPlugin源碼解析_第2頁(yè)
微前端架構(gòu)ModuleFederationPlugin源碼解析_第3頁(yè)
微前端架構(gòu)ModuleFederationPlugin源碼解析_第4頁(yè)
微前端架構(gòu)ModuleFederationPlugin源碼解析_第5頁(yè)
已閱讀5頁(yè),還剩14頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

第微前端架構(gòu)ModuleFederationPlugin源碼解析目錄序言背景MF基本介紹應(yīng)用場(chǎng)景微前端架構(gòu)服務(wù)化的library和componentsModuleFederationPlugin源碼解析入口源碼ExposesRemotesShared小結(jié)總結(jié)

序言

本文是WebpackModuleFederationPlugin(后面簡(jiǎn)稱MF)源碼解析文章中的第一篇,在此系列文章中,我將帶領(lǐng)大家抽絲剝繭、一步步地去解析MF源碼。當(dāng)然為了幫助大家理解,可能中間也會(huì)涉及到Webpack源碼中的其它實(shí)現(xiàn),我會(huì)根據(jù)情況或淺或深的一并進(jìn)行講解。因?yàn)榭碬ebpack源碼需要掌握的知識(shí)量非常大,所以為了更好理解文章中的內(nèi)容,你最好有如下Webpack相關(guān)的背景知識(shí):

對(duì)Webpack核心的數(shù)據(jù)結(jié)構(gòu):Dependency、Module、Chunk等有基本的認(rèn)識(shí)了解Webpack中的插件機(jī)制,對(duì)基于tabpable的Hooks機(jī)制有一定的了解,如果寫過Webpack插件就更好了看過MF的官方文檔,對(duì)其帶來的關(guān)鍵性作用有基本的認(rèn)識(shí),如果了解一些其應(yīng)用場(chǎng)景就更好了

話不多說,讓我們開始正文。

背景

先簡(jiǎn)單說一下為什么要去閱讀MF的源碼,我個(gè)人理解閱讀源碼有兩個(gè)原因:

一,它的實(shí)現(xiàn)非常優(yōu)秀,通過閱讀源碼能學(xué)習(xí)一些設(shè)計(jì)思想和編程技巧;

二,工作或者自己的項(xiàng)目使用到了,但是官方給的文檔不太夠,遇到問題無論最后有沒有解決,都有點(diǎn)摸不著頭腦,閱讀源碼是為了更好地了解其內(nèi)部實(shí)現(xiàn),遇到問題更容易debug。

而我閱讀MF的源碼,主要是出于第二種目的,當(dāng)然我個(gè)人對(duì)Webpack也是非常感興趣。目前我們部門B端的產(chǎn)品是基于MF實(shí)現(xiàn)的微前端架構(gòu),而我主要負(fù)責(zé)B端的開發(fā)以及參與B端性能優(yōu)化專項(xiàng),今年大部分時(shí)間都是跟MF搏斗。

雖然到目前為止,性能優(yōu)化已經(jīng)獲得了一些階段性的勝利,但是實(shí)際上在這個(gè)過程中,我們還是走了很多彎路。這些彎路不少是由于對(duì)MF內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)不夠了解導(dǎo)致的,當(dāng)然除此之外,我們還需要建立一套規(guī)范的MF標(biāo)準(zhǔn)化開發(fā)流程。所以,閱讀MF源碼對(duì)于我個(gè)人來說非常有必要。

首先,我們先簡(jiǎn)單了解下MF相關(guān)知識(shí)。

MF基本介紹

首先,MF是一個(gè)Webpack的官方插件,在Webpack生態(tài)中有茫茫多的插件中,好像一個(gè)插件有點(diǎn)微不足道。但是,MF的作者稱其為Agame-changerinJavaScriptarchitecture,當(dāng)然從構(gòu)建工具的角度來講,有點(diǎn)言過其實(shí),因?yàn)樗荒苡糜赪ebpack中。

但是從它帶來的JavaScript架構(gòu)設(shè)計(jì)上的理念:遠(yuǎn)程依賴共享(復(fù)用組件或者其它邏輯),我覺得其實(shí)是給前端帶來新的思考視角。

以前我們復(fù)用組件或者邏輯主要的方式有:

抽離一個(gè)NPM包,從維護(hù)性和復(fù)用性角度來講,是目前最常見的方式。缺點(diǎn)在于,在微前端架構(gòu)中,如果fix了一個(gè)NPM包問題,那么每一個(gè)應(yīng)用都需要升級(jí)版本,重新構(gòu)建打包部署上線,多團(tuán)隊(duì)開發(fā)的時(shí)候非常低效;將產(chǎn)物打包成UMD的格式,然后通過CDN的方式能一定程度解決重新構(gòu)建打包上線的問題,但是隨著復(fù)用的組件和邏輯越多,可能會(huì)引入很多多余的chunk問題(如果對(duì)性能有很高的要求)。比如A和B組件同時(shí)依賴了lodash,那么打包成UMD格式有多余的lodashchunk,沒法復(fù)用。

我們來看下MF是怎么解決這個(gè)問題的。首先看一個(gè)簡(jiǎn)單的MF使用的例子,假設(shè)我們現(xiàn)在有兩個(gè)應(yīng)用app1和app2:

//app1webpack.config.js

module.exports={

//省略其它配置

plugins:[

newModuleFederationPlugin({

name:'app1',

filename:'remoteEntry.js',

remotes:{

app2:'app2@http://localhost:3002/remoteEntry.js',

exposes:{

'./input':'./src/components/Input'

shared:{

'react':{

singleton:true,

requiredVersion:require('./package.json').dependencies.react

'react-dom':{

singleton:true,

requiredVersion:require('./package.json').dependencies['react-dom']

'lodash':{

requiredVersion:require('./package.json').dependencies['lodash'],

singleton:true,

//app1src/components/Input.tsx

import*asReactfrom'react'

import{Input}from'antd'

exportdefaultfunctionWrapperInput(){

return(

div

app1input:Input/

/div

//app1src/App.tsx

import{Input}from'antd';

import*asReactfrom'react';

constRemoteButton=React.lazy(()=import('app2/Button'));

constApp=()=(

div

h1Typescript/h1

h2App1/h2

React.Suspensefallback="LoadingButton"

RemoteButton/

/React.Suspense

div

Input/

/div

/div

exportdefaultApp;

app2的部分代碼:

//app2webpack.config.js

module.exports={

//省略其它配置

plugins:[

newModuleFederationPlugin({

name:'app2',

filename:'remoteEntry.js',

exposes:{

'./Button':'./src/Button',

remotes:{

app1:'app1@http://localhost:3001/remoteEntry.js',

//app2src/Button.tsx

import*asReactfrom'react';

constButton=()=buttonApp3Button/button

exportdefaultButton;

//app2src/App.tsx

import*asReactfrom'react';

importLocalButtonfrom'./Button';

importRemoteInputfrom'app1/input';

constApp=()=(

div

h1Typescript/h1

h2App3/h2

LocalButton/

React.Suspensefallback={null}

RemoteInput/

/React.Suspense

/div

exportdefaultApp;

最后實(shí)現(xiàn)的效果:

簡(jiǎn)單的Webpack配置,我們就可以實(shí)現(xiàn)app1和app2兩個(gè)應(yīng)用之間的組件遠(yuǎn)程共享,從代碼看,我們知道app1依賴了app2的Button組件,而app2依賴了app1的Input組件。

當(dāng)然不止如此,MF還可以做到:

依賴復(fù)用,app1和app2同時(shí)依賴了react和react-dom,那我們可以在雙方的Webpack配置中,將兩個(gè)依賴配置成shared,而且通過requiredVersion指定版本;微前端架構(gòu),微前端架構(gòu)有很多實(shí)現(xiàn)的方式,比如iframe、web-component等,但是MF的出現(xiàn),使得實(shí)現(xiàn)一套微前端的架構(gòu)更加簡(jiǎn)單,也能非常容易解決微前端架構(gòu)中的一些組件復(fù)用問題、頻繁構(gòu)建部署上線問題;支持服務(wù)端渲染,MF的實(shí)現(xiàn)不依賴瀏覽器,同樣的代碼,只需要將Webpack配置中的target改成

node,那么構(gòu)建的產(chǎn)物就能支持SSR。

到這里,讀者已經(jīng)對(duì)MF的使用和定位有了基本的印象,根據(jù)MF帶來的全新的復(fù)用能力,我們可以做一些應(yīng)用場(chǎng)景的思考。

應(yīng)用場(chǎng)景

微前端架構(gòu)

微前端是這幾年比較火的一個(gè)前端應(yīng)用架構(gòu)方案,其中比較核心的一點(diǎn)是各子應(yīng)用之間要做到獨(dú)立開發(fā),獨(dú)立構(gòu)建部署上線。從上一節(jié)對(duì)MF的介紹中,我們發(fā)現(xiàn)它天然就已經(jīng)有這個(gè)優(yōu)勢(shì),因此為了設(shè)計(jì)一個(gè)基于MF的微前端架構(gòu),我們要解決的第一點(diǎn)是子應(yīng)用之間需要有個(gè)類似中心化的服務(wù),將其它子應(yīng)用的服務(wù)地址下發(fā)給需要消費(fèi)的子應(yīng)用;第二點(diǎn),我們要解決子應(yīng)用之間的一些通信問題,例如共享的一些用戶狀態(tài)。當(dāng)然還有一些其它問題,例如UI一致性問題。

基于以上的問題,我們可以很容易想到一種非常經(jīng)典的微前端架構(gòu)方案,那就是基于一個(gè)基座服務(wù)的中心化的架構(gòu)方式。

每個(gè)APP都是一個(gè)子應(yīng)用,這里可以有兩種方式:如果完全不需要依賴基座的狀態(tài),則可以做成一個(gè)更加通用的前端服務(wù),只作為提供方,在MF中也稱為remotes應(yīng)用。如果需要依賴主應(yīng)用的狀態(tài),或者說只導(dǎo)出路由讓基座幫忙注冊(cè),這樣就可以共享基座的所有狀態(tài),這種方式與我們現(xiàn)在B端的架構(gòu)方式類似。這樣的架構(gòu)方式,也能通過MFshared機(jī)制鎖定UI庫(kù)的版本,保證所有子應(yīng)用UI的一致性。

服務(wù)化的library和components

跳出微前端架構(gòu),假設(shè)我們現(xiàn)在的場(chǎng)景是維護(hù)一個(gè)巨型前端應(yīng)用,我們發(fā)現(xiàn)隨著頁(yè)面和依賴的第三方依賴逐漸增多,那么每次開發(fā)構(gòu)建部署上線的時(shí)長(zhǎng)也會(huì)不斷增加。雖然Webpackv5+版本已經(jīng)做了很多優(yōu)化例如本地緩存,但是對(duì)于巨型應(yīng)用,我們還是發(fā)現(xiàn)構(gòu)建還是非常低效。于是,基于MF的能力,我們可以做這樣的一個(gè)架構(gòu)設(shè)計(jì):

我們可以將平時(shí)使用的第三方庫(kù)和組件庫(kù),分別做成一個(gè)單獨(dú)的服務(wù),如果部門技術(shù)棧統(tǒng)一的項(xiàng)目可以通過MF插件遠(yuǎn)程使用這兩個(gè)服務(wù),這樣無論是開發(fā)時(shí)還是上線構(gòu)建都可以省掉這部分的構(gòu)建時(shí)間,一定程度上提高了開發(fā)效率。

MF的使用姿勢(shì)非常靈活,你可以根據(jù)開發(fā)需要,充分挖掘更多的使用場(chǎng)景。MF介紹的部分就到這里,下面我們正式進(jìn)入源碼解析的內(nèi)容。

ModuleFederationPlugin源碼解析

入口源碼

MF插件相關(guān)的源碼放在lib/container下,我們首先看下lib/container/ModuleFedration.js的代碼:

//省略一些import代碼

classModuleFederationPlugin{

*@param{ModuleFederationPluginOptions}optionsoptions

constructor(options){

validate(options);

this._options=options;

*Applytheplugin

*@param{Compiler}compilerthecompilerinstance

*@returns{void}

apply(compiler){

const{_options:options}=this;

//expose模塊編譯產(chǎn)物導(dǎo)出的類型,選項(xiàng)有var、umd、commonjs、module等,跟output配置中的library作用是一樣的

//var代表輸出的模塊是掛在window對(duì)象上

constlibrary=options.library||{type:"var",name:};

//remote模的類型,選項(xiàng)有var、umd、commonjs、module等,跟output配置中的library作用是一樣的

constremoteType=

options.remoteType||

(options.libraryisValidExternalsType(options.library.type)

/**@type{ExternalsType}*/(options.library.type)

:"script");

//enabledLibraryTypes專門存儲(chǔ)entry需要輸出的library類型,然后被EnableLibraryPlugin插件消費(fèi),

if(

library

!compiler.options.output.enabledLibraryTypes.includes(library.type)

compiler.options.output.enabledLibraryTypes.push(library.type);

//在完成所有內(nèi)部插件注冊(cè)后處理MF插件

compiler.hooks.afterPlugins.tap("ModuleFederationPlugin",()={

if(

options.exposes

(Array.isArray(options.exposes)

options.exposes.length0

:Object.keys(options.exposes).length0)

//如果有expose配置,則注冊(cè)一個(gè)ContainerPlugin

newContainerPlugin({

name:,

library,

filename:options.filename,

runtime:options.runtime,

shareScope:options.shareScope,

exposes:options.exposes

}).apply(compiler);

if(

options.remotes

(Array.isArray(options.remotes)

options.remotes.length0

:Object.keys(options.remotes).length0)

//如果有expose配置,則初始化一個(gè)ContainerReferencePlugin

newContainerReferencePlugin({

remoteType,

shareScope:options.shareScope,

remotes:options.remotes

}).apply(compiler);

if(options.shared){

//如果有shared配置,則初始化一個(gè)SharePlugin

newSharePlugin({

shared:options.shared,

shareScope:options.shareScope

}).apply(compiler);

從代碼中可以看出,MF插件入口的代碼其實(shí)不復(fù)雜,核心的代碼不到100行,我們首先把焦點(diǎn)放在插件初始化的options參數(shù)上,它的類型為ModuleFederationPluginOptions。

這里有個(gè)細(xì)節(jié)可以注意下,因?yàn)閃ebpack的源碼是用純JS寫的,為了彌補(bǔ)如像TypeScript的類型注釋的優(yōu)勢(shì)使得源碼更加可讀的問題,Webpack使用了JSDoc配合VSCode,在大多數(shù)場(chǎng)景下也能起到類型注釋的效果,而Webpack根目錄下的declarations目錄使用了TS定義了核心的一些數(shù)據(jù)類型,然后導(dǎo)出給其它JS文件在使用JSDoc時(shí)使用。

我們回到主題,我們看下ModuleFederationPluginOptions的類型定義:

exportinterfaceModuleFederationPluginOptions{

*container應(yīng)用導(dǎo)出的模塊配置,一般是一個(gè)對(duì)象

exposes:Exposes;

*打包產(chǎn)物的文件名稱

filename:string;

*構(gòu)建產(chǎn)物的類型,里面的type配置可以是umd、commonjs、var等類型

library:LibraryOptions;

*container的名稱

name:string;

*依賴的remote應(yīng)用library類型,配置的值可以是umd、commonjs、script、var等

remoteType:ExternalsType;

*container應(yīng)用依賴的遠(yuǎn)程應(yīng)用

remotes:Remotes;

*配置了該選項(xiàng),會(huì)為模塊split一個(gè)以該名稱命名的chunk

runtime:EntryRuntime;

*所有共享模塊的作用域名稱,默認(rèn)為default,很少會(huì)修改

shareScope:string;

*應(yīng)用之間需要共享的模塊

shared:Shared;

每個(gè)選項(xiàng)我都用注釋做了簡(jiǎn)單的介紹,我們重點(diǎn)關(guān)注幾個(gè)常用的配置,對(duì)于library、runtime、

remoteType等配置平時(shí)很少使用,這里先不過多介紹,后面看到相關(guān)的源碼可以再回顧。

filename和name比較好理解,以上一小節(jié)的app1的Webpack配置為例,我們可以看到其配置如下:

newModuleFederationPlugin({

name:'app1',

filename:'remoteEntry.js',

//省略其它配置

如果這樣配置,app1正好expose了一些模塊給其它應(yīng)用消費(fèi),例如app2,則app2首先要通過app1的

name去找到它,也就是會(huì)在remotes的配置中添加app1的指向,這里等會(huì)介紹remotes選項(xiàng)時(shí)再細(xì)說。而app2在運(yùn)行時(shí)就會(huì)加載到app1的構(gòu)建產(chǎn)物remoteEntry.js,訪問app2的服務(wù),打開network,我們可以看到其加載了app1的remoteEntry.js:

我們重點(diǎn)介紹下exposes、remotes、shared等選項(xiàng)。

在上面的MF插件源碼中,其核心的幾行代碼就是,在afterPluginshook觸發(fā)后(完成其它所有內(nèi)置插件初始化后),根據(jù)是否有上面三個(gè)配置,來決定是否要注冊(cè)ContainerPlugin、

ContainerReferencePlugin、SharePlugin等插件。所以更加核心的實(shí)現(xiàn),是分別交給了上面三個(gè)插件去完成。

Exposes

exposes的配置是告訴Webpack當(dāng)前應(yīng)用導(dǎo)出給其它應(yīng)用消費(fèi)的模塊,首先我們來看下exposes配置的類型定義:

exporttypeExposes=(ExposesItem|ExposesObject)[]|ExposesObject;

exporttypeExposesItem=string;

exporttypeExposesItems=ExposesItem[];

exportinterfaceExposesObject{

[k:string]:ExposesConfig|ExposesItem|ExposesItems;

exportinterfaceExposesConfig{

import:ExposesItem|ExposesItems;

name:string;

上面的類型定義相對(duì)來說比較簡(jiǎn)單,只是套娃比較多,還是以上面app1的Webpack配置為例,據(jù)我平時(shí)了解到的,最常見的配置方式還是:

exposes:{

'./input':'./src/components/Input'

但是你也可以配置:

exposes:{

'./input':{

name:'input',

import:'./src/components/Input'

這種方式配置會(huì)有什么不一樣了?這里會(huì)留一個(gè)懸念,在看后續(xù)的源碼中,我們?cè)僭敿?xì)介紹。

Remotes

remotes配置是告訴Webpack當(dāng)前應(yīng)用依賴了哪些遠(yuǎn)程應(yīng)用,我們來看下其類型定義:

exporttypeRemotes=(RemotesItem|RemotesObject)[]|RemotesObject;

exporttypeRemotesItem=string;

exporttypeRemotesItems=RemotesItem[];

exportinterfaceRemotesObject{

[k:string]:RemotesConfig|RemotesItem|RemotesItems;

exportinterfaceRemotesConfig{

*共享模塊需要依賴的其它模塊

external:RemotesItem|RemotesItems;

//共享作用域的名稱,默認(rèn)為default

shareScope:string;

還是以app1為例,我們回顧其remote的配置:

remotes:{

app2:'app2@http://localhost:3002/remoteEntry.js',

告訴了Webpack如果需要消費(fèi)app2導(dǎo)出的模塊,那么則需要加載app2服務(wù)的remoteEntry.js文件,所以app1在初始化的時(shí)候就會(huì)加載此文件,然后通過下面的方式加載app2導(dǎo)出的模塊:

importRemoteButtonfrom'app2/Button';

是不是有點(diǎn)神奇,這里面的實(shí)現(xiàn)用了什么黑魔法,簡(jiǎn)單的幾個(gè)配置,然后啟動(dòng)服務(wù),就能消費(fèi)其它遠(yuǎn)程應(yīng)用的模塊。保持耐心,后續(xù)我們將慢慢揭開其神秘的面紗。

Shared

MF關(guān)于shared配置部分是我個(gè)人覺得最復(fù)雜的部分,當(dāng)然SharedPlugin的實(shí)現(xiàn)也是相對(duì)來說比較復(fù)雜,因?yàn)檫@里牽扯到一些需要shared配置延伸出的例如單例問題。先留個(gè)懸念,稍后解釋單例問題,我們還是先看shared配置類型定義:

exporttypeShared=(SharedItem|SharedObject)[]|SharedObject;

exporttypeSharedItem=string;

exportinterfaceSharedObject{

[k:string]:SharedConfig|SharedItem;

exportinterfaceSharedConfig{

//配置了eager是告訴webpack該模塊是作為一個(gè)initialchunk,無論怎么樣,初始化都需要加載該模塊

eager:boolean;

//共享模塊依賴的模塊

import:false|SharedItem;

//共享模塊的包名

packageName:string;

//共享模塊的版本

requiredVersion:false|string;

//如果配置了key,查找共享模塊的時(shí)候,會(huì)在當(dāng)前共享作用域查找配置的key

shareKey:string;

//共享作用域

shareScope:string;

//是否需要保持單例

singleton:boolean;

//是否需要嚴(yán)格校驗(yàn)共享模塊的版本,只有配置了requiredVersion配置該選型才有效

strictVersion:boolean;

//指定提供的模塊的版本,將會(huì)替代低版本的模塊,但是不會(huì)替代版本更好的模塊

version:false|string;

從SharedConfig類型我們就可以看到shared配置有很多的場(chǎng)景需要適配,每個(gè)配置我都做了簡(jiǎn)單注釋來介紹。當(dāng)然可能這個(gè)時(shí)候,不熟悉MF的小伙伴看到這些配置可能是懵逼的狀態(tài)。不用著急,這些配置項(xiàng),在后面更加具體的源碼使用場(chǎng)景,我會(huì)再進(jìn)行介紹,這里先留個(gè)印象。

我們還是看下app1的配置:

shared:{

'react':{

singleton:true,

requiredVersion:require('./package.json').dependencies.react

'react-dom':{

singleton:true,

requiredVersion:require('./package.json').dependencies['react-dom']

'lodash':{

requiredVersion:require('./package.json').dependencies['lodash'],

singleton:true,

這里分別將react、react-dom、lodash等三

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

最新文檔

評(píng)論

0/150

提交評(píng)論