单文件模块webpack打包后结果分析

115 阅读9分钟

打包配置

下面展示一个极简单(单文件模块打包)的webpack配置:

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    // 打包模式
    mode: 'development',
    // 不需要产出source-map
    devtool: 'none',
    // 打包入口
    entry: './src/index.js',
    output: {
        // 产出文件名字
        filename: 'built.js',
        // 产出文件路径
        path: path.resolve('dist')
    },
    plugins: [
        new htmlWebpackPlugin({
            // 模板文件路径
            template: 'src/index.html',
            // 输出的html文件名字
            filename: 'index1.html'
        })
    ]
}

以及src/index.js内容:

console.log('index.js内容')
module.exports = {
    text: '入口文件导出内容'
}

当然最后打包的结果我也写好注释啦。(此处不要关注函数内容,先看整体的代码结构,后面针对CommonJS模块和ESM会有区分)

(function (modules) {
    // 用于缓存已经加载过得模块
    var installedModules = {};
    // The require function
    // webpack重写的 require 方法
    function __webpack_require__ (moduleId) {
        // 检查缓存中是否有   或者说当前模块是否已经加载过
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // 如果没有加载过,则手动创建一个新的模块
        var module = installedModules[moduleId] = {
            i: moduleId, // 模块id 其实就是模块的路径组成的字符串
            l: false, // 该模块是否已加载 true是  false 否
            exports: {} // 模块导出的内容
        };

        // 加载模块的方法
        // 初次加载相当于  modules[moduleId].call({}, {
        //   i: './src/index.js',
        //   l: false,
        //   exports: {}
        // }, __webpack_require__)
        // 如果模块中引入了其他模块,则递归的将 __webpack_require__ 传下去,因为加载模块是 __webpack_require__ 处理的
        // 对于加载入口模块来说,其实就是调用了参数中 "./src/index.js" 对应的函数罢了
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // 加载完模块后将该模块标记为 已加载
        module.l = true;

        // 返回模块导出的内容
        return module.exports;
    }


    // 记录所有的模块
    __webpack_require__.m = modules;

    // 记录模块缓存
    __webpack_require__.c = installedModules;

    // 给模块的某属性定义getter
    __webpack_require__.d = function (exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, { enumerable: true, get: getter });
        }
    };

    // 给模块定义 __esModule
    __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

    // 根据mode值不同对value进行不同的处理
    __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;
    };

    // 给模块定义getter方法并返回
    __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 方法
    __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

    // 记录 webpack.config.js 配置的 output.publicPath
    __webpack_require__.p = "";
    // 加载入口模块并返回入口模块的导出内容
    return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
    "./src/index.js": (function (module, exports) {
        console.log('index.js内容')
        module.exports = {
            text: '入口文件导出内容'
        }
    })
});

打包出来的结果其实就是一个IIFE,参数为一个json对象,入口文件的路径作为key,value为一个函数体为文件内容的函数。主要通过IIFE中定义的 __webpack_require__ 来执行文件的加载操作。

__webpack_require__ 定义了几个功能属性像m,c,d,r,f等等每个属性有不同的作用,在单文件打包中这些属性体现不出来作用,但是在真实项目下循环引用的模块就需要这些属性去处理了。

通过require导入的模块,在webpack处理下会被替换为__webpack_require__。此处也是__webpack_require__中执行call函数的是传入__webpack_require__函数的原因所在。另外说的webpack会改变项目的require方法也是说的这里。
即:

({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      // 此处递归调用 __webpack_require__ 加载模块
      const text = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log('index.js内容执行了')
      console.log(text)
    }),
  "./src/login.js":
      // 此处没有__webpack_require__是因为在login中没有再引入别的模块
    (function (module, exports) {
      module.exports = "测试文本";
    })
});

Commionjs模块打包

下面来说说CommonJs模块打包。

由CommonJs规范加载CommonJs规范导出的模块

