微前端場景下的代碼共享

前言
在現(xiàn)有前端應(yīng)用日益復(fù)雜化的業(yè)務(wù)場景下,將一個(gè)體積龐大的前端應(yīng)用拆分為幾個(gè)可以獨(dú)立開發(fā)、測試、部署的微應(yīng)用變得越來越普遍。微前端的這種模式這大大提高了我們的構(gòu)建效率,在每次構(gòu)建時(shí)我們不再需要去構(gòu)建一個(gè)龐大的應(yīng)用,而是構(gòu)建我們所需要構(gòu)建的某個(gè)子應(yīng)用。通常在一個(gè)微前端的架構(gòu)下應(yīng)用之間又會(huì)有許多公共的代碼,那么在此基礎(chǔ)上又如何更加靈活更加有效的共用這些代碼呢?(下面介紹的各種方案與微前端的場景并無綁定關(guān)系,只是基于這個(gè)場景更好去說明一些問題)。

Common Solutions
NPM依賴
比較常見的做法是將公有模塊作為npm包發(fā)布,然后作為每個(gè)子應(yīng)用的依賴來引入,但實(shí)際上我們只做到了代碼層面上的共享與復(fù)用,在構(gòu)建時(shí)我們?nèi)匀粫?huì)重復(fù)打包,并沒有真正實(shí)現(xiàn)共享,而且以npm包的形式引入的話也存在一些版本問題,每次公共代碼的更新都得去所有依賴方應(yīng)用升級(jí)這個(gè)版本,發(fā)布成本較大。而且從性能上考慮,若子應(yīng)用A、B、C都為依賴方,那最終頁面上會(huì)加載三份公共npm包的代碼。


Monorepo
還有一個(gè)比較常見的方法就是將公共代碼也作為Monorepo下的一個(gè)子項(xiàng)目,之后將這個(gè)package作為其他應(yīng)用的dependency來import


{
  "name": "@package/a",
  "dependencies": {
     "@package/common": "0.0.1"
   }
}
雖然不像 npm 那樣存在版本更新的問題,但是他們也有一個(gè)同樣的問題就是只做到了代碼層面上的共享與復(fù)用,實(shí)際上在構(gòu)建時(shí)還是會(huì)重復(fù)打包,在頁面性能上也會(huì)造成重復(fù)加載問題。

Webpack DLLPlugin
DLL(Dynamic Link Library)文件為動(dòng)態(tài)鏈接庫文件,在Windows中,許多應(yīng)用程序并不是一個(gè)完整的可執(zhí)行文件,它們被分割成一些相對(duì)獨(dú)立的動(dòng)態(tài)鏈接庫,即DLL文件,放置于系統(tǒng)中。當(dāng)我們執(zhí)行某一個(gè)程序時(shí),相應(yīng)的DLL文件就會(huì)被調(diào)用。

形象一點(diǎn)說就是,我們把一部分公用模塊抽離預(yù)先打包成動(dòng)態(tài)鏈接庫,每次構(gòu)建時(shí)我們只需要構(gòu)建我們的業(yè)務(wù)代碼而不需要再去打包構(gòu)建動(dòng)態(tài)庫,除非公用模塊有更新,我們才需要去對(duì)其進(jìn)行打包構(gòu)建。

這里只做簡單介紹:


將需要共享的依賴打包,通過DllPlugin生成manifest.json文件描述對(duì)應(yīng)的dll文件里保存的模塊
module.exports = {
        resolve: {
                extensions: [".js", ".jsx"]
        },
        entry: {
                alpha: ["./a", "./b"]
        },
        output: {
                path: path.join(__dirname, "dist"),
                filename: "MyDll.[name].js",
                library: "[name]_[fullhash]"
        },
        plugins: [
                new webpack.DllPlugin({
                        path: path.join(__dirname, "dist", "[name]-manifest.json"),
                        name: "[name]_[fullhash]"
                })
        ]
};
需要使用DLL模塊則通過manifest.json文件把依賴名稱映射成DLL模塊上對(duì)應(yīng)的模塊id來require
{"name":"alpha_32ae439e7568b31a353c","content":{"./a.js":{"id":1,"buildMeta":{}},"./b.js":{"id":2,"buildMeta":{}}}}
使用DLL Reference模塊來讀取manifest文件實(shí)現(xiàn)依賴的映射
  // webpack.config.js
  new webpack.DllReferencePlugin({
    context: path.join(__dirname, "..", "dll"),
    manifest: require("../dll/dist/alpha-manifest.json")
  }),
