webpack 打包文件分析(上)

1,734 阅读7分钟

前言

Webpack 是一个用于静态资源打包的工具。它分析你的项目结构,会递归的构建依赖关系,找到其中脚本、图片、样式等将其转换和打包输出为浏览器能识别的资源。

本篇文章仅对webpack打包输出的文件进行简要的分析。

项目准备

项目地址

看一下几个关键文件:

  • 依赖文件 src/foo.js
module.exports = 'foo';
  • 入口文件 src/index.js
const foo = require('./foo.js');
console.log(foo)
  • webpack配置文件 webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development', // 标识不同的环境,development 开发 | production 生产
  devtool: 'none', // 不生成 source map 文件
  entry: './src/index.js', // 文件入口
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出目录
    filename: 'bundle.js', // 输出文件名称
  }
}

bundle分析

首先放上打包输出文件 dist/bundle.js

 (function(modules) {
  // 模块缓存对象
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 创建一个新的模块对象
    var module = installedModules[moduleId] = {
      i: moduleId, // 模块id,即模块所在的路径
      l: false, // 该模块是否已经加载过了
      exports: {} // 导出对象
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 标识模块已经加载过了
    module.l = true;
    return module.exports;
  }
  // 该属性用于公开modules对象 (__webpack_modules__)
  __webpack_require__.m = modules;
  // 该属性用于公开模块缓存对象
  __webpack_require__.c = installedModules;
  // 该属性用于定义兼容各种模块规范输出的getter函数,d即Object.defineProperty
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };
  // 该属性用于在导出对象exports上定义 __esModule = true,表示该模块是一个 ES 6 模块
  __webpack_require__.r = function(exports) {
    // 定义这种模块的Symbol.toStringTag为 [object Module]
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // 创建一个命名空间对象
  // mode & 1: 传入的value为模块id,使用__webpack_require__加载该模块
  // mode & 2: 将传入的value的所有的属性都合并到ns对象上
  // mode & 4: 当ns对象已经存在时,直接返回value。表示该模块已经被包装过了
  // mode & 8|1: 行为类似于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);
    // 将ns对象标识为es模块
    __webpack_require__.r(ns);
    // 给ns对象定义default属性,值为传入的value
    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;
  };
  // 获取模块的默认导出对象,这里区分 CommonJS 和 ES module 两种方式
  __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;
  };
  // 该属性用于判断对象自身属性中是否具有指定的属性,o即Object.prototype.hasOwnProperty
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  // 该属性用于存放公共访问路径,默认为'' (__webpack_public_path__)
  __webpack_require__.p = "";
  // 加载入口模块并返回模块的导出对象
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
  "./src/foo.js":
  (function(module, exports) {
    module.exports = 'foo';
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
    const foo = __webpack_require__("./src/foo.js");
    console.log(foo)
  })
});

根据上面的源码可以看出,最终打包出的是一个自执行函数。

首先,这个自执行函数它接收一个参数modulesmodules为一个对象,其中key为打包的模块文件的路径,对应的value为一个函数,其内部为模块文件定义的内容。

然后,我们再来看一看自执行函数的函数体部分。函数体返回 __webpack_require__(__webpack_require__.s = "./src/index.js") 这段代码,此处为加载入口模块并返回模块的导出对象。

可以发现,webpack自己实现了一套加载机制,即__webpack_require__,可以在浏览器中使用。该方法接收一个moduleId,返回当前模块的导出对象。

webpack文件加载 (__webpack_require__)

  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;
  }
  // ...

首先,当前作用域顶端声明了installedModules这个对象,它用于缓存加载过的模块。在__webpack_require__方法内部,会对于传入的moduleId在缓存对象中查找对应的模块是否存在,如果已经存在,返回该模块对象的导出对象;否则,创建一个新的模块对象,记录当前模块id、标识模块是否加载过、以及定义导出对象,同时将它放到缓存对象中。

接下来就是重要的一步,执行模块的函数内容,传入module、module.exports及__webpack_require__作为参数。

 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

也就是去执行自执行函数传入的modules对象中当前moduleId对应的函数。接着将该模块标识为已经加载的状态,最后返回当前模块的导出对象。此时便完成了模块的加载任务。

