all articles

learn webpack 1 - how it compiles

2017-03-09 @sunderls

js webpack

最近尝试写了个类似于raml的东西,想要做一些在webpack上的工具,实际用了一下才发现自己对webpack的理解还停留在初级阶段,也就是会用而已,并没有深入理解。现在回过头来重新认真学习一下。 以下是学习笔记,的样子?

1. 最基础的例子

首先根据官网(2.0)做如下例子:

index.html

<html>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

webpack.config.js

module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle.js'
    }
}

这里把app.js打包为bundle.js,清楚明了

app.js

import bar from './bar';

bar();

bar.js

export default function bar() {
    document.querySelector('#app').innerHTML = 'Hello Webpack';
}

app很简单,在页面上输出Hello Webpack

一般情况下都会使用local的webpack吧?

npm install --save-dev webpack@beta

package.json

"scripts": {
"build": "webpack"
}

npm run build之后,可以看到app没有什么问题。

2. 来看build之后的成果

虽然我们只写了几行代码,编译出来的结果却不只那么多,共有接近100行(太长,大概瞟一眼就可以跳到后面去了,后面会详细解释):

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;

/******/    // identity function for calling harmony imports with the correct context
/******/    __webpack_require__.i = function(value) { return value; };

/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, {
/******/                configurable: false,
/******/                enumerable: true,
/******/                get: getter
/******/            });
/******/        }
/******/    };

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

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = bar;
function bar() {
    document.querySelector('#app').innerHTML = 'Hellow Webpack';
}

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

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(0);

__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */])();

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

3. 非核心部分

可以看到webpack自己的代码部分,用了明显的*注释来标明,这一部分我们简写为bootstrap(),上述代码变为:

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

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = bar;
function bar() {
    document.querySelector('#app').innerHTML = 'Hellow Webpack';
}

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

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(0);

__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */])();

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

暂时不看bootstrap是干什么的。首先看到第0个module是bar.js,第一个是app.js。 来看bar.js

function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_exports__["a"] = bar;
    function bar() {
        document.querySelector('#app').innerHTML = 'Hellow Webpack';
    }
}

3.1 wrapper

可以看到webpack在我们的代码之上,增加了两个部分: 一个wrapper,一个exports,这个很像之前的文章Node.js module的require是如何实现的中的require不是么。 回顾一下:

  1. node.js首先查找到目标文件,读取文件内容,比如exports.foo = 'bar'
  2. 然后在文件内容中添加闭包的头尾: ['(function (exports, require, module, __filename, __dirname) { ', 'exports.foo = 'bar', '\n})']
  3. 通过原生引擎编译得到一个moduleWrapper,执行过后就初始化了一个module,这个module有一个foo可以用。

webpack做的事情类似上述流程,除了「原生引擎」编译变成了「把文件内容打包在一起,让浏览器执行」。所有的module都在一起,要不就是放在数组里面用index来指明,要不就是放一个map,用名字来指明module了,在我们的例子中,webpack用的是数组的方式。

bar.js中的 export default function bar()被字符串替换为了__webpack_exports__["a"] = bar;。这里的"a"应该就是default的意思吧。

3.2 app.js

再看 app.js的内容

(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(0);
__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */])();
})

首先__esModule的定义,应该是我们用了es6语法导致的,如果用require的话,这一句就会消失。

然后看下一句,__webpack_require__(0)通过index获取到module,然后__webpack_require__.i(module['a'])()执行,这里好奇的是为什么不直接module()? 在后面可以看到i()的定义。

4. bootstrap()

最后来看bootsrap()的内容,首先bootstrap接受modules方法数组(这里面不是module,而是module的生成方法)参数。嗯

4.1 __webpack_require__

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

可以看到require做了如下步骤

  1. 检查是否module已经加载了,在installedModules中,如果有就返回。
  2. 如果没有,就初始化一个module。初始化的过程是从modules数组中找到初始化方法,初始化并返回。

4.2 .m .c .i

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules

// expose the module cache
__webpack_require__.c = installedModules;

