Webpack的ModuleFederation原理分析及發散

前言

前幾天咱們稍微嘗試了一下Webpack提供的新能力Module Federation,它爲咱們代碼共享跟團隊協做提供了新的可能性。以前如果咱們項目A跟項目B有一些共同的邏輯,那咱們可能會選擇把它抽成一個npm包,而後在兩個項目間引入。可是這有個缺點是隻要npm包更新,咱們的項目就須要從新打包來引入公共邏輯的更新,哪怕項目裏一行代碼沒改。javascript

而經過ModuleFederation,咱們指定exposesshared,就能夠配置要導出的模塊跟它依賴的一些庫,就能夠成功地把這個模塊分享出去。經過配置remotes,就能夠指定一些依賴的遠程模塊。咱們的應用會在運行時去請求依賴的遠程模塊,不須要從新打包(前提是遠程模塊沒有breaking change)。這個時候項目A就能夠在它的項目裏實現這部分邏輯而後把這部分邏輯分享出去,項目B再引入,兩個項目各自獨立部署運行同時又在公共邏輯這邊保持相同的行爲。html

這帶來的好處毫不只是減小體力勞動這麼簡單,今天咱們就來進一步探討一下其它方向的可能性。前端

開始吧~

先建立多個項目:java

  1. app:主項目,依賴了從其餘項目暴露出來的遠程模塊,運行在8000端口
  2. header:提供了一個Header組件,展現一些視圖,運行在8001端口
  3. content:提供了一個Content組件,展現一些視圖,運行在8002端口
  4. footer:提供了一個Footer組件,展現一些視圖,運行在8003端口

咱們先實現一些組件,先在咱們的header項目裏實現Header組件:node

const Header = ({count,reset}) => {
  return (
        <header>
            <h1>計數器Header</h1>
            <span>{`當前數量是:${count}`}</span>
            <button onClick={reset}>重置</button>
        </header>
    )
}
複製代碼

它接受一個屬性count來展現當前數量以及提供了一個按鈕來重置數字。react

而後把這個Header導出:webpack

const commonConfig = merge([
    parts.basis({mode}),
    parts.loadJavaScript(),
    parts.page({title: 'Header'}),
    parts.federateModule({
        name: 'header',
        filename: 'headerComp.js',
        remotes: {
            header: 'header@http://127.0.0.1:8001/headerComp.js',
        },
        shared: sharedDependencies,
        exposes: {'./Header': './src/Header'},
    }),
])
複製代碼

我用函數封裝的方式,將Webpack各個單一功能的配置對象管理起來(基礎配置、頁面配置、js配置、ModuleFederation配置等等),最後把各個不一樣功能的函數返回的配置對象mergeWebpack熟悉的形式,感興趣的能夠看看以前這篇文章,如今咱們直接拿來複用。ios

content項目裏的的Content組件內容大致相似:git

const Content = ({count,add}) => {
  return (
        <main>
            <span>計數器Content</span>
            <div>
                <span>{count}</span>
                <button onClick={add}>加</button>
            </div>
        </main>
    );
}
複製代碼

它接受一個屬性count來展現數字以及提供了一個按鈕來增長數字。github

footer項目裏的Footer組件展現固定的UI:

const Footer = () => {
    return <span>計數器Footer</span>
}
複製代碼

別忘了也要在Webpack配置中分別把這兩個組件導出,咱們app項目才能正常使用它們,具體操做跟Header相似,這邊就再也不贅述。

而後咱們就能夠在app裏引入並使用他們啦!

const commonConfig = merge([
    parts.basis({mode}),
    parts.loadJavaScript(),
    parts.page({title: 'App'}),
    parts.federateModule({
        name: 'app',
        remotes: {
            header: 'header@http://127.0.0.1:8001/headerComp.js',
            content: 'content@http://127.0.0.1:8002/contentComp.js',
            footer: 'footer@http://127.0.0.1:8003/footerComp.js',
        },
        shared: sharedDependencies,
    }),
])
複製代碼

加載組件並渲染:

const Header = lazy(() => import('header/Header'))
const Content = lazy(() => import('content/Content'))
const Footer = lazy(() => import('footer/Footer'))