在入口html文件中插入打包生成的dll文件
<body>
  <!--引用dll文件-->
  <script src="../dist/dll/MyDll.alpha.js" ></script>
</body>
可以看到這個(gè)配置成本還是蠻高的,實(shí)在不是很友好,但是確實(shí)解決了每次構(gòu)建重復(fù)打包的問題,但是和下文要介紹的external一樣也造成了一些問題,例如入口文件隨著dll文件的插入會(huì)越來越大導(dǎo)致性能的下降

Webpack Externals
再來看看Externals,它解決的問題與上面說到的DLLPlugin差不多,防止將某些依賴打包到bundle中,而是在運(yùn)行時(shí)再去加載這部分依賴,實(shí)現(xiàn)模塊的復(fù)用同時(shí)提高編譯速度

index.html
<script src="https://code.jquery.com/jquery-3.1.0.js"integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="crossorigin="anonymous"></script>
webpack.config.js
module.exports = {//...
  externals: {
    jquery: 'jQuery',},
};
剝離掉不需要改動(dòng)的模塊,下面代碼仍能正確運(yùn)行
import $ from 'jquery';

$('.my-element').animate(/* ... */);
可以看到Externals比之前介紹的DLLPlugin在使用上要簡單的多,同時(shí)這也是現(xiàn)在一些微前端框架依賴共享的方式(如Garfish[1])

總結(jié)
上文我們簡單聊了四種代碼共享的解決方案,看起來Externals是最能解決我們的問題的,也是目前一些微前端框架使用的依賴共享方案,既在打包構(gòu)建上解決了重復(fù)打包的問題,提高打包效率,減小了包體積,又在頁面性能上避免了公共代碼的重復(fù)加載,但是Externals并不是那么靈活,同時(shí)也有許多問題:

模塊可能獨(dú)立頁面
如果子應(yīng)用是會(huì)作為獨(dú)立頁面的話,通過external在主應(yīng)用加載依賴就不行了,而需要手動(dòng)引入

公共包不匹配
external是比較受限制的,對(duì)于那些非常通用不需要頻繁更新的依賴可以采用這種方式(如react,react-dom),其他如Redux或者M(jìn)obx等別的依賴不一定是每個(gè)子應(yīng)用都在使用的

性能差
主模塊越來越大,加載了許多該模塊不需要的代碼導(dǎo)致首屏很慢

所以我們是否可以更加動(dòng)態(tài)化得按需加載,而不是像external一樣在入口處加載所有公共代碼呢?那當(dāng)然是有的,就是下文將介紹的Webpack 5新特性Module Federation Plugin

Webpack 5 Module Federation[2]
什么是Module Federation?
官方文檔的解釋為可以讓一個(gè)應(yīng)用與其他應(yīng)用在運(yùn)行時(shí)互相提供或者消費(fèi)各自的模塊,即它可以讓一個(gè)JS應(yīng)用在進(jìn)程中動(dòng)態(tài)地去加載其他應(yīng)用的代碼。為了更加清晰一點(diǎn),我們可以將每個(gè)模塊定義為下面三種角色:

Host(消費(fèi)方): 消費(fèi)其他應(yīng)用內(nèi)容的消費(fèi)方
Remote(被消費(fèi)方):提供部分內(nèi)容給其他應(yīng)用消費(fèi)的一方
Bidirectional-hosts(既是消費(fèi)方也是被消費(fèi)方): 一個(gè)既作為host消費(fèi)其他應(yīng)用的內(nèi)容,又作為remote提供給其他應(yīng)用內(nèi)容的構(gòu)建
我們從一個(gè)官方的demo[3]開始,先簡單介紹下這個(gè)demo做的事情

complete-react-case
├─ component-app 組件層App,依賴lib-app,暴露一些組件,既是Remote也是Host
│  ├─ App.jsx
│  ├─ bootstrap.js
│  ├─ index.js
│  ├─ package.json
│  ├─ public
│  │  └─ index.html
│  ├─ src
│  │  ├─ Button.jsx
│  │  ├─ Dialog.jsx
│  │  ├─ Logo.jsx
│  │  ├─ MF.jpeg
│  │  ├─ ToolTip.jsx
│  │  └─ tool-tip.css
│  └─ webpack.config.js
├─ lib-app 底層App,暴露了一些基礎(chǔ)庫:react, react-dom,屬于一個(gè)remote
│  ├─ index.js
│  ├─ package.json
│  └─ webpack.config.js
├─ main-app 上層App,依賴了lib-app和component-app應(yīng)用,一個(gè)純粹的host
│  ├─ App.jsx
│  ├─ bootstrap.js
│  ├─ index.js
│  ├─ package.json
│  ├─ public
│  │  └─ index.html
│  └─ webpack.config.js
├─ package-lock.json
└─ package.json
我們?cè)賮砜聪耺ain_app的代碼,因?yàn)樗鳛閔ost消費(fèi)了其他兩個(gè)應(yīng)用的代碼

import React from 'lib-app/react';
import Button from 'component-app/Button';
import Dialog from 'component-app/Dialog';
import ToolTip from 'component-app/ToolTip';
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dialogVisible: false,
    };
    this.handleClick = this.handleClick.bind(this);
    this.handleSwitchVisible = this.handleSwitchVisible.bind(this);
  }
  handleClick(ev) {
    console.log(ev);
    this.setState({
      dialogVisible: true,
    });
  }
  handleSwitchVisible(visible) {
    this.setState({
      dialogVisible: visible,
    });
  }
  render() {
    return (
      <div>
        <h1>Open Dev Tool And Focus On Network,checkout resources details</h1>
        <p>
          react、react-dom js files hosted on <strong>lib-app</strong>
        </p>
        <p>
          components hosted on <strong>component-app</strong>
        </p>
        <h4>Buttons:</h4>
        <Button type="primary" />
        <Button type="warning" />
        <h4>Dialog:</h4>
        <button onClick={this.handleClick}>click me to open Dialog</button>
        <Dialog switchVisible={this.handleSwitchVisible} visible={this.state.dialogVisible} />
        <h4>hover me please!</h4>
        <ToolTip content="hover me please" message="Hello,world!" />
      </div>
    );
  }
}
我們可以看到main_app從lib-app里引入了依賴react,又從componet-app里引入了幾個(gè)組件,通過配置Module federation plugin實(shí)現(xiàn)了跨應(yīng)用的代碼復(fù)用,而且配置也非常簡單,下面會(huì)繼續(xù)介紹






import React from 'lib-app/react';
import Button from 'component-app/Button';
import Dialog from 'component-app/Dialog';
import ToolTip from 'component-app/ToolTip';
Module federation的配置
/**
 * lib-app/webpack.config.js
 */
 {
  plugins: [
    new ModuleFederationPlugin({
      name: 'lib-app',
      filename: 'remoteEntry.js',
      exposes: {
        './react': 'react',
        './react-dom': 'react-dom'
      }
    })
  ],
}

