Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >ModuleFederation原理分析及发散

ModuleFederation原理分析及发散

作者头像
写代码的阿宗
发布于 2021-08-13 06:37:45
发布于 2021-08-13 06:37:45
2.4K00
代码可运行
举报
文章被收录于专栏:一道题做一宿一道题做一宿
运行总次数:0
代码可运行

前言

前几天我们稍微尝试了一下Webpack提供的新能力Module Federation,它为我们代码共享跟团队协作提供了新的可能性。之前若是我们项目A跟项目B有一些共同的逻辑,那我们可能会选择把它抽成一个npm包,然后在两个项目间引入。但是这有个缺点是只要npm包更新,我们的项目就需要重新打包来引入公共逻辑的更新,哪怕项目里一行代码没改。

而通过ModuleFederation,我们指定exposesshared,就可以配置要导出的模块跟它依赖的一些库,就可以成功地把这个模块分享出去。通过配置remotes,就可以指定一些依赖的远程模块。我们的应用会在运行时去请求依赖的远程模块,不需要重新打包(前提是远程模块没有breaking change)。这个时候项目A就可以在它的项目里实现这部分逻辑然后把这部分逻辑分享出去,项目B再引入,两个项目各自独立部署运行同时又在公共逻辑这边保持相同的行为。

这带来的好处绝不只是减少体力劳动这么简单,今天我们就来进一步探讨一下其它方向的可能性。

开始吧~

先创建多个项目:

  1. app:主项目,依赖了从其他项目暴露出来的远程模块,运行在8000端口
  2. header:提供了一个Header组件,展示一些视图,运行在8001端口
  3. content:提供了一个Content组件,展示一些视图,运行在8002端口
  4. footer:提供了一个Footer组件,展示一些视图,运行在8003端口

我们先实现一些组件,先在我们的header项目里实现Header组件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Header = ({count,reset}) => {
  return (
        <header>
            <h1>计数器Header</h1>
            <span>{`当前数量是:${count}`}</span>
            <button onClick={reset}>重置</button>
        </header>
    )
}

它接受一个属性count来展示当前数量以及提供了一个按钮来重置数字。

然后把这个Header导出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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熟悉的形式,感兴趣的可以看看之前这篇文章,现在我们直接拿来复用。

content项目里的的Content组件内容大体类似:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Content = ({count,add}) => {
  return (
        <main>
            <span>计数器Content</span>
            <div>
                <span>{count}</span>
                <button onClick={add}></button>
            </div>
        </main>
    );
}

它接受一个属性count来展示数字以及提供了一个按钮来增加数字。

footer项目里的Footer组件展示固定的UI

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Footer = () => {
    return <span>计数器Footer</span>
}

别忘了也要在Webpack配置中分别把这两个组件导出,我们app项目才能正常使用它们,具体操作跟Header类似,这边就不再赘述。

然后我们就可以在app里引入并使用他们啦!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,
    }),
])

加载组件并渲染:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export const counter = atom({
    key: 'counter',
    default: 0,
})

然后把RecoilRoot作为我们App组件的根目录,之后把atoms.js导出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
parts.federateModule({
    name: 'app',
    filename: 'state.js',

    ...

    exposes: {
        './state': './src/atoms',
    },
}),

headercontent项目里引入这个模块,这样做是没问题的,这几个项目既然没有固有的主次关系,都可以独立运行的,我能分享给你自然你也能分享给我,任何能以js模块导出的东西都可以通过ModuleFederation分享,这是仅能分享UI代码的微前端框架做不到的。(但是它们可以通过支持ModuleFederation来解决,手动滑稽下?)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
remotes: {
    ...
    state: "app@http://127.0.0.1:8000/state.js" },
    }
    ...