const App = () => {
    const [count, setCount] = useState(0)
    return (
        <div>
            <Suspense
                fallback={<FallbackContent text={'正在加載Header'}/>}
            >
                <Header count={count} reset={() => setCount(0)}/>
            </Suspense>

            <Suspense
                fallback={<FallbackContent text={'正在加載Content'}/>}
            >
                <Content count={count} add={() => setCount(count + 1)}/>
            </Suspense>

            <Suspense
                fallback={<FallbackContent text={'正在加載Footer'}/>}
            >
                <Footer/>
            </Suspense>
                
        </div>
    )
}
複製代碼

如今咱們把各個項目都跑起來,

運行項目控制檯輸出

來看看效果:

頁面展現

能夠看到這些遠程導入的組件,只用起來跟本地項目裏的組件並無什麼區別,咱們能夠正常地傳遞數據給它們。

這邊細心的同窗可能已經注意到了,沒錯,headercontentfooter這幾個項目都是能夠獨立運行的,它們只是跟app共享了部分邏輯,不是要徹底做爲app的一部分。在這共享的邏輯以外,它們能夠有所做爲,自成一體。這種擴展性可讓多個團隊快速迭代,獨立測試,聽起來是否是有點像亞馬遜的那種micro site的開發方式?

那狀態管理呢?

好多同窗可能會疑惑了,這種很是規的開發方式,還涉及到「能夠各自獨立部署運行」,跟以前咱們開發單頁應用時有點不同,那咱們以前的那些狀態管理方案還管用嗎?

我和大家同樣疑惑😉,實踐出真知,咱們來嘗試引入recoil來作狀態管理看看。

添加recoil的依賴,而後在app下新建一個atoms.js

export const counter = atom({
    key: 'counter',
    default: 0,
})
複製代碼

而後把RecoilRoot做爲咱們App組件的根目錄,以後把atoms.js導出:

parts.federateModule({
    name: 'app',
    filename: 'state.js',
    
    ...
    
    exposes: {
        './state': './src/atoms',
    },
}),
複製代碼

headercontent項目裏引入這個模塊,這樣作是沒問題的,這幾個項目既然沒有固有的主次關係,均可以獨立運行的,我能分享給你天然你也能分享給我,任何能以js模塊導出的東西均可以經過ModuleFederation分享,這是僅能分享UI代碼的微前端框架作不到的。(可是它們能夠經過支持ModuleFederation來解決,手動滑稽下😆)

...
remotes: {
    ...
    state: "app@http://127.0.0.1:8000/state.js" },
    }
    ...
複製代碼

而後調整一下咱們的組件,經過hook來使用這個atom

const Content = () => {
    const [count, setCount] = useRecoilState(counter)
    return (
        <main>
            <span>計數器Content</span>
            <div>
                <span>{count}</span>
                <button onClick={() => setCount(count + 1)}>加</button>
            </div>
        </main>
    );
}
複製代碼
const Header = () => {
    const [count, setCount] = useRecoilState(counter)
    return (
        <header>
            <h1>計數器Header</h1>
            <span>{`當前數量是:${count}`}</span>
            <button onClick={() => setCount(0)}>重置</button>
        </header>
    )
}
複製代碼

useRecoilState幾乎能夠跟useState無縫切換,並且能夠避免沒必要要的重複渲染,這點很棒~

接着從新把這幾個項目跑起來,打開http://127.0.0.1:8000/,咱們能夠看到它表現得跟以前用屬性注入的方式實現的效果如出一轍,狀態管理在這種開發模式下仍是能夠正常發揮做用的。這方面又跟咱們的單頁應用很像了。這邊不是限制只有recoil才能夠,經我實測reduxmobx均可以正常使用。

Webpack爲咱們作了什麼?

你們確定都很好奇,Webpack到底是怎樣作到這一切的?咱們既能夠把每一個部分都當成一個獨立的應用來開發,相似於micro site,又能夠把它們組合成一個完整的應用,相似於spa。這也太黑科技了吧!!

咱們來仔細看看Webpack爲咱們作了什麼,直接打開咱們的footer項目,運行yarn start,能夠看到以下輸出:

[0] footer
[0]  | ⬡ webpack: assets by chunk 972 KiB (id hint: vendors)
[0]  |     asset vendors-node_modules_react-dom_index_js.js 909 KiB [emitted] (id hint: vendors)
[0]  |     asset vendors-node_modules_react_index_js.js 62.8 KiB [emitted] (id hint: vendors)
[0]  |   asset main.js 94.2 KiB [emitted] (name: main)
[0]  |   asset footerComp.js 61.1 KiB [emitted] (name: footer)
[0]  |   asset node_modules_object-assign_index_js-node_modules_prop-types_checkPropTypes_js.js 8.19 KiB [emitted]
[0]  |   asset src_index_js.js 2.14 KiB [emitted]
[0]  |   asset src_Footer_js.js 1.65 KiB [emitted]
[0]  |   asset index.html 229 bytes [emitted]

複製代碼

咱們能夠在dist目錄找到這些文件。

  • main.js 這裏是這個應用的入口代碼
  • index.html 這個生成的HTMl文件引入了上面main.js
  • src_Footer_js.js 這是咱們Footer組件編譯後產生的js文件
  • footerComp.js 默認給的名字是remoteEntry.js,咱們這邊爲了突出導出的是個Footer組件改爲了footerComp.js,這是一個特殊的清單js文件,同時也包含咱們經過ModuleFederationPluginexposes配置項導出去的模塊以及運行時環境,
  • venders-node_modules_*.js 這些都是一些共享的依賴,也就是咱們經過ModuleFederationPluginshared選項配置的依賴包

爲了搞清楚整個加載流程,咱們打開appmain.js,由於它做爲宿主加載了不少遠程模塊,其中有段代碼被註釋爲remotes的加載過程,咱們一塊兒來看看:

/* webpack/runtime/remotes loading */
    /******/
    (() => {
        var chunkMapping = {
            /******/            "webpack_container_remote_header_Header": [
                /******/                "webpack/container/remote/header/Header"
                /******/],
            /******/            "webpack_container_remote_content_Content": [
                /******/                "webpack/container/remote/content/Content"
                /******/],
            /******/            "webpack_container_remote_footer_Footer": [
                /******/                "webpack/container/remote/footer/Footer"
                /******/]
            /******/
        };
        /******/
        var idToExternalAndNameMapping = {
            /******/            "webpack/container/remote/header/Header": [
                /******/                "default",
                /******/                "./Header",
                /******/                "webpack/container/reference/header"
                /******/],
            /******/            "webpack/container/remote/content/Content": [
                /******/                "default",
                /******/                "./Content",
                /******/                "webpack/container/reference/content"
                /******/],
            /******/            "webpack/container/remote/footer/Footer": [
                /******/                "default",
                /******/                "./Footer",
                /******/                "webpack/container/reference/footer"
                /******/]
            /******/
        };
        /******/
        __webpack_require__.f.remotes = (chunkId, promises) => {
            /******/
            if (__webpack_require__.o(chunkMapping, chunkId)) {
                /******/
                chunkMapping[chunkId].forEach((id) => {
                    /******/
                    var getScope = __webpack_require__.R;
                    /******/
                    if (!getScope) getScope = [];
                    /******/
                    var data = idToExternalAndNameMapping[id];
                    /******/
                    if (getScope.indexOf(data) >= 0) return;
                    /******/
                    getScope.push(data);
                    /******/
                    if (data.p) return promises.push(data.p);
                    /******/
                    var onError = (error) => {
                        /******/
                        if (!error) error = new Error("Container missing");
                        /******/
                        if (typeof error.message === "string")
                            /******/                            error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
                        /******/
                        __webpack_modules__[id] = () => {
                            /******/
                            throw error;
                            /******/
                        }
                        /******/
                        data.p = 0;
                        /******/
                    };
                    /******/
                    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_modules__[id] = (module) => {
                            /******/
                            module.exports = factory();
                            /******/
                        }
                        /******/
                    };
                    console.log(data[2], data[0], data[1])
                    /******/
                    handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
                    /******/
                });
                /******/
            }
            /******/
        }
        /******/
    })();