/**
 * component-app/webpack.config.js
 */
 {
  plugins: [
    new ModuleFederationPlugin({
      name: 'component-app',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button.jsx',
        './Dialog': './src/Dialog.jsx',
        './Logo': './src/Logo.jsx',
        './ToolTip': './src/ToolTip.jsx',
      }
      remotes: {
        'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js',
      },
    })
  ],
}
 
 /**
 * main-app/webpack.config.js
 */
 {
  plugins: [
    new ModuleFederationPlugin({
      name: 'main_app',
      remotes: {
        'lib-app': 'lib_app@http://localhost:3000/remoteEntry.js',
        'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
      },
    })
  ],
}
這個(gè)就是Module federation的配置,大概想表達(dá)的意思如下圖:


頁面表現(xiàn)
看到這里可能我們還沒啥感覺,不就是從一個(gè)應(yīng)用引另一個(gè)應(yīng)用的代碼嗎,但是接下來要介紹的頁面表現(xiàn)才是令人驚訝的地方,我們來看下main_app的頁面文件加載

可以看到文件的加載順序?yàn)椋?br>
main.js (主模塊)
remoteEntry.js(指向lib-app 113B)
remoteEntry.js(指向component-app 113B)
bootstrap_js.js(主模塊啟動(dòng)文件)
lib-app/react
lib-app/react-dom
component-app/Button
component-app/Dialog
component-app/Tooltip
這里我們就能看到幾個(gè)非常吸引人的點(diǎn)了

Host模塊
main_app的外部依賴不會(huì)被打包進(jìn)入主模塊的main.js,解決了隨著公共代碼使用的增多導(dǎo)致main.js越來越大的問題,其次我們可以看到lib-app暴露的兩個(gè)依賴(react, react-dom)及component-app暴露的組件(Button,Dialog及ToolTip)是分開加載的,作為不同的文件進(jìn)行了構(gòu)建(實(shí)際看build生成也是如此),在動(dòng)態(tài)加載下可以進(jìn)一步提高頁面性能

舉個(gè)例子,我們將上面main_app稍作改造,懶加載Dialog

const Dialog = React.lazy(() => import('component-app/Dialog'));

<button onClick={this.handleClick}>click me to open Dialog</button>
{this.state.dialogVisible && (
  <React.Suspense fallback={null}>
    <Dialog switchVisible={this.handleSwitchVisible} visible={this.state.dialogVisible} />
  </React.Suspense>
)}
看下頁面表現(xiàn):


Remote模塊
那么Remote模塊的性能又會(huì)如何呢?隨著公共代碼的增多會(huì)不會(huì)影響Remote模塊的頁面性能?

暴露出去的每個(gè)外部模塊都是單獨(dú)打包,而不是全部打在remote模塊的入口文件main.js里,如果沒有使用這些代碼的話并不會(huì)加載,因此被消費(fèi)方并不會(huì)因?yàn)楣泊a的增多而影響到頁面性能

源碼解析
加載main_app的入口文件main.js

首先我們來簡單看看作為消費(fèi)方的main_app入口文件main.js里webpack_modules的定義

var __webpack_modules__ = ({

/***/ "webpack/container/reference/component-app":
/*!*********************************************************************!*\
  !*** external "component_app@http://localhost:3001/remoteEntry.js" ***!
  *********************************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {

"use strict";
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
    if(typeof component_app !== "undefined") return resolve();
    __webpack_require__.l("http://localhost:3001/remoteEntry.js", (event) => {
        if(typeof component_app !== "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__);
    }, "component_app");
}).then(() => (component_app));

/***/ "webpack/container/reference/lib-app": //.... 與component-app類似
/***/ }),
入口文件定義的webpack_modules即為兩個(gè)被消費(fèi)應(yīng)用的remoteEntry文件,這個(gè)文件看起來就像DLL Plugin里的mainfest.json文件一樣鏈接起了這些應(yīng)用,讓我們隨著頁面的文件加載流程接著往下看

加載啟動(dòng)文件bootstrap.js

