Skip to content

chunk 和 module

module:每一个源码 js 文件其实都可以看成一个 module

chunk:每一个打包落地的 js 文件其实都是一个 chunk,每个 chunk 都包含很多 module

默认的 chunk 数量实际上是由你的入口文件的 js 数量决定的,但是如果你配置动态加载或者提取公共包的话,也会生成新的 chunk。

打包代码解读

总结:模块引入会被打包成webpack_require函数引入模块并返回该模块,而webpack_require的实现,即每次require的时候,先去缓存的 installedModules 这个缓存 map 里面看是否加载过了,如果没有加载过,那就从 modules 这个所有模块的 map 里去加载。 而整个bundle模块,就是个立即执行函数,modules就是函数的入参,具体值就是打包后的的所有 module。这种是一个chunk就是一个js文件,维护着自己局部的module,自己使用没有问题,对于动态引入的会生成一个新的 js 文件。

首先,动态 import 的代码会变成就是外面套了一层 webpck_require.e,然后这是一个 promise,在 then 里面再去执行 webpack_require的代码。实际上 webpck_require.e 就是去加载 chunk 的 js 文件,具体就是动态创建script 标签,脚本在完成时,resolve这个promise。

另一个重点是加载的chunk代码也做了相应的改装,代码是向一个全局数组里面 push 了自己的模块 id 以及对应的 modules。而在主文件bundle里,劫持了这个数组的push方法,作用是把模块加到自己的modules中。

demo

javascript
src
---main.js
---moduleA.js
---moduleB.js
 
/**
* moduleA.js
*/
export default function testA() {
    console.log('this is A');
}
 
/**
* main.js
*/
import testA from './moduleA';
 
testA();
 
import('./moduleB').then(module => {
 
});

入口 js 是 main.js,里面就是直接引入 moduleA.js,然后动态引入 moduleB.js,那么最终生成的文件就是两个 chunk,分别是:

  1. main.js 和 moduleA.js 组成的 bundle.js
  2. moduleB.js 组成的 0.bundle.js 在webpack底层是用 mainTemplatechunkTemplate 分别渲染出来的

import 变成了什么样?

整个 main.js 的代码打包后是下面这样的

javascript
(function (module, __webpack_exports__, __webpack_require__) {
 
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */
    var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*!        ./moduleA */ "./src/moduleA.js");
 
    Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__["default"])();
 
    __webpack_require__.e( /*! import() */ 0).then(__webpack_require__.bind(null, /*! ./moduleB             */ "./src/moduleB.js")).then(module => {
 
    });
 
})

可以看到,直接 import moduleA 最后会变成 webpack_require,而这个函数是 webpack 打包后的一个核心函数,就是解决依赖引入的。

webpack_require的实现

javascript
function __webpack_require__(moduleId) {
    // Check if module is in cache
    // 先检查模块是否已经加载过了,如果加载过了直接返回
    if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // 如果一个import的模块是第一次加载,那之前必然没有加载过,就会去执行加载过程
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };
    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
}

化一下它的实现,其实很简单,就是每次 require,先去缓存的 installedModules 这个缓存 map 里面看是否加载过了,如果没有加载过,那就从 modules 这个所有模块的 map 里去加载。

modules 从哪里来的

把 bundle.js 生成的 js 再简化一下:

javascript
(function (modules) {})({
    "./src/main.js": (function (module, __webpack_exports__, __webpack_require__) {}),
    "./src/moduleA.js": (function (module, __webpack_exports__, __webpack_require__) {})
});

所以可以看到,这其实是个立即执行函数,modules 就是函数的入参,具体值就是包含的所有 module,到此,一个 chunk 是如何加载的,以及 chunk 如何包含 module就很清楚了。

动态引入

上面的 chunk 就是一个 js 文件,所以维护了自己的局部 modules,然后自己使用没啥问题,但是动态引入我们知道是会生成一个新的 js 文件的,那这个新的 js 文件 0.bundle.js 里面是不是也有自己的 modules 呢?那 bundle.js 如何知道 0.bundle.js 里面的 modules 呢?

动态 import 的代码

javascript
__webpack_require__.e( /*! import() */ 0)
  .then(__webpack_require__.bind(null, /*! ./moduleB  */ "./src/moduleB.js"))
  .then(module => {
 
});

从代码看,实际上就是外面套了一层 webpck_require.e,然后这是一个 promise,在 then 里面再去执行 webpack_require。 实际上 webpck_require.e 就是去加载 chunk 的 js 文件 0.bundle.js。

等到加载回来后它认为bundle.js 里面的 modules 就一定会有了 0.bundle.js 包含的那些 modules,这是如何做到的呢?