複製代碼

這段代碼不是寫給人看的,讀起來真難受,不過咱們只要照着這些變量看一下最後執行的那個handleFunction函數就行了,好歹尋到了一些蛛絲馬跡。

第一次執行handleFunction傳入了data[2],那對於footer來講,就是傳入了webpack/container/reference/footer,那咱們去搜索一下這個字符串。

webpack/container/reference/footer爲key就這段代碼了:

/***/ "webpack/container/reference/footer":
      /*!*************************************************************!*\ !*** external "footer@http://127.0.0.1:8003/footerComp.js" ***! \*************************************************************/
 /***/ ((module, __unused_webpack_exports, __webpack_require__) => {

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

          /***/
  })
複製代碼

這邊去請求了footerComp.js了。咱們來看一下__webpack_require__.l的定義:

(() => {
        /******/
        var inProgress = {};
        /******/
        var dataWebpackPrefix = "app:";
        /******/ 		// loadScript function to load a script via script tag
        /******/
        __webpack_require__.l = (url, done, key, chunkId) => {
            /******/
            if (inProgress[url]) {
                inProgress[url].push(done);
                return;
            }
            /******/
            var script, needAttach;
            /******/
            if (key !== undefined) {
                /******/
                var scripts = document.getElementsByTagName("script");
                /******/
                for (var i = 0; i < scripts.length; i++) {
                    /******/
                    var s = scripts[i];
                    /******/
                    if (s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) {
                        script = s;
                        break;
                    }
                    /******/
                }
                /******/
            }
            /******/
            if (!script) {
                /******/
                needAttach = true;
                /******/
                script = document.createElement('script');
                /******/
                /******/
                script.charset = 'utf-8';
                /******/
                script.timeout = 120;
                /******/
                if (__webpack_require__.nc) {
                    /******/
                    script.setAttribute("nonce", __webpack_require__.nc);
                    /******/
                }
                /******/
                script.setAttribute("data-webpack", dataWebpackPrefix + key);
                /******/
                script.src = url;
                /******/
            }
            /******/
            inProgress[url] = [done];
            /******/
            var onScriptComplete = (prev, event) => {
                    /******/ 				// avoid mem leaks in IE.
                    /******/
                    script.onerror = script.onload = null;
                    /******/
                    clearTimeout(timeout);
                    /******/
                    var doneFns = inProgress[url];
                    /******/
                    delete inProgress[url];
                    /******/
                    script.parentNode && script.parentNode.removeChild(script);
                    /******/
                    doneFns && doneFns.forEach((fn) => (fn(event)));
                    /******/
                    if (prev) return prev(event);
                    /******/
                }
                /******/;
            /******/
            var timeout = setTimeout(onScriptComplete.bind(null, undefined, {type: 'timeout', target: script}), 120000);
            /******/
            script.onerror = onScriptComplete.bind(null, script.onerror);
            /******/
            script.onload = onScriptComplete.bind(null, script.onload);
            /******/
            needAttach && document.head.appendChild(script);
            /******/
        };
        /******/
    })();
複製代碼

它會建立一個script標籤而後監聽加載狀態,那咱們再去看footerComp.js

footerComp.js最開始定義了一個全局變量footer,而後它去請求一些被導出來的文件,即咱們的Footer組件:

var footer;

...