然后调整一下我们的组件,通过hook来使用这个atom

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Content = () => {
    const [count, setCount] = useRecoilState(counter)
    return (
        <main>
            <span>计数器Content</span>
            <div>
                <span>{count}</span>
                <button onClick={() => setCount(count + 1)}></button>
            </div>
        </main>
    );
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,可以看到如下输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[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的加载过程,我们一起来看看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 /* 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就这段代码了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/***/ "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的定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 (() => {
        /******/
        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组件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的最后:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
var __webpack_exports__ = __webpack_require__("webpack/container/entry/footer");
/******/ 
footer = __webpack_exports__;

当回到appmain.js的时候,又hui执行这两个方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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组件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
window.footer.get('./Footer')

这会返回一个promise,当resolve的时候会给我们一个factory,我们来尝试调用它:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的地址:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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文件,我们需要把背后真正的模块设置到到全局:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const loadComponent = (scope, module) => {
    return () =>
        window[scope].get(module).then((factory) => {
            return factory()
        })
}

最后我们需要在这些前置工作都完成的时候,把指定的内容加载出来:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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组件,所以直接实现了加载组件的逻辑,实际上还有很多其他类型的模块也可以分享,严谨一些这边要分情况处理。

然后精彩的地方来了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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这种新事物怎么看呢,欢迎来跟我交流~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 写代码的阿宗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
详解 Module Federation 的实现原理
简单理解就是说 “一个应用可以由多个独立的构建组成,这些构建彼此独立没有依赖关系,他们可以独立开发、部署。这就是常被认为的微前端,但不局限于此”
五月君
2023/11/17
8840
详解 Module Federation 的实现原理
Webpack5 跨应用代码共享 - Module Federation
Webpack 5 的消息尽管已经出来了许久,但是正式版一直还未发布。Webpack 5 的 ChangeLog 中,除了常规的性能优化、编译提速之外,有一个比较让人期待的功能就是 Module Federation。Module Federation 可以强行翻译成「模块联邦」,但是听起来很是怪异,我在某个前端群也抛出了这个问题,没想到大家的回复也是五花八门。所以,本文就直接用 Module Federation 了,不进行翻译听起来好像更舒服一点。
前端老王
2020/09/23
2.9K0
Webpack5 跨应用代码共享 - Module Federation
Webpack模块联邦:微前端架构的新选择
Webpack模块联邦(Module Federation)是Webpack 5引入的一项革命性特性,它彻底改变了微前端架构的实现方式。模块联邦允许不同的Web应用程序(或微前端应用)在运行时动态共享代码,无需传统的打包或发布过程中的物理共享。这意味着每个微应用可以独立开发、构建和部署,同时还能轻松地共享组件、库甚至是业务逻辑。
天涯学馆
2024/06/07
6290
它改变了 JavaScript 的体系结构——Webpack 5 Module Federation
Webpack 5 Module Federation: A game-changer in JavaScript architecture
疯狂的技术宅
2020/05/11
2.1K0
它改变了 JavaScript 的体系结构——Webpack 5 Module Federation
Webpack 5 Module Federation: JavaScript 架构的变革者
Module Federation [ˌfedəˈreɪʃn] 使 JavaScript 应用得以在客户端或服务器上动态运行另一个 bundle 或者 build 的代码。
ConardLi
2020/04/15
1.9K0
Webpack 5 Module Federation: JavaScript 架构的变革者
【工程化】探索webpack5中的Module Federation
Module Federation 是 webpack5 中振奋人心的新特性,也是号称能改变 JavaScript 架构游戏规则的功能。接下来让我们慢慢揭开 Module Federation 的神秘面纱
GopalFeng
2022/08/01
2.1K0
【工程化】探索webpack5中的Module Federation
从微组件到代码共享
随着前端应用越来越复杂,越来越庞大。前有巨石应用像滚雪球一般不断的叠高,后有中后台应用随着历史长河不断地积累负债,或者急需得到改善。微前端的工程方案在前端er心中像一道曙光不断的被提起,被实践,多年至今终于有了比较好的指引。它在解决大型应用之间复杂的依赖关系,或是解决我们技术栈的迁移历史负担,都在一定程度上扮演了极其关键的桥梁。
用户6835371
2021/09/03
1.7K0
从微组件到代码共享
Vite 也可以模块联邦
之前写过一篇文章,《将 React 应用迁移至 Vite》介绍了 Vite 的优势,并且和 webpack 做对比,但 webpack5 有个很重要的功能,就是模块联邦,那么什么是模块联邦?Vite 中也可以实现吗? 我们一起来探究下。
狂奔滴小马
2022/09/16
5.8K4
Vite 也可以模块联邦
基于Webpack5实现微前端架构
最近这段时间微前端这个概念越来越被提及,它采用了微服务的相关理念,我们可以把一个应用拆分成多个可以互不依赖可以独立开发并单独部署的模块,然后在运行时把它们组合成一个完整的App。
写代码的阿宗
2021/08/13
9490
我所知道的webpack5那些不太一样的改变
在webpack 5之前,webpack是没有提供持久化缓存,我们开发的时候需要使用类似 cache-loader 来做缓存方面的处理。
winty
2022/11/07
7700
我所知道的webpack5那些不太一样的改变
Module Federation最佳实践
Module Federation[1]官方称为模块联邦,模块联邦是webpack5支持的一个最新特性,多个独立构建的应用,可以组成一个应用,这些独立的应用不存在依赖关系,可以独立部署,官方称为微前端。
Maic
2022/07/28
1.5K0
Module Federation最佳实践
Vue+Webpack打造todo应用 原
B.vue-loader需要安装15版本以下(参考官方文档 https://vue-loader.vuejs.org/migrating.html#a-plugin-is-now-required . Vue-loader在15.*之后的版本都是 vue-loader的使用都是需要伴生 VueLoaderPlugin的)
晓歌
2018/08/15
1.3K0
Vue+Webpack打造todo应用
                                                                            原
Webpack 写一个 markdown loader
前段时间在公司内部写了个 UI 组件库,需要有组件说明文档。我们的组件文档一般都是用 md 文件书写,然后渲染成页面展示。我们首先基于 vue-cli 脚手架生成前端项目配置,然后我们通过 webpack 配置 loader 的方式加载我们的扩展。
前端逗逗飞
2021/04/30
1.1K0
Webpack 写一个 markdown loader
CSS Modules入门教程
或者可以这么说,CSS Modules为我们解决了什么痛点。针对以往我写网页样式的经验,具体来说可以归纳为以下几点:
糊糊糊糊糊了
2018/09/28
2.6K0
CSS Modules入门教程
webpack5学习笔记
assetModuleFilename: 'images/contenthash.png'
代码哈士奇
2022/01/26
2.6K0
微前端架构实战
之前比较多的处理方式是npm包形式抽离和引用,比如多个应用项目之间,可能有某业务逻辑模块或者其他是可复用的,便抽离出来以npm包的形式进行管理和使用。但这样却带来了以下几个问题:
西岭老湿
2021/04/25
3.9K0
微前端架构实战
Webpack入门
Webpack是一个前端构建工具,本文将简要介绍它最常用的功能,并创建一个基于webpack的前端开发环境。
张子阳
2018/09/30
1.8K0
Webpack入门
Webpack 5 新特性尝鲜
Webpack 5 发布已经有一段时间了,很多小伙伴都在考虑要不要升级,有没有升级的必要,不知道升级后有哪些改变;
西岭老湿
2021/02/04
1.3K0
Webpack 5 新特性尝鲜
前端MVC学习总结(四)——NodeJS+MongoDB+AngularJS+Bootstrap书店示例
摘要总结:本文介绍了如何使用AngularJS04进行图书信息管理系统开发。主要包括了图书信息录入、查询、修改和删除等功能。同时,还提供了编辑图书和删除图书的接口。通过使用AngularJS04的指令和指令模块,可以方便地实现图书信息管理系统。
张果
2018/01/04
2.4K0
前端MVC学习总结(四)——NodeJS+MongoDB+AngularJS+Bootstrap书店示例
【文末赠书】Vue3 用法 + 原理 + 使用小 tip 总结
整体来看,变化不大,只是名字大部分需要 + on,功能上类似。使用上 Vue3 组合式 API 需要先引入;Vue2 选项 API 则可直接调用,如下所示。
用户3806669
2022/11/11
1K0
【文末赠书】Vue3 用法 + 原理 + 使用小 tip 总结
相关推荐
详解 Module Federation 的实现原理
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验