接着,再来看看传入的modules对象部分。

 ({
  "./src/foo.js":
  (function(module, exports) {
    module.exports = 'foo';
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
    const foo = __webpack_require__("./src/foo.js");
    console.log(foo)
  })
})

观察函数体内容,可以看到对于依赖模块foo.js而言,函数体内即为foo.js文件中的定义内容。而对于入口模块index.js,则需要执行__webpack_require__方法将依赖的文件加载进来使用。

那么,到此为止,我们已经明白了webpack加载模块的基本原理。但细心的你一定发现了,我们的文件导入导出遵循的是commonjs规范,而webpack是基于nodejs实现的,所以在文件加载部分并没有特别的处理。因此,这里我们来看看不同模块规范相互加载时,webpack是如何处理的。

harmony(和谐,即对于不同模块规范加载的一个兼容处理)

  • CommonJS 加载 CommonJS

这种方式即我们上面示例的加载方式,就不做赘述了。

CommonJS 加载 ES module

src/foo.js

export default 'foo';

src/index.js

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

dist/bundle.js

({
  "./src/foo.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    __webpack_exports__["default"] = ('foo');
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
    const foo = __webpack_require__("./src/foo.js");
    console.log(foo)
  })
})

由打包后的源码可以发现,当foo.js使用es module方式导出,与之前的相比,多了__webpack_require__.r(__webpack_exports__)这段代码,__webpack_exports__很好理解,即模块的导出对象。那么,__webpack_require__.r方法是干嘛的呢?

// ...
__webpack_require__.r = function(exports) {
  if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  }
  Object.defineProperty(exports, '__esModule', { value: true });
};
// ...

根据其实现可知,该方法将传入的对象标识上__esModule=true,即表明该模块为es6模块。同时定义该对象的Symbol.toStringTagModule,即当使用Object.prototype.toString.call时将返回[object Module]

最后,将模块的内容挂在__webpack_exports__default属性上。

ES module 加载 ES module

src/foo.js

export default 'foo';

src/index.js

import foo from './foo.js';
console.log(foo)

dist/bundle.js

({
  "./src/foo.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    __webpack_exports__["default"] = ('foo');         
  }),
  "./src/index.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
    console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0__["default"])
  })
})

当入口文件index.js和依赖文件foo.js都遵循es module的方式时,可以发现在index.js中,对于获取导出对象的方式也有所不同。_foo_js__WEBPACK_IMPORTED_MODULE_0__用来接收导入的文件,并通过default属性获取到文件的默认导出内容。

那么,是如何实现这种方式的呢?

// ...
__webpack_require__.d = function(exports, name, getter) {
  if(!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: 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;
};

__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// ...

分析这几个方法可以发现,__webpack_require__.oObject.prototype.hasOwnProperty的一个重写,用于判断对象自身属性中是否具有指定的属性。而__webpack_require__.dObject.defineProperty,这里用于定义兼容各种模块规范输出的 getter 函数。

__webpack_require__.n则是用于获取模块的默认导出对象,兼容 CommonJS 和 ES module 两种方式。

ES module 加载 CommonJS

src/foo.js

module.exports = 'foo';

src/index.js

import foo from './foo.js';
console.log(foo)

dist/bundle.js

({
  "./src/foo.js":
  (function(module, exports) {
    module.exports = 'foo';
  }),
  "./src/index.js":
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
    var _foo_js__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_foo_js__WEBPACK_IMPORTED_MODULE_0__);
    console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0___default.a)
  })
})

当入口文件index.js以es module的方式加载遵循commonjs规范的foo.js时,通过__webpack_require__加载传入的模块,将得到的模块_foo_js__WEBPACK_IMPORTED_MODULE_0__再传入__webpack_require__.n方法获取到该模块的默认导出对象。因为foo.js中的内容是通过export导出,而非export default导出。因此foo被挂在了default的一个a属性上。

结语

webpack对于不同模块规范的相互加载的处理,我们已经有了基本的了解。但此时我们的文件加载都是同步的,那么文件的异步加载又是怎么样的呢?

请听下回分解。