var __webpack_modules__ = ({

      /***/ "webpack/container/entry/footer":
      /*!***********************!*\ !*** container entry ***! \***********************/
 /***/ ((__unused_webpack_module, exports, __webpack_require__) => {

          eval("var moduleMap = {\n\t\"./Footer\": () => {\n\t\t" +
              "return Promise.all([__webpack_require__.e(\"webpack_sharing_consume_default_react_react-_1a68\"), " +
              "__webpack_require__.e(\"src_Footer_js\")]).then(() => " +
              "(() => ((__webpack_require__(/*! ./src/Footer */ \"./src/Footer.js\")))));\n\t}\n};\n" +
              "var get = (module, getScope) => {\n\t" +
              "__webpack_require__.R = getScope;\n\t" +
              "getScope = (\n\t\t" +
              "__webpack_require__.o(moduleMap, module)\n\t\t\t" +
              "? moduleMap[module]()\n\t\t\t: Promise.resolve().then(() => {\n\t\t\t\t" +
              "throw new Error('Module \"' + module + '\" does not exist in container.');\n\t\t\t})\n\t);\n\t" +
              "__webpack_require__.R = undefined;\n\treturn getScope;\n};\n" +
              "var init = (shareScope, initScope) => {\n\tif (!__webpack_require__.S) return;\n\t" +
              "var oldScope = __webpack_require__.S[\"default\"];\n\t" +
              "var name = \"default\"\n\tif(oldScope && oldScope !== shareScope) " +
              "throw new Error(\"Container initialization failed as it has already been initialized with a different share scope\");\n\t" +
              "__webpack_require__.S[name] = shareScope;\n\t" +
              "return __webpack_require__.I(name, initScope);\n};\n\n// This exports getters to disallow modifications\n" +
              "__webpack_require__.d(exports, {\n\tget: () => (get),\n\tinit: () => (init)\n});\n\n//# sourceURL=webpack://footer/container_entry?");

          /***/
  })

      /******/
  });

...
複製代碼

而後在footerComp.js的最後:

...
var __webpack_exports__ = __webpack_require__("webpack/container/entry/footer");
/******/ 
footer = __webpack_exports__;
複製代碼

當回到appmain.js的時候,又會執行這兩個方法:

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));
複製代碼

這下大體的邏輯就有了,當remoteEntry.js被瀏覽器加載後,它會用咱們在ModuleFederationPlugin裏面指定的name註冊一個全局變量。這個變量有一個get方法來返回remote模塊以及一個init函數,這個函數用來管理全部共享的依賴的。

就拿咱們上面的footer項目來講,當它的footerComp.js文件(注意沒有設置filename時叫remoteEntry.js),被瀏覽器加載後,會建立一個名爲footer(咱們經過name選項指定的)的全局變量,咱們能夠用控制檯來看看它的組成: window.footer

控制檯執行結果

經過這個get函數,咱們能夠拿到暴露出來的Footer組件:

window.footer.get('./Footer')
複製代碼

這會返回一個promise,當resolve的時候會給咱們一個factory,咱們來嘗試調用它:

window.footer.get('./Footer').then(factory=>console.log(factory()))
複製代碼

咱們把這個模塊打印到控制檯上了。

獲取組件後的結果

這邊咱們的Footer是默認導出,因此咱們看到這個返回的Module對象有個key名爲default,若是這個模塊包含其餘的命名導出,也會被添加到這個對象中。

須要注意的是,咱們調用這個factory會去加載這個遠程模塊須要的共享依賴,Webpack在這方面作得還比較智能,像咱們headercontent模塊都依賴了recoil,那這兩個遠程模塊誰先被加載誰就去加載recoil,若是這個recoil版本知足剩下的那個的要求,剩下的那個遠程模塊就會直接使用這個已經加載好的recoil。並且循環引入跟嵌套的remotes都是支持的,好比咱們這裏,app暴露了stateheader引入了stateheader暴露了Headerapp引入了HeaderWebpack會正確處理這一流程。

騷操做一下?

那咱們這個時候就恍然大悟了,原來,這邊就跟react hook同樣,經過全局變量來實現它的功能。一個能夠隨處訪問的全局變量,咱們只須要保證它先被加載進來就行了。

既然知道Webpack是怎麼實現遠程模塊的加載的了,邏輯都很常規,那其實咱們就能夠手動模擬這一過程,沒必要把咱們須要的遠程模塊都寫在Webpack配置裏。

首先是請求遠程模塊,把它添加在全局做用域內,咱們先寫一個hook來處理從url加載模塊,這邊須要的是咱們清單文件也就是remoteEntry.js的地址:

const useScript = (args) => {
    const [ready, setReady] = useState(false)
    const [failed, setFailed] = useState(false)

    useEffect(() => {
        if (!args.url) {
            return
  }

        const element = document.createElement('script')

        element.src = args.url
  element.type = 'text/javascript'
  element.async = true    setReady(false)
        setFailed(false)

        element.onload = () => {
            console.log(`遠程依賴已加載: ${args.url}`)
            setReady(true)
        }

        element.onerror = () => {
            console.error(`遠程依賴加載失敗: ${args.url}`)
            setReady(false)
            setFailed(true)
        }

        document.head.appendChild(element)

        return () => {
            console.log(`移除遠程依賴: ${args.url}`)
            document.head.removeChild(element)
        }
    }, [args.url])

    return {
        ready,
        failed,
    }
}
複製代碼

這個是咱們這個方案的靈魂,咱們動態地添加一個script標籤,而後監聽加載的過程,經過useState的變量把導入遠程依賴的狀態動態地傳遞出去。

而後光把這樣還不行,畢竟咱們才引入了清單js文件,咱們須要把背後真正的模塊設置到到全局:

const loadComponent = (scope, module) => {
    return () =>
        window[scope].get(module).then((factory) => {
            return factory()
        })
}
複製代碼

最後咱們須要在這些前置工做都完成的時候,把指定的內容加載出來:

const LoaderContainer = ({ url, scope, module }) => {
    const { ready, failed } = useScript({
        url: url,
    })

    if (!url) {
        return <h2>沒有指定遠程依賴</h2>
    }

    if (!ready) {
        return <h2>正在加載遠程依賴: {url}</h2>
    }

    if (failed) {
        return <h2>加載遠程依賴失敗: {url}</h2>
    }

    const Component = lazy(loadComponent(scope, module))

    return (
        <Suspense fallback={<FallbackContent text={'加載遠程依賴'} />}>
            <Component />
        </Suspense>
    )
}
複製代碼

這邊由於咱們知道遠程那邊導出的是一個React組件,因此直接實現了加載組件的邏輯,實際上還有不少其餘類型的模塊也能夠分享,嚴謹一些這邊要分狀況處理。

而後精彩的地方來了:

<LoaderContainer
  module={'./Footer'}
    scope={'footer'}
    url={'http://127.0.0.1:8003/footerComp.js'}
/>
複製代碼

注意,因爲咱們LoaderContainer裏面作了一些錯誤處理,在遠程依賴被加載成功前會return別的UI元素,咱們想要導入的遠程模塊的組件就不能使用hook了,不然會由於違反hook的規則報錯。

如今咱們從新運行一下項目,應該不會發現有什麼變化。咱們這邊的例子雖然簡單,看起來作了不必作的事,可是這爲咱們提供了新世界的大門,由於咱們不須要把咱們項目依賴的遠程模塊寫死在Webpack配置裏了,也就是說,只要咱們腦洞夠大,模塊配置能夠以任何形式出現,咱們甚至能夠對用戶作到「千人千面」,在運行時動態地拼裝新的頁面,而不須要藉助各類flag,是否是頗有意思呢?

總結

這麼一通操做下來,我以爲ModuleFederation的可玩性仍是很高的,咱們能夠看到它並不僅是讓咱們少維護了幾個代碼倉庫、少打了幾回包這麼簡單,在各個體驗上也一樣出色。它既能給咱們提供相似micro site同樣的開發體驗,又能帶來spa提供的測試與使用體驗,這是二者單獨都很難作到的。將來可期,後面社區愈來愈多人擁抱它以後,必定還會開發出其它更有意思的使用方法。就目前來看,把基礎依賴徹底經過運行時動態請求可能不是很好的選擇,好比基礎組件庫,在這種場景下咱們能夠同時構建npm包跟遠程模塊,而後優先使用遠程模塊,在遠程模塊沒法使用時再轉而使用應用打包時依賴的npm包做爲備用方案(至於新的代碼邏輯咱們能夠下次打包時再更新到它的最新npm版本),這樣雖然可能沒用上最新的代碼,不過至少能夠保證項目穩定運行。另一些通用的代碼,想要分享給更多人而不只僅是內部業務使用的代碼,好比React啊,axios啊,這種框架跟工具包等等,npm包仍是最好的選擇。

你們對ModuleFederation這種新事物怎麼看呢,歡迎來跟我交流~

完整代碼地址

相關文章
相關標籤/搜索