// identity function for calling harmony imports with the correct context
__webpack_require__.i = function(value) { return value; };

m和c都比较好懂,虽然不怎么用,i就有点不明白了。感觉其是根context,也就是this相关。如果bar()中用到了this,同时没有用identify function的话,bundle 的结果是:

__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */]();

这样,bar中的this指向了__WEBPACK_IMPORTED_MODULE_0bar,这实际上是module bar本身了。

如果加上了identity function, function(value) { return value; }, this一层一层往上,指向了global(在strict环境下,是undefined)。也就是说,用identity function来保证this不会受到干扰。这到底有没有用,实际上也有点微妙,webpack也正在考虑优化: https://github.com/webpack/webpack/issues/3001

4.3 .o, .d

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

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, {
            configurable: false,
            enumerable: true,
            get: getter
        });
    }
};

o实际上是hasOwnProperty的wrapper,可以看到d实际上是defineProperty的wrapper

4.4 .p 和entry module

// __webpack_public_path__
__webpack_require__.p = "";

// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 1);

public path暂时也不知道干嘛的。最后一句return,定义了entry module,第1个,然后require(1),这样第1个module就会被初始化,其中的代码就会被执行。

嗯,总体来看还是比较清楚的。

5. 多个export

尝试在bar.js增加一个export,就没有default了。

export let a = 'Hello';
export let b = 'world';
export let c = 'what?';
export function bar(msg) {
    document.querySelector('#app').innerHTML = msg;
}

app中import a,b和bar

import {bar, a, b} from './bar';
bar(a + b);

从bar.js编译的结果上看

/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return a; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return b; });
/* unused harmony export c */
/* harmony export (immutable) */ __webpack_exports__["a"] = bar;
let a = 'Hello';
let b = 'world';
let c = 'what?';
function bar(msg) {
    document.querySelector('#app').innerHTML = msg;
}

可以看到,bar()还是在"a",export的a和b,则通过closer,绑定到了export的"b""c",说明webpack在处理exports的时候是按照字母顺序的。 另外"c"则被忽略了,因为实际上没有被require。实际上require还不够,还需要被use才可以。

经过测试可以发现,a,b,c的顺序是根据实际使用时的顺序来的。比如

import {foo,bar,too} from 'moduleA';

bar(foo)
too();

那么编译的结果中,foo, bar, too对应的index就是"b", "a","c"

6. 非esModule的情况

上面的5种出现了这么一段代码:

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

可以看到时用来获取default exports的,针对非es6module,我们来试试。

修改bar.js,改为node module.

exports.a = 'Hello';
exports.b = 'world';
exports.c = 'what?';

exports.bar = (msg) => {
    document.querySelector('#app').innerHTML = msg;
}

app.js用如下import:

import * as Bar from './bar';
Bar.bar(Bar.a + Bar.b);

编译后可以看到出现了__webpack_require__.n(),用来获取exports本身,而bar.js本身也按照node module的方式进行了wrap。

function(module, exports) {

exports.a = 'Hello';
exports.b = 'world';
exports.c = 'what?';

exports.bar = (msg) => {
    document.querySelector('#app').innerHTML = msg;
}

/***/ }
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(0);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__bar___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__bar__);

__WEBPACK_IMPORTED_MODULE_0__bar__["bar"](__WEBPACK_IMPORTED_MODULE_0__bar__["a"] + __WEBPACK_IMPORTED_MODULE_0__bar__["b"]);

7. 总结

虽然还有很多地方没有看到,不过大致上可以感觉webpack做了哪些事情,简单总结一下(可能有错。。)

  1. 分析各个文件的export/import情况
  2. 根据语法(esModule或者node module)进行wrap,export的内容根据使用位置先后进行标号,称为初始化方法(或者生成器?)好了, 生成器存在数组中。
  3. 生成器数组最终传递给bootstrap方法中,这里定义require,module会被缓存,没有的时候才会调用生成器,生成器中通过传入的require可以引用其他模块。
  4. 最终require entry module,app就跑起来了。