主要区别还是 webpack_require 函数。对于CommonJs导出的模块来说和上面代码没有任何区别。

// src/index.js
const text = require("./login");
console.log(text)
console.log("index文件")
// src/login.js
module.exports = "测试文本"

结果(此处只摘抄了__webpack_require__函数,其他代码同上):

function __webpack_require__ (moduleId) {
  // 检查缓存中是否有   或者说当前模块是否已经加载过
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 如果没有加载过,则手动创建一个新的模块
  var module = installedModules[moduleId] = {
    i: moduleId, // 模块id 其实就是模块的路径组成的字符串
    l: false, // 该模块是否已加载 true是  false 否
    exports: {} // 模块导出的内容
  };

  // 加载模块的方法
  // 初次加载相当于  modules[moduleId].call({}, {
  //   i: './src/index.js',
  //   l: false,
  //   exports: {}
  // }, __webpack_require__)
  // 如果模块中引入了其他模块,则递归的将 __webpack_require__ 传下去,因为加载模块是 __webpack_require__ 处理的
  // 对于加载入口模块来说,其实就是调用了参数中 "./src/index.js" 对应的函数罢了
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 加载完模块后将该模块标记为 已加载
  module.l = true;

  // 返回模块导出的内容
  return module.exports;
}

webpack是默认支持CommonJs模块打包的,直接把代码中的require替换为 __webpack_require__ 函数,而且对于module.exports是没有做任何操作的,即webpack默认支持的就是CommonJs规范。

加载ESM

为了展示加载ESM的效果,我改了index.js和login.js的内容:

// src/index.js
const miaomiao = require("./login");
console.log(miaomiao.default, miaomiao.test)
// src/login.js
export default "我家猫真调皮";
export const test = "真想揍它一顿";

相对应的,打包的结果肯定是有变化的。 CommonJs:

({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      // 此处递归调用 __webpack_require__ 加载模块
      const text = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log('index.js内容执行了')
      console.log(text)
    }),
});

ESM:

({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      const miaomiao = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log(miaomiao.default, miaomiao.test)
    }),
  "./src/login.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "test", function () { return test; });
      /* harmony default export */ __webpack_exports__["default"] = ("我家猫真调皮");
      const test = "真想揍它一顿";
    })
});

可以看到,首先对于参数来说,加载模块时直接将require替换为了 webpack__require ,其他的没有变化,但是在src/login.js中导出ESM时,是有特殊操作的:

  • 先对module.exports进行了一个r操作,主要是给module.exports定义了ES模块的属性:__esModule 和toStringTag(只有ES6有)
  • 给module.exports定义test属性的getter,因为我们在login.js中定义的变量就是test。
  • 给module.exports定义default属性,因为在login.js中使用了 export default

对于ESM来说,首先会对参数module.exports(形参写成了__webpack_exports__,其实是module.exports)进行了r操作的处理。上面说过了,r操作其实是给参数添加Symbol(如果是ES6的话)和__esModule属性。

之后又对module.exports定义default属性,因为在进行了d操作,即给module.exports定义属性,属性名就是在login.js中定义的test。

最后一步就是给module.exports定义default属性,因为在login.js中有使用default导出的对象。而default属性对应的值也是在login.js中定义的值。

export default "我家猫真调皮";

很乱吧。其实只要记住一点,这里的module.exports和我们在文件中使用的不一样,module其实是webpack定义的对象(在__webpack_require__中定义的),exports只是他的一个属性而已,exports的初始值就是一个空对象。这里可以去翻__webpack_require__函数的内容。

ESM模块打包

上面说了CommonJs引入ESM的情况,那接下来试试使用ESM的方式引入模块。

CommonJs加载ESm

我将两个实例文件的内容改成如下:

// src/index.js
import miaomiao from "./login";
console.log(miaomiao)
// src/login.js  注意这时候是通过CommonJs导出的
module.exports = "我家猫真调皮"

