从bundle中分析webpack5 Module Federation

1,166 阅读5分钟

背景

部门启动了新的项目,技术选型使用到了webpack5 Module Federation,因此我对该新特性做了一番研究,本文从bundle中来分析webpack是如何实现remote component调用以及package的共享。 带着这两个问题开启咱们探索之旅吧

案例配置

展示配置之前先解释一下host和remote的关系,从微前端角度来解释,host就是基座。remote就是子应用

Remote

new ModuleFederationPlugin({
  // 子应用名称
  name: 'home',
  // 获取子应用入口
  filename: 'remoteEntry.js',
  exposes: {
    // 将这两个组件共享出去
    './Content': './src/components/Content',
    './Button': './src/components/Button',
  },
  shared: {
    // 与基座共享一个package
    vue: {
      singleton: true,
    },
  },
});

Host

new ModuleFederationPlugin({
  // 基座名称
  name: 'layout',
  remotes: {
    // 连接子应用
    home: 'home@http://localhost:3002/remoteEntry.js',
  },
  shared: {
    // 与子应用共享一个package
    vue: {
      singleton: true,
    },
  },
});

Host的./src/main.js

import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';

const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));

const app = createApp(Layout);

app.component('content-element', Content);
app.component('button-element', Button);

app.mount('#app');

bundle分析

看构建后的产物,需要做一些公共方法和变量的解释,这边会更利于你的阅读

  1. webpack_modules, 存储的是module,例如webpack配置的entry
  2. webpack_require,从__webpack_modules__中加载模块
  3. webpack_require.e,动态加载模块,比如,import('./Button')
  4. webpack_require.o,判断对象中是否有该元素,用的是hasOwnProperty该方法
  5. webpack_require.d,在对象上定义getter方法,用的是 defineProperty
  6. webpack_require.S,存储的共享package get方法
  7. webpack_require.l 加载共享package 并将 package 注入到远程组件内

remote的共享组件的入口 remoteEntry.js

// ....
// module入口
var __webpack_modules__ = {
  'webpack/container/entry/home': (__unused_webpack_module, exports, __webpack_require__) => {
    // remote 向外提供的组件
    var moduleMap = {
      './Content': () => {
        return Promise.all([
          __webpack_require__.e('webpack_sharing_consume_default_vue_vue'),
          __webpack_require__.e('src_components_Content_vue-_b1070'),
        ]).then(() => () => __webpack_require__('./src/components/Content.vue'));
      },
      './Button': () => {
        return Promise.all([
          __webpack_require__.e('webpack_sharing_consume_default_vue_vue'),
          __webpack_require__.e('src_components_Button_js-_e56a0'),
        ]).then(() => () => __webpack_require__('./src/components/Button.js'));
      },
    };
    // host 通过该方法获取 ‘./Content’ ‘./Button’ 组件
    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;
    };
    // host 将共享的package注入到remote,便于 ‘./Content’ ‘./Button’获取 package
    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);
    };

    // 给 webpack/container/entry/home 模块添加 getter,get 和 init 方法
    // getter 是 __webpack_require__.d 实现的
    __webpack_require__.d(exports, {
      get: () => get,
      init: () => init,
    });
  },
};
// ...
// 加载 上面的 webpack/container/entry/home module
var __webpack_exports__ = __webpack_require__('webpack/container/entry/home');
// 将 webpack/container/entry/home module导出的两个方法 get 和 init 挂载到 home 上
home = __webpack_exports__;

上面是remote的入口文件 remoteEntry.js,它主要干了这些事

  1. window 挂在 home 变量,home是 remote和host沟通的桥梁
  2. 加载 webpack/container/entry/home,将 remote的Content、Button 组件get和共享package注入方法暴露出去

先简单介绍上面两个,其实已经将咱们的问题解释了三分之一了(如何实现remote component调用以及package的共享)。

咱们接着看看,Host干了什么

Host 入口 main

Host这边做的比较多,咱们从入口文件一段段代码分析

// host 模块入口
var __webpack_modules__ = {
  // 入口module 入口
  './src/index.js': (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
    __webpack_require__
      .e(/* import() */ 'src_main_js')
      .then(__webpack_require__.bind(__webpack_require__, './src/main.js'));
  },
  // remote module 的入口 其实就是 remoteEntry
  'webpack/container/reference/home': (module, __unused_webpack_exports, __webpack_require__) => {
    var __webpack_error__ = new Error();
    module.exports = new Promise((resolve, reject) => {
      // 防止重复加载
      if (typeof home !== 'undefined') return resolve();
      // 使用jsonp加载 remoteEntry.js 并执行 remoteEntry.js
      __webpack_require__.l(
        'http://localhost:3002/remoteEntry.js',
        event => {
          if (typeof home !== 'undefined') return resolve();
          // remote加载或者执行失败回调异常抛出
          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__);
        },
        'home',
      );
      // 返回 remote export === home
    }).then(() => home);
  },
};

// ....
var __webpack_exports__ = __webpack_require__('./src/index.js');