(() => {
/*!******************!*\
  !*** ./index.js ***!
  ******************/
__webpack_require__.e(/*! import() */ "bootstrap_js").then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap.js */ "./bootstrap.js"));
})();
加載完main.js文件后就要開始加載啟動(dòng)文件bootstrap.js了,從之前的加載過程我們看到remoteEntry文件是先于啟動(dòng)文件bootstrap文件的,看上面的代碼這一步應(yīng)該發(fā)生在__webpack_require__.e中,在remoteEntry文件執(zhí)行完后再執(zhí)行bootstrap.js,確保了依賴的前置






webpack_require.e

__webpack_require__.e = (chunkId) => {
  return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
     __webpack_require__.f[key](chunkId, promises "key");
     return promises;
  }, []));
};
這里實(shí)際調(diào)用了__webpack_require__.f上的函數(shù),生成一個(gè)promise數(shù)組,并resolve所有的promise

webpack_require.f

接下來看下__webpack_require__.f上的函數(shù)

webpack_require.f.remotes
/* webpack/runtime/remotes loading */

/******/        var chunkMapping = {
/******/            "bootstrap_js": [
/******/                "webpack/container/remote/lib-app/react",
/******/                "webpack/container/remote/component-app/Button",
/******/                "webpack/container/remote/component-app/Dialog",
/******/                "webpack/container/remote/component-app/ToolTip",
/******/                "webpack/container/remote/lib-app/react-dom"
/******/            ]
/******/        };
/******/        var idToExternalAndNameMapping = {
/******/            "webpack/container/remote/lib-app/react": [
/******/                "default",
/******/                "./react",
/******/                "webpack/container/reference/lib-app"
/******/            ],
/******/            "webpack/container/remote/component-app/Button": [
/******/                "default",
/******/                "./Button",
/******/                "webpack/container/reference/component-app"
/******/            ],
/******/            "webpack/container/remote/component-app/Dialog": [
/******/                "default",
/******/                "./Dialog",
/******/                "webpack/container/reference/component-app"
/******/            ],
/******/            "webpack/container/remote/component-app/ToolTip": [
/******/                "default",
/******/                "./ToolTip",
/******/                "webpack/container/reference/component-app"
/******/            ],
/******/            "webpack/container/remote/lib-app/react-dom": [
/******/                "default",
/******/                "./react-dom",
/******/                "webpack/container/reference/lib-app"
/******/            ]
/******/        };

/******/        __webpack_require__.f.remotes = (chunkId, promises) => {
/******/                    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) {
/******/                            onError(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);
/******/                });
/******/            }
/******/        }
這里的代碼量比較多,簡單來說就是通過用當(dāng)前模塊id通過Map找到所依賴remote模塊id再添加具體的依賴到webpack_modules中。

以Button為例,當(dāng)我們加載 src_bootstrap_js 這個(gè) chunk 時(shí),經(jīng)過 remotes,發(fā)現(xiàn)這個(gè) chunk 依賴了component-app/Button

var chunkMapping = {
    "bootstrap_js": [
        "webpack/container/remote/component-app/Button",
        //...
    ]
};
var idToExternalAndNameMapping = {
   "webpack/container/remote/component-app/Button": [
   "default",
   "./Button",
   "webpack/container/reference/component-app"
   ],
   //...
 };
 
 
 // 最終結(jié)果可以簡化為下面這段代碼
  __webpack_require__.m[id] = (module) => {
        module.exports = __webpack_require__( "webpack/container/reference/component-app").then(componet-app => component-app.get('./Button'))
     }
  }
可以看出我們對(duì)遠(yuǎn)程模塊componet-app上button的使用其實(shí)就是調(diào)用component-app.get('./Button'),這貌似就是使用了個(gè)全局變量的get方法?這些又是在哪里定義的呢,就是remoteEntry文件!

remoteEntry.js
看下component-app的remoteEntry.js文件,把整個(gè)代碼簡化一下

 // 定義全局變量
