webpack-打包后代码分析
17 March 2019

你是否清楚webpack究竟将你的代码处理成了什么样子?webpack是如何实现模块化的?为啥webpack既能支持es6 module又能支持commonjs规范的module?为啥webpack打包出来的模块在commonjs规范的模块里引用时要加default,即const moduleName = require('modulePath').default

如果你都清楚,那么本文可以跳过了~

webpack是什么

官方说: webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)。浏览器原生是不支持模块化的,虽然有新版本已经开始支持,但是考虑兼容性在相当长的一段时间里我们不能依赖,而webpack帮我们实现了模块化的支持,我们将代码按功能有序的进行模块化组织,webpack将这些模块化的代码合并打包成一个bundle文件,并使用自己的脚手架代码实现了模块化的语义,让模块与模块之间能够作用域隔离,能够互相引用。

webpack比较神奇的是不仅支持js的模块化,还能支持css\file\image等各种文件的模块化,得益于强大的loader将各种类型的文件处理成js模块。

webpack打包产物分析

demo

文中相关代码在这里:https://github.com/echizen/webpack-demo/tree/master/webpack-mod,可以自行clone下来验证

我们通过一个最简单的例子来分析打包产物,创造3个文件:

index.js:

import message from './message.js';

export const msg = message
export default msgEntry = message + '!'

message.js:

import {name} from './name.js';

export default `hello ${name}!`;

index.js:

export const name = 'world';

webpack配置:

const path = require('path');

module.exports = {
  mode: "production",
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.bundle.js'
  },
  optimization: {
    minimize: false
  }
};

看看index.bundle.js的内容:

/******/ (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 = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/name.js
const name_name = 'world';
// CONCATENATED MODULE: ./src/message.js


/* harmony default export */ var message = (`hello ${name_name}!`);
// CONCATENATED MODULE: ./src/index.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "msg", function() { return msg; });


const msg = message
/* harmony default export */ var src = __webpack_exports__["default"] = (msgEntry = message + '!');

/***/ })
/******/ ]);

分析

可以看出,webpack打包出来的文件,用installedModules记录缓存的模块分析结果。自定义了一个require的实现__webpack_require__来实现模块的依赖引入功能。

这个函数即是实现模块化的重点。原模块被包裹在function(module, __webpack_exports__, __webpack_require__) {}函数中。__webpack_require__里创造的局部变量module变量来记录此模块函数调用后返回的结果,将module.exports, module, module.exports, __webpack_require__作为参数传入,每个模块函数,先调用__webpack_require__.r(__webpack_exports__)module.exports上定义__esModule属性,作用是和前辈babel转化的结果保持一致,表明这是个由 es6 转换来的 commonjs 输出。。然后执行原模块代码,将export出的内容处理到module.exports上,如果是export default的内容则直接放置到module.exports["default"]上,其他的非default的valName的export通过__webpack_require__.d实现的在module.exportsdefinePropertyvalName的getter来获取。这样调用了包裹原模块的函数后,__webpack_require__函数最终return出来的module.exports上就有我们所有export出来的内容,实现了隔离作用域的模块化。


// 调用处
// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;

// 定义处
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/name.js
const name_name = 'world';
// CONCATENATED MODULE: ./src/message.js

/* harmony default export */ var message = (`hello ${name_name}!`);
// CONCATENATED MODULE: ./src/index.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "msg", function() { return msg; });


const msg = message
/* harmony default export */ var src = __webpack_exports__["default"] = (msgEntry = message + '!');

然后还定义了一群常用的功能函数:

  • webpack_require.d: define getter function for harmony exports,使用Object.defineProperty来定义对象上指定属性的getter函数
  • webpack_require.r: define __esModule on exports
  • webpack_require.t: create a fake namespace object,根据不同的mode做不同的处理
  • webpack_require.n: getDefaultExport function for compatibility with non-harmony modules
  • webpack_require.o: object.prototype.hasOwnProperty.call

Scope Hoisting