上面代码块加载入口模块 ./src/index.js ,加载这个模块主要干了这些事

  1. 执行__webpack_require__.e(/* import() */ 'src_main_js')
    a. 主要去加载共享package vue、remote remoteEntry.js和src_main_js ,并将 vue 的get方式注入到 remote 中,并将 Button和Content 组件的get方法暴露到 host中,具体看代码实现 i.加载共享package vue
register('vue', '3.2.41', () =>
  __webpack_require__
    // 远程加载vue组件,并将 vue内的模块通过 ./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js 挂在到 _webpack_module_ 中
    .e('vendors-node_modules_vue_runtime-dom_dist_runtime-dom_esm-bundler_js')
    .then(
      () => () =>
        // 从 _webpack_module_ 中获取 vue
        __webpack_require__('./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js'),
    ),
);

ii. 加载 remoteEntry.js & 将 vue 的get方式注入到 remote 中

initExternal('webpack/container/reference/home');

var initExternal = id => {
  // id: webpack/container/reference/home
  // ...
  try {
    // __webpack_require__(id) 直接从 __webpack_modules__ 中获取,可以看看 上 __webpack_modules__['webpack/container/reference/home']加载
    var module = __webpack_require__(id);
    if (!module) return;
    //
    var initFn = (
      module, // module: remoteEntry.js 抛出来的全局home
    ) =>
      // module.init === home.init, home.init 将 共享package的vue注入到remoteEntry中
      module && module.init && module.init(__webpack_require__.S[name], initScope);
    // remoteEntry加载完成之后,执行注入
    if (module.then) return promises.push(module.then(initFn, handleError));
    var initResult = initFn(module);
    if (initResult && initResult.then) return promises.push(initResult['catch'](handleError));
  } catch (err) {
    handleError(err);
  }
};  

iii. 加载 host的 ./src/main.js

// jsonp形式加载 ./src/main.js
__webpack_require__.f.j = (chunkId, promises) => {
  var url = __webpack_require__.p + __webpack_require__.u(chunkId);
  __webpack_require__.l(url, loadingEnded, 'chunk-' + chunkId, chunkId);
};

// 加载完成之后,将 ./src/main.js 放到 __webpack_module_中
// webpackChunk_vue3_demo_layout.push 实现了将 ./src/main.js 放到 __webpack_module_中
(self['webpackChunk_vue3_demo_layout'] = self['webpackChunk_vue3_demo_layout'] || []).push([
  ['src_main_js'],
  {
    './src/main.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {},
  },
]);

iv. Button和Content 组件的get方法暴露到 host中
其实这个方式分析 remote remoteEntry.js 已经提到了,就是home.get 这个方法,如何去获取的,咱们继续往下面分析

  1. 执行完__webpack_require__.e(/* import() */ 'src_main_js')之后,继续执行后面的 .then(__webpack_require__.bind(__webpack_require__, './src/main.js'))
    a. 这其实就是去加载 ./src/main.js 模块并执行,也就是去执行vue那部分代码了,里面就涉及到了 加载 remote的Button 和Content

  2. 加载 remote 的 Button

const Button = (0, runtime_dom_esm_bundler_js_.defineAsyncComponent)(() =>
  // 下载远程的组件 Button,这里实际执行的是 __webpack_require__.f.remotes 方法
  __webpack_require__.e(/* import() */ 'webpack_container_remote_home_Button').then(
    // 加载 Button 组件
    __webpack_require__.t.bind(__webpack_require__, 'webpack/container/remote/home/Button', 23),
  ),
);

const app = (0, runtime_dom_esm_bundler_js_.createApp)(Layout);

app.component('content-element', Content);

a.看看 webpack_require.f.remotes 如何下载 remote Button
i. 先通过 webpack/container/reference/home module查找到 remoteEntry.js export,也就是上面说的 home,包含get和init方法
ii.然后通过 home.get('./Button') 获取 对应的remote Button,并将它缓存下下来

/* webpack/runtime/remotes loading */
(() => {
  var chunkMapping = {
    webpack_container_remote_home_Button: ['webpack/container/remote/home/Button'],
  };
  var idToExternalAndNameMapping = {
    'webpack/container/remote/home/Button': [
      'default',
      './Button',
      'webpack/container/reference/home',
    ],
  };
  // chunkId: webpack_container_remote_home_Button
  __webpack_require__.f.remotes = (chunkId, promises) => {
    if (__webpack_require__.o(chunkMapping, chunkId)) {
      chunkMapping[chunkId].forEach(id => {
        // id: webpack/container/remote/home/Button
        var getScope = __webpack_require__.R;
        if (!getScope) getScope = [];
        // data: ['default', './Button', 'webpack/container/reference/home'];
        var data = idToExternalAndNameMapping[id];
        if (getScope.indexOf(data) >= 0) return;
        getScope.push(data);
        if (data.p) return promises.push(data.p);
        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) {}
        };
        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);
      });
    }
  };
})();

总结

用一张图来解释一下,package如何共享、remote component 如何加载的 image.png