前言
webpack 是一个用于静态资源打包的工具。它分析你的项目结构,会递归地构建依赖关系,找到其中脚本、图片、样式等将其转换和打包输出为浏览器能识别的资源。
本篇文章仅对 webpack 打包输出的文件进行简要的分析。
项目准备
项目地址
看一下几个关键文件:
12
| const foo = require('./foo.js');console.log(foo)
|
1234567891011
| 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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
| (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) })});
|
根据上面的源码可以看出,最终打包出的是一个自执行函数。
首先,这个自执行函数它接收一个参数 modules
,modules
为一个对象,其中 key
为打包的模块文件的路径,对应的 value
为一个函数,其内部为模块文件定义的内容。
然后,我们再来看一看自执行函数的函数体部分。函数体返回 __webpack_require__(__webpack_require__.s = "./src/index.js")
这段代码,此处为加载入口模块并返回模块的导出对象。
可以发现,webpack 自己实现了一套加载机制,即 __webpack_require__
,可以在浏览器中使用。该方法接收一个 moduleId
,返回当前模块的导出对象。
webpack 文件加载 (__webpack_require__)
123456789101112131415
| 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__
作为参数。
1
| modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
也就是去执行自执行函数传入的 modules
对象中当前 moduleId
对应的函数。接着将该模块标识为已经加载的状态,最后返回当前模块的导出对象。此时便完成了模块的加载任务。
接着,再来看看传入的 modules
对象部分。
1234567891011
| ({ "./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 是基于 Node.js 实现的,所以在文件加载部分并没有特别的处理。因此,这里我们来看看不同模块规范相互加载时,webpack 是如何处理的。
harmony(和谐,即对于不同模块规范加载的一个兼容处理)
这种方式即我们上面示例的加载方式,就不做赘述了。
CommonJS 加载 ES module
src/foo.js
src/index.js
12
| const foo = require('./foo.js');console.log(foo)
|
dist/bundle.js
123456789101112
| ({ "./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
方法是干嘛的呢?
12345678
| // ...__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
,即表明该模块为 ES 6 模块。同时定义该对象的 Symbol.toStringTag
为 Module
,即当使用 Object.prototype.toString.call
时将返回 [object Module]
。
最后,将模块的内容挂在 __webpack_exports__
的 default
属性上。
ES module 加载 ES module
src/foo.js
src/index.js
12
| import foo from './foo.js';console.log(foo)
|
dist/bundle.js
12345678910111213
| ({ "./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
属性获取到文件的默认导出内容。
那么,是如何实现这种方式的呢?
1234567891011121314151617
| // ...__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__.o
其实就是 Object.prototype.hasOwnProperty
的一个重写,用于判断对象自身属性中是否具有指定的属性。而 __webpack_require__.d
即 Object.defineProperty
,这里用于定义兼容各种模块规范输出的 getter 函数。__webpack_require__.n
则是用于获取模块的默认导出对象,兼容 CommonJS 和 ES module 两种方式。
ES module 加载 CommonJS
src/foo.js
src/index.js
12
| import foo from './foo.js';console.log(foo)
|
dist/bundle.js
12345678910111213
| ({ "./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 对于不同模块规范的相互加载的处理,我们已经有了基本的了解。但此时我们的文件加载都是同步的,那么文件的异步加载又是怎么样的呢?
且听下回分解。
推荐阅读:
高校技术社团 ifLab 如何通过线上训练营促进编程学习
为什么说学会学习是开发者重要且必备的技能?
[fCC 100] 祝贺 freeCodeCamp 全球社区 2019 Top Contributors
活动预告
6.20 本周六上午 10:00 - 11:30,开发者 S1ng S1ng 直播做 freeCodeCamp 中级算法题目,欢迎大家在 bilibili 搜索关注 freeCodeCamp 后进入直播间交流、学习!
扫码观看 S1ng S1ng 直播做基础算法题目的视频
非营利组织 freeCodeCamp.org 自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为
freeCodeCamp 社区的贡献者吗
欢迎点击以下文章了解
✨✨
招募丨freeCodeCamp 翻译计划
成为 freeCodeCamp 专栏作者,与世界各地的开发者分享技术知识
在 freeCodeCamp 专栏阅读更多