认真观察你会发现一个现象,按理说我们是3个文件对应的3个模块,但是在打包产物里却被合并成了一个模块。这就是Scope Hoisting特性。

webpack3开始引入了Scope Hoisting,Scope Hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并。

能使用Scope Hoisting特性的要求:

  • 必须是ES6规范的模块化
  • 没有使用 eval() 函数

modules列表用了Scope Hoisting作用域提升合并成了一个。好处:

  • 代码体积更小,因为函数申明语句会产生大量代码;
  • 代码在运行时因为创建的函数作用域更少了,内存开销也随之变小。

commonjs规范的模块打包

上面的示例是es6规范的模块,我们再来看看commonjs规范的模块:

index.js

var c = require('./name.js')
exports.default = c

name.js

let c1 = 'c1'
let c2 = 'c2'
module.exports = {
	c1,
	c2,
}

打包产物:

/******/ (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;
/******/ 	}
/******/
/******/

// 省略n个__webpack_require__上挂载的功能函数

/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

// import message from './message.js';

// export const msg = message
// export default msgEntry = message + '!'

var c = __webpack_require__(1)
exports.default = c


/***/ }),
/* 1 */
/***/ (function(module, exports) {

// export const name = 'world';
let c1 = 'c1'
let c2 = 'c2'
module.exports = {
	c1,
	c2,
}

/***/ })
/******/ ]);

可以看到cmd规范的模块没有使用Scope Hoisting了,区别也就是从es module都是在操作’module.exports’,变成了如果原模块透出的方式是module.exports则继续赋值给module.exports的对应属性上,如果原模块使用是exports.default则赋值给module.exports.default

所以webpack通过在__webpack_require__上内部构造一个module的局部变量,无论原模块是es6还是commonjs,或者是2者混用,都将原透出属性透出到module.exports的对应属性上,将透出的default属性透出到module.exports.default,来达到统一,内部可以支持各种类型的模块通用,因为都已经转化成了commonjs的格式。

为啥commonjs格式的模块引入webpack打包后的模块需要加default?

const moduleName = require('modulePath').default

上面的分析我们知道,commonjs 里的require对应的是webpack内部的__webpack_require__透出的module.exports,而default的对象被赋值在module.exports.default上,default也只是module.exports的一个叫default名的普通透出对象,自然是要加default属性声明的。

那么es6的模块为啥就可以直接import而不用管default呢?因为规范定义的import moduleName from 'modulePath'就是引入一个模块export default的内容啊,webpack为了遵循这个规范,在内部对default的内容做了标记处理,在引入的时候直接透出default的内容。

为了查看这个,我们必须打破Scope Hoisting特性,来看看import语句和__webpack_require__的对应调用关系。于是我们在message.js里使用下eval:

message.js

import {name} from './name.js';
eval('var test = 1')
export default `hello ${name}!`;

index.js

import message from './message.js';

export const msg = message
export default msgEntry = message + '!'

打包出来模块相关部分的代码:

([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony import */ var _name_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

eval('var test = 1')
/* harmony default export */ __webpack_exports__["a"] = (`hello ${_name_js__WEBPACK_IMPORTED_MODULE_0__[/* name */ "a"]}!`);

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return name; });
const name = 'world';
// let c1 = 'c1'
// let c2 = 'c2'
// module.exports = {
// 	c1,
// 	c2,
// }

/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "msg", function() { return msg; });
/* harmony import */ var _message_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);


const msg = _message_js__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"]
/* harmony default export */ __webpack_exports__["default"] = (msgEntry = _message_js__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"] + '!');

// var c = require('./name.js')
// exports.default = c


/***/ })
/******/ ]);

可以看到bundle发生了很大的变化,export default的内容不是再赋值给__webpack_exports__["default"],而是赋值给__webpack_exports__a, b这样内部定义属性的getter上,在import语句出处理成__webpack_require__(moduleId)["a"]这样找到对应的属性,即es6模块中也没有将default看成特殊属性,而是取了别名,内部也通过别名去__webpack_require__找到对应内容,这里的注释/* default */在标记时起到与原代码透出变量对应的功能。

