不同的需求与不同的运行环境,衍生出了不同的模块化标准,比如commonjs/esmodule/amd/cmd等,当然还有兼容大部分标准的umd。

    commonjs由node.js的具体实现。

    esmodule是js/es的标准实现,将是未来的主流。

    amd/cmd是异步的模块化规范,有requirejs,sea.js,systemjs等实现。

    在实际项目中,commonjs/esmodule是用的更多的。在我们的项目中,通常是commonjs/esmodule写代码,最后转成了webpack实现的浏览器端commonjs。

    在试验webpack前,我们先来试想下commonjs是如何运行呢?module,exports是个怎么回事?

    接下来,我们暂且用commonjs机制来瞧瞧打包的前后结果。

    b.js:

    1. module.exports = 2;

    a.js:

    const b = require('./b.js');
    
    module.exports = function (num) {
        return num+b
    };
    

    入口文件index.js:

    const a = require('./a.js');
    
    console.log(a(2));
    

    实际上三个文件,逻辑很简单,即入口index.js引入了a,a是个函数,接受一个参数,并引入了b.js导出的常量,返回参数与常量相加的结果。

    众所周知,浏览器不支持commonjs规范,想要让这样的代码生效,就要做模块化打包。

    我们接下来会用webpack尝试打包,但尝试之前我们想来设想下,commonjs是怎么回事,requre,module和exports是怎么回事,为什么通过require可以引用到其他的模块,require是怎么来的?

    站在node.js设计者的角度上来想,如何实现一个模块加载机制呢?如何管理这些依赖和文件呢?

    首先我们要把所有用到的文件加载出来,这一步按照node.js加载规则递归逐个分析并读取就好,重点在于读取后怎么办呢?

    比如上面的代码,我们的运行文件是index.js,我们调用了require(‘./a.js’),那么就去找到a.js文件,把文件内容读取出来,缓存起来。接下来,在a里又发现了require(‘./b.js’),那么就去找到b.js,把文件内容读取出来,缓存起来,直到没有require为止,当然了,这里深入下去还会涉及到循环引用的问题,但百度上有一堆解读的文章,有兴趣的同学可以去了解一下,我们这里先不跑题。

    回到之前的问题,加载文件就是一个寻找require然后加载文件内容并缓存的过程,那么要存在哪呢?肯定是内存里了,以什么形式去存呢?数组肯定是无法满足了,对象(map)是可以的。首先,我们在加载文件的过程中,是可以获取到当前目录的,比如a.js在c盘目录下,那全局路径就是c:/a.js。c:/a.js是一个唯一的标识,那么就可以作为该文件的key。value的话,我们可以是用一个函数,来包裹住文本内容。

    const sourceModules = {
        "c:\\index.js":function(module,exports,require) {
        const a = require('./a.js');
            console.log(a(2));
      },
      "c:\\a.js":function(module,exports,require) {
        const b = require('./b.js');
    
            module.exports = function (num) {
            return num+b
            };
      },
      "c:\\b.js":function(module,exports,require) {
        module.exports = 2;
      },
    }
    

    运行文件是index.js,第一步肯定要先执行他。后面每个引用的模块,第一次被引用时,都会被调用一次。但是require方法,还有modules和exports等对象从哪来呢?当然要自己实现了。

    function run(execFile,modules) {
    
        const installedModules = {};
    
        const require = function require(id) {
    
            // 假设currentPath是当前文件的cwd目录 并且加上id 就是完整的路径
        const currentPath = "c:\\";
          const fullModuleName = currentPath+id.slice(2);
          // 如果已经引用过这个模块了,直接返回结果,不重新执行
          if(installedModules[fullModuleName]) {
              return depModules[fullModuleName].exports;
          }
    
          const module = {
              id:fullModuleName,
            exports:{},
          }
    
          installedModules[fullModuleName] = module;
    
          sourceModules[fullModuleName](module,module.exports,require);
    
          return module.exports;
    
        }
    
      require(execFile);
    
    }
    
    run("c:\index.js",sourceModules);
    

    我们现在把这两段代码运行到浏览器里去运行一下,运行成功,并如期望般输出了a(2)的结果,即2+2等于4。真的没有什么黑魔法,只不过是map的与缓存模式结合的简单实践。我们仅仅用了几十行代码就实现了一个简易的commonjs模块加载器。

    下面我们用webpack来打包下看看:

    /******/
    (function (modules) { // webpackBootstrap
        /******/     // The module cache
        /******/
        var installedModules = {};
        /******/
        /******/     // The require function
        /******/
        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)
            /******/
            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;
            /******/
        }
    
        /******/
        /******/
        /******/     // expose the modules object (__webpack_modules__)
        /******/
        __webpack_require__.m = modules;
        /******/
        /******/     // expose the module cache
        /******/
        __webpack_require__.c = installedModules;
        /******/
        /******/     // define getter function for harmony exports
        /******/
        __webpack_require__.d = function (exports , name , getter) {
            /******/
            if (!__webpack_require__.o(exports , name)) {
                /******/
                Object.defineProperty(exports , name , {
                    enumerable: true ,
                    get: getter
                });
                /******/
            }
            /******/
        };
        /******/
        /******/     // define __esModule on exports
        /******/
        __webpack_require__.r = function (exports) {
            /******/
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                /******/
                Object.defineProperty(exports , Symbol.toStringTag , { value: 'Module' });
                /******/
            }
            /******/
            Object.defineProperty(exports , '__esModule' , { value: true });
            /******/
        };
        /******/
        /******/     // create a fake namespace object
        /******/     // mode & 1: value is a module id, require it
        /******/     // mode & 2: merge all properties of value into the ns
        /******/     // mode & 4: return value when already ns object
        /******/     // mode & 8|1: behave like require
        /******/
        __webpack_require__.t = function (value , mode) {
            /******/
            if (mode & 1) value = __webpack_require__(value);
            /******/
            if (mode & 8) return value;
            /******/
            if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
            /******/
            var ns = Object.create(null);
            /******/
            __webpack_require__.r(ns);
            /******/
            Object.defineProperty(ns , 'default' , {
                enumerable: true ,
                value: value
            });
            /******/
            if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns , key , function (key) { return value[key]; }.bind(null , key));
            /******/
            return ns;
            /******/
        };
        /******/
        /******/     // getDefaultExport function for compatibility with non-harmony
                  // modules
        /******/
        __webpack_require__.n = function (module) {
            /******/
            var getter = module && module.__esModule ?
                /******/            function getDefault() { return module['default']; } :
                /******/            function getModuleExports() { return module; };
            /******/
            __webpack_require__.d(getter , 'a' , getter);
            /******/
            return getter;
            /******/
        };
        /******/
        /******/     // Object.prototype.hasOwnProperty.call
        /******/
        __webpack_require__.o = function (object , property) { return Object.prototype.hasOwnProperty.call(object , property); };
        /******/
        /******/     // __webpack_public_path__
        /******/
        __webpack_require__.p = "/";
        /******/
        /******/
        /******/     // Load entry module and return exports
        /******/
        return __webpack_require__(__webpack_require__.s = "./index.js");
        /******/
    })
    /************************************************************************/
    /******/({
    
        /***/ "./a.js":
        /*!**************!*\
         !*** ./a.js ***!
         \**************/
        /*! no static exports found */
        /***/ (function (module , exports , __webpack_require__) {
    
            var b = __webpack_require__(/*! ./b.js */ "./b.js");
    
            module.exports = function (num) {
                return num + b;
            };
    
            /***/
        }) ,
    
        /***/ "./b.js":
        /*!**************!*\
         !*** ./b.js ***!
         \**************/
        /*! no static exports found */
        /***/ (function (module , exports) {
    
            module.exports = 2;
    
            /***/
        }) ,
    
        /***/ "./index.js":
        /*!******************!*\
         !*** ./index.js ***!
         \******************/
        /*! no static exports found */
        /***/ (function (module , exports , __webpack_require__) {
    
            var a = __webpack_require__(/*! ./a.js */ "./a.js");
    
            console.log(a(2));
    
            /***/
        })
    
        /******/
    });
    

    上面有一些无用的代码和注释,我们精简一下,看看实际产生的代码。

    (function (modules) {
    
        var installedModules = {};
    
        function __webpack_require__(moduleId) {
    
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
    
            }
    
            var module = installedModules[moduleId] = {
                i: moduleId ,
                l: false ,
                exports: {}
    
            };
    
            modules[moduleId].call(module.exports , module , module.exports , __webpack_require__);
    
            module.l = true;
    
            return module.exports;
        }
    
        return __webpack_require__("./index.js");
    })({
    
        "./a.js": (function (module , exports , __webpack_require__) {
    
            var b = __webpack_require__("./b.js");
    
            module.exports = function (num) {
                return num + b;
            };
    
        }) ,
    
        "./b.js": (function (module , exports) {
    
            module.exports = 2;
    
        }) ,
    
        "./index.js": (function (module , exports , __webpack_require__) {
    
            var a = __webpack_require__("./a.js");
    
            console.log(a(2));
    
        })
    });
    

    除了用了立即执行函数,和改了require方法名外,基本和我们的实现是一致的,为什么webpack把require改成了webpackrequire__而不是require?这个问题大家可以想一想。

    顺着生成的文件和代码,我们来逆向推理一下,webpack是如何做到这一点的?

    从webpack的设计者角度出发,我们首先要读取配置,通常也就是webpack.config.js,在这个文件里我们会读取到入口文件的配置,打包结果的配置,entry即是我们的index.js。我们必须要有一个入口文件,否则代码无法执行,这就是为什么entry是必填项的原因。

    接下来,通过entry文件加载,先分析调用了require方法的地方,分解出require的调用参数也就是模块id,然后拼接路径,去给文件内容读取出来,建立sourceModules,来确保后面代码runtime的依赖关系,其实一切依赖都是被摊平的一层维度。

    抽象函数的过程就是找出不变,和变化的地方,我们这里也应当遵循这个原则,例如require方法的实现和installedModules的初始化,以及是不会产生变化的。

    唯一变动的,就是初始调用的入口id还有modules,那我们就可以基于这个不变的模板来动态生成文件。