我理解的webpack打包原理

434 阅读4分钟

在最近学习webpack的过程中,讲解了个关于webpack打包整合代码的过程,觉得很有意思,然后自己想了一下,分享出来。 比方说,有如下代码:

// src/index.js
console.log("index module");
const a = require("./a.js");
const c = require("./c.js");
console.log("index import a, and a is " + a);
console.log("index import c, and c is " + c);
// src/c.js
console.log("module c");
const a = require("./a.js");
console.log("c import a, and a is " + a);
module.exports = "c";
// src/a.js
console.log("module a");
module.exports = "a";

webpack打包的时候由于浏览器不认识require或import,所以他会将requre的内容和依赖方进行拼接,具体怎么个拼接规则呢?

  1. 列出入口文件的依赖和其依赖模块的依赖(递归执行)
  2. 将被依赖的文件和入口文件进行整合

比如上面的例子的依赖关系如下

index.js -- [a.js, c.js]
c.js ------ [a.js]

然后webpack打包时会将依赖整合然后形成模块的结合,但是又不能直接拼在一起,所以就有了如下的代码:

const modules = {
    "./a.js": function (module, exports) {
        console.log("module a");
        module.exports = "a";
    },
    "./c.js": function (module, exports, require) {
        console.log("module c");
        const a = require("./a.js");
        console.log("c import a, and a is " + a);
        module.exports = "c";
    },
    "./index.js": function (module, exports, require) {
        console.log("index module");
        const a = require("./a.js");
        const c = require("./c.js");
        console.log("index import a, and a is " + a);
        console.log("index import c, and c is " + c);
    }
}

如上述的代码所示,在commonjs中,exports=module.exports,所以这里module和exports易得,那么这个require在哪里呢?这里require就需要我们自己去造,怎么造呢?

(function (modules) {
    function require(moduleID) {
        const func = modules[moduleID];
        const module = {
            exports: {}
        };
        func(module, module.exports, require);
        return module.exports;
    }
    
    require("./index.js");
})(modules)

如上我们就可以将webpack打包整合代码的过程模拟出来,但是注意这里多了一个modules的变量,不是很完美,而且require对于同一模块的加载有缓存,所以需要稍微改动一下。

(function (modules) {
    const map = new Map();
    function require(moduleID) {
        if (map.has(moduleID)) {
            return map.get(moduleID);
        }
        const func = modules[moduleID];
        const module = {
            exports: {}
        };
        func(module, module.exports, require);
        map.set(moduleID, module.exports);
        return module.exports;
    }
    require("./index.js");
})({
    "./a.js": function (module, exports) {
        console.log("module a");
        module.exports = "a";
    },
    "./c.js": function (module, exports, require) {
        console.log("module c");
        const a = require("./a.js");
        console.log("c import a, and a is " + a);
        module.exports = "c";
    },
    "./index.js": function (module, exports, require) {
        console.log("index module");
        const a = require("./a.js");
        const c = require("./c.js");
        console.log("index import a, and a is " + a);
        console.log("index import c, and c is " + c);
    }
})

之后我们对比webpack打包整合代码之后的main.js的代码一下呢(去除了webpack打包之后的注释):

(() => {
    var __webpack_modules__ = ({
        "./src/a.js":module => {
            eval("console.log("module a");\r\nmodule.exports = "a";\n\n//# sourceURL=webpack://day-02/./src/a.js?");
        },
        "./src/c.js": (module, __unused_webpack_exports, __webpack_require__) => {
            eval("console.log("module c");\r\nconst a = __webpack_require__(/*! ./a.js */ "./src/a.js");\r\nconsole.log("c import a, and a is " + a);\r\nmodule.exports = "c";\n\n//# sourceURL=webpack://day-02/./src/c.js?");
        },
    
        "./src/index.js": (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
            eval("console.log("index module");\r\nconst a = __webpack_require__(/*! ./a.js */ "./src/a.js");\r\nconst c = __webpack_require__(/*! ./c.js */ "./src/c.js");\r\nconsole.log("index import a, and a is " + a);\r\nconsole.log("index import c, and c is " + c);\n\n//# sourceURL=webpack://day-02/./src/index.js?");
        }
   });
   var __webpack_module_cache__ = {};

   function __webpack_require__(moduleId) {
      var cachedModule = __webpack_module_cache__[moduleId];
      if (cachedModule !== undefined) {
         return cachedModule.exports;
      }
      var module = __webpack_module_cache__[moduleId] = {
         exports: {}
      };
      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
      return module.exports;
   }
   var __webpack_exports__ = __webpack_require__("./src/index.js");
 })();

可以看到webpack打包整合代码之后输出的代码和我们写的差不了多少。

但是可以看到的是,在每一个源文件依赖中是使用eval进行代码执行的,这是为啥呢?而且每一个eval最后都会有代码注释,这又是干啥用的呢?

关于eval这里我需要额外引入一个小知识点:

var a = 1;
var b = 2;
var obj = null;
obj.toString();

毫无意外上面的代码会报错,且报错内容如下: image.png

点进去看的时候会看到源代码所在的位置和代码内容。而//# sourceURL=${src},这一段内容虽然不参与JS代码执行,但是却被浏览器识别,当我们在代码中使用eval并且加上上述JS注释时:

var a = 1;
var b = 2;
eval(`var obj = null;
obj.toString();//# sourceURL=./src/a.js`)

加上如上eval时,浏览器报错如下

image.png

可以很明显的看到,右上角报错的原地址和之前的不一样,点进去查看报错代码时,代码也和之前不一样

所以用eval执行,可以开启一个新的JS执行环境,从而使当前代码和原代码脱离,在其中的报错也会和源代码隔离开

对于我们自己写的代码,也可以改为和webpack打包之后加上eval一样的代码如下:

(function (modules) {
    const map = new Map();
    function require(moduleID) {
        if (map.has(moduleID)) {
            return map.get(moduleID);
        }
        const func = modules[moduleID];
        const module = {
            exports: {}
        };
        func(module, module.exports, require);
        map.set(moduleID, module.exports);
        return module.exports;
    }
    require("./index.js");
})({
    "./a.js": function (module, exports) {
        eval("console.log("module a");\nmodule.exports = "a";//# sourceURL=./src/a.js")
    },
    "./c.js": function (module, exports, require) {
        eval("console.log("module c");\nconst a = require("./a.js");\nconsole.log("c import a, and a is " + a);\nmodule.exports = "c";//# sourceURL=./src/c.js")
    },
    "./index.js": function (module, exports, require) {
        eval("console.log("index module");\nconst a = require("./a.js");\nconst c = require("./c.js");\nconsole.log("index import a, and a is " + a);\nconsole.log("index import c, and c is " + c);//# sourceURL=./src/index.js")
    }
})

这里可以在其中某个eval中报一个错,各位小伙伴也可以拿去试试。

ok,差不多就这样,我理解的webpack打包时,整合代码的过程。欢迎各位大佬指正