当然最终结果的区别还是在参数上:

({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./login */ "./src/login.js");
      var _login__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_login__WEBPACK_IMPORTED_MODULE_0__);
      console.log(_login__WEBPACK_IMPORTED_MODULE_0___default.a)
    }),
  "./src/login.js":
    (function (module, exports) {
      module.exports = "我家猫真调皮"
    })
});

可以发现,通过ESM的方式引入CommonJs时,通过CommonJs导出的内容不会进行特殊操作,如参数中的src/login.js的内容,而且也会对importrequire一样替换为__webpack_require__,但是也会有一些差异:

  • 引入时对module.exports进行r操作处理(见参数中的src/index.js),这是因为引入时判断是通过ESM的方式,所以进行了一个针对ES6的处理。
  • 不同于CommonJS,在接收模块内容的时候变量名字是经过webpack处理了的,如_login__WEBPACK_IMPORTED_MODULE_0__,并且用_login__WEBPACK_IMPORTED_MODULE_0___default.a来表示导出的内容(a属性是通过__webpack_require__.n添加上去的),当然使用时也是_login__WEBPACK_IMPORTED_MODULE_0___default.a

ESM加载ESM

为了演示这种情况,我双叒叕改了两个文件的内容:

// src/index.js
import miaomiao from "./login";
console.log(miaomiao)
// src/login.js
export default "我家猫真调皮";
export const test = "真想揍它一顿"

于此同时,打包结果也肯定会变啦。
还是只展示参数:

({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log(_login__WEBPACK_IMPORTED_MODULE_0__["default"])
    }),
  "./src/login.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, "test", function () { return test; });
      __webpack_exports__["default"] = ("我家猫真调皮");
      const test = "真想揍它一顿"
    })
});

你们有没有发现区别?r操作和d操作去找上面的代码吧,记得对于ESM来说会有两个操作将其标记为ESM即可(即r)我要去撸猫啦。 最后还是附上一份结果:

(function (modules) { // webpackBootstrap
  // 用于缓存已经加载过得模块
  var installedModules = {};
  // webpack重写的 require 方法
  function __webpack_require__ (moduleId) {
    // 检查缓存中是否有   或者说当前模块是否已经加载过
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 如果没有加载过,则手动创建一个新的模块
    var module = installedModules[moduleId] = {
      i: moduleId, // 模块id 其实就是模块的路径组成的字符串
      l: false, // 该模块是否已加载 true是  false 否
      exports: {} // 模块导出的内容
    };

    // 加载模块的方法
    // 初次加载相当于  modules[moduleId].call({}, {
    //   i: './src/index.js',
    //   l: false,
    //   exports: {}
    // }, __webpack_require__)
    // 如果模块中引入了其他模块,则递归的将 __webpack_require__ 传下去,因为加载模块是 __webpack_require__ 处理的
    // 对于加载入口模块来说,其实就是调用了参数中 "./src/index.js" 对应的函数罢了
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 加载完模块后将该模块标记为 已加载
    module.l = true;

    // 返回模块导出的内容
    return module.exports;
  }


  // 记录所有的模块
  __webpack_require__.m = modules;

  // 记录模块缓存
  __webpack_require__.c = installedModules;

  // 给模块的某属性定义getter
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // 将模块定义为一个ES Module
  __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
  // 根据mode值不同对value进行不同的处理
  __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;
  };

  // 给模块定义getter方法并返回
  __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 方法
  __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // 记录 webpack.config.js 配置的 output.publicPath
  __webpack_require__.p = "";


  //  // 加载入口模块并返回入口模块的导出内容
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log(_login__WEBPACK_IMPORTED_MODULE_0__["default"])
    }),
  "./src/login.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, "test", function () { return test; });
      __webpack_exports__["default"] = ("我家猫真调皮");
      const test = "真想揍它一顿"
    })
});