为什么要这么搞呢?为啥default的es6和commonjs引入不搞成一致呢?为了跟规范保持一致啊,规范定义的import moduleName from 'modulePath'就是引入一个模块export default的内容,而const moduleName = require('modulePath')获取的是modulePath对应的模块透出的所有内容的一个对象即module.exports的内容,default只是一个普通属性即module.exports.default

再提个历史,在 babel5 时代,大部分人在用 require 去引用 es6 输出的 default,只是把 default 输出看作是一个模块的默认输出,所以 babel5 对这个逻辑做了 hack,如果一个 es6 模块只有一个 default 输出,那么在转换成 commonjs 的时候也一起赋值给 module.exports,即整个导出对象被赋值了 default 所对应的值。这样就不需要加 default。

babel5的这种做法其实会出问题:

// a.js

export default 123;

export const a = 123; // 新增

// b.js 

var foo = require('./a.js');

// 由之前的 输出 123, 变成 { default: 123, a: 123 },导致使用方要改动引入代码

所以babel6不再做module.exports=exports.default的处理了,如果因为历史问题依赖这个处理,可以加plugin:babel-plugin-add-module-exports

webpack和babel

webpack和babel都支持es6 module,babel通常作为webpack的loader处理js,这2者又是什么关系呢?

其实webpack2开始已经支持es6 module的处理,如果只是为了模块化,无需在加载babel,只是es6不止module还有很多其他特性需要babel帮我们处理。babel也是将es6 module转化成commonjs的module,从而再经过webpack时当commonjs module进行处理。

譬如, es module:

export default 123;

export const a = 123;

const b = 3;
const c = 4;
export { b, c };

babel转化后,处理成commonjs格式:

exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;

webpack的打包类型

webpack支持var、umd、commonjs、commonjs2、amd、amd-require、this、window、global、jsonp10种类型的打包产物:output-librarytarget

var (默认)

(function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
  // module code
  })
])

amd

define("modName", [], function() {
  return /******/ (function(modules) { // webpackBootstrap
      // webpackBootstrap
    })([
      // modules code
    ])
})

commonjs2

module.exports = (function(modules) { // webpackBootstrap
  // webpackBootstrap
})([
  // modules code
])

umd

output.library指定为modName时:

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)
		define([], factory);
	else if(typeof exports === 'object')
		exports["modName"] = factory();
	else
		root["modName"] = factory();
})(window, function() {
  return /******/ (function(modules) { // webpackBootstrap
      // webpackBootstrap
    })([
      // modules code
    ])
})

output.library未指定时:

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)
		define([], factory);
	else {
		var a = factory();
		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
	}
})(window, function() {
  return /******/ (function(modules) { // webpackBootstrap
      // webpackBootstrap
    })([
      // modules code
    ])
})

window || global

output.library指定为modName时:

window["modName"] = (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
  // modules code
])

output.library未指定时:

(function(e, a) { 
  for(var i in a) e[i] = a[i]; 
}(window, /******/ (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
    // module code
    })
  ])
))

this

output.library指定为modName时:

this["modName"] = (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
  // modules code
])

output.library未指定时:

(function(e, a) { 
  for(var i in a) e[i] = a[i]; 
}(this, /******/ (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
    // module code
    })
  ])
))

commonjs

output.library指定为modName时:

exports["modName"] = (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
  // modules code
])

output.library未指定时:

(function(e, a) { 
  for(var i in a) e[i] = a[i]; 
}(exports, /******/ (function(modules) { // webpackBootstrap
  // __webpack_require__ 相关定义
})([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
    // module code
    })
  ])
))

可以看到各种模块化的方式都是使用对应规范的代码包裹了webpack原本的脚手架代码和模块加载代码,使执行后的结果挂载到入windowsmodule.exportsexports["modName"]这些对象上。

参考文档

自己倒腾了demo之后,去网上搜了下,发现这位同僚的文章讲的更透彻:

import、require、export、module.exports 混合使用详解