webpack-打包后代码分析

转载自: http://echizen.github.io/tech/2019/03-17-webpack-bundle-code

你是否清楚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 混合使用详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容