javascript
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(
    [
        [0],
        {
            "./src/moduleB.js": (function (module, __webpack_exports__, __webpack_require__) {})
        }
    ]
);

只是向一个全局数组里面 push 了自己的模块 id 以及对应的 modules。那看起来魔法的核心应该是在 bundle.js 里面了,事实的确也是如此。

javascript
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

劫持了 push 函数

Module federation

Module federation allows a JavaScript application to dynamically run code from another bundle/build, on both client and server

允许运行时动态决定代码的引入和加载。

依赖前置,先去分析,然后生成配置文件,再去加载。

通过一个全局变量去搭建桥梁。

demo

app1
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
 
app2
---index.js 入口文件
---bootstrap.js 启动文件
---App.js react组件
---Button.js react组件

这是文件结构,两个独立应用 app1 和 app2

javascript
/** app1 **/
/**
* index.js
**/
import('./bootstrap');
 
/**
* bootstrap.js
**/
import('./bootstrap');
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
 
ReactDOM.render(<App />, document.getElementById("root"));
 
/**
* App.js
**/
import('./bootstrap');
import React from "react";
 
import RemoteButton from 'app2/Button';
 
const App = () => (
  <div>
    <h1>Basic Host-Remote</h1>
    <h2>App 1</h2>
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);
 
export default App;

app2的代码不需要关注,但在app中,import RemoteButton from 'app2/Button';,引入了app2的代码。即跨应用复用代码

Module federation 的配置

javascript
/**
 * app1/webpack.config.ts
 */
{
    plugins: [
        new ModuleFederationPlugin({
            name: "app1",
            library: {
                type: "var",
                name: "app1"
            },
            remotes: {
                app2: "app2"
            },
            shared: ["react", "react-dom"]
        })
    ]
}

Module federation 的配置主要:

  1. 用了远程模块 app2,它叫 app2
  2. 用了共享模块,它叫 shared remotes 和 shared 还是有一点区别的。

生成的 html 文件:

html
<html>
  <head>
    <script src="app2/remoteEntry.js"></script>
  </head>
  <body>
    <div id="root"></div>
  <script src="app1/app1.js"></script><script src="app1/main.js"></script></body>
</html>

app1 打包生成的文件:

app1/index.html
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app1/src_bootstrap.js

最终页面表现以及加载的 js:

从上往下加载的 js 时序其实是很有讲究的,后面将会是解密的关键:

app2/remoteEntry.js
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app2/src_button_js.js
app1/src_bootstrap.js

这里最需要关注的其实还是每个文件从哪里加载,在不去分析原理之前,看文件加载我们至少有这些结论:

  1. remotes 的代码自己不打包,类似 external,例如 app2/button 就是加载 app2 打包的代码
  2. shared 的代码自己是有打包的

Module federation 的原理

webpack 的文件模块核心,即使升级 5,也没有发生变化

app1 和 app2 还是有自己的 modules,所以实现的关键就是两个 modules 如何同步,或者说如何注入

import 变成了什么

javascript
// import源码
import RemoteButton from 'app2/Button';
 
// import打包代码 在app1/src_bootstrap.js里面
/* harmony import */
var app2_Button__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__( /*! app2/Button */ "?ad8d");
/* harmony import */
var app2_Button__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/ __webpack_require__.n(app2_Button__WEBPACK_IMPORTED_MODULE_1__);

这段并没有什么特别。

注意看加载的 js 顺序:

javascript
app2/remoteEntry.js
app1/app1.js
app1/main.js
app1/react.js
app1/react-dom.js
app2/src_button_js.js // app2的button竟然先加载了,比我们的自己启动文件还前面
app1/src_bootstrap.js

main.js 文件内容

javascript
(() => { // webpackBootstrap
    var __webpack_modules__ = ({})
 
    var __webpack_module_cache__ = {};
 
    function __webpack_require__(moduleId) {
 
        if (__webpack_module_cache__[moduleId]) {
            return __webpack_module_cache__[moduleId].exports;
        }
        var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        };
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        return module.exports;
    }
    __webpack_require__.m = __webpack_modules__;
 
    __webpack_require__("./src/index.js");
})()

webpack_modules 内部的实现:

javascript
var __webpack_modules__ = ({
 
    "./src/index.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
        __webpack_require__.e( /*! import() */ "src_bootstrap_js").then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap */ "./src/bootstrap.js"));
    }),
 
    "container-reference/app2": ((module) => {
        "use strict";
        module.exports = app2;
    }),
 
    "?8bfd": ((module, __unused_webpack_exports, __webpack_require__) => {
        "use strict";
        var external = __webpack_require__("container-reference/app2");
        module.exports = external;
    })
});

In case I don't see you. Good afternoon, good evening, and good night.