var component_app;
(() => { //....
(() => {
var exports = __webpack_exports__;
/*!***********************!*\
  !*** container entry ***!
  ***********************/
// 生成暴露出去的模塊Map
var moduleMap = {
    "./Button": () => {
        return Promise.all([__webpack_require__.e("webpack_container_remote_lib-app_react"), __webpack_require__.e("src_Button_jsx")]).then(() => (() => ((__webpack_require__(/*! ./src/Button.jsx */ "./src/Button.jsx")))));
    },
    "./Dialog": () => {
        return Promise.all([__webpack_require__.e("webpack_container_remote_lib-app_react"), __webpack_require__.e("src_Dialog_jsx")]).then(() => (() => ((__webpack_require__(/*! ./src/Dialog.jsx */ "./src/Dialog.jsx")))));
    },
    "./Logo": () => {
        return Promise.all([__webpack_require__.e("webpack_container_remote_lib-app_react"), __webpack_require__.e("src_Logo_jsx")]).then(() => (() => ((__webpack_require__(/*! ./src/Logo.jsx */ "./src/Logo.jsx")))));
    },
    "./ToolTip": () => {
        return Promise.all([__webpack_require__.e("webpack_container_remote_lib-app_react"), __webpack_require__.e("src_ToolTip_jsx")]).then(() => (() => ((__webpack_require__(/*! ./src/ToolTip.jsx */ "./src/ToolTip.jsx")))));
    }
};

// 定義Get方法
var get = (module, getScope) => {
    __webpack_require__.R = getScope;
    getScope = (
        __webpack_require__.o(moduleMap, module)
            ? moduleMap[module]( "module")
            : Promise.resolve().then(() => {
                throw new Error('Module "' + module + '" does not exist in container.');
            })
    );
    __webpack_require__.R = undefined;
    return getScope;
};

// 定義init方法
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);
};

// 在component_app上定義get, init方法
__webpack_require__.d(exports, {
    get: () => (get),
    init: () => (init)
});
})();

component_app = __webpack_exports__;
})();
總結(jié)來說這個(gè)文件定義了全局的 component-app變量,然后提供了一個(gè) get 函數(shù),通過get函數(shù)來加載moduleMap里具體的模塊,即通過全局變量鏈接了兩個(gè)應(yīng)用。

webpack_require.f.j
webpack_require.f上的另一個(gè)方法,也是實(shí)際加載模塊的方法,這里加載了bootstrap.js文件,這個(gè)函數(shù)沒啥好說的,就是webpackJsonp異步加載,但也正因?yàn)槿绱瞬疟WC了文件的加載順序

加載流程

加載應(yīng)用入口文件main.js
準(zhǔn)備加載啟動(dòng)文件bootstap.js
加載啟動(dòng)文件前發(fā)現(xiàn)有依賴的remotes模塊
動(dòng)態(tài)加載依賴的remotes模塊
加載執(zhí)行完所有的前置依賴后再加載bootstrap.js
所有依賴加載完成,再執(zhí)行then邏輯,即webpack_require( "./bootstrap.js")
在啟動(dòng)應(yīng)用時(shí),進(jìn)行了依賴的前置分析,通過生成的remoteEntry文件內(nèi)的全局變量來get依賴的內(nèi)容,最后再執(zhí)行業(yè)務(wù)代碼。

與微前端的結(jié)合
對(duì)比微前端框架現(xiàn)在普遍使用的Externals方式,顯然Module Federation是個(gè)更好的解決方案,

主應(yīng)用
通過主應(yīng)用可以導(dǎo)出一些公共庫及公共組件等,但并不影響主應(yīng)用頁面性能

子應(yīng)用
動(dòng)態(tài)加載主應(yīng)用暴露的公共庫及公共組件,減小了入口文件的大小,提高了頁面性能

總結(jié)
Module Federation Plugin通過提供鏈接文件remoteEntry.js的cdn地址即可將不同的應(yīng)用連接起來,不僅局限于微前端場景,即使不同的項(xiàng)目工程也可以讓我們輕松做到更細(xì)粒度的代碼共享。





作者:ELab.liuhexiang


歡迎關(guān)注微信公眾號(hào) :前端民工