WEBPACK 模块化分析

96
MindMost
2016.06.05 12:53* 字数 1535

最近学习react的时候,发现前端的自动化构建大都基于webpack进行,webpack可能在未来会成为一个通用的标准,而不需要在茫茫多的自动化构建工具中选择啦。

webpack,故名思意,打包web相关的东西,模块化是它立身的根本。其官网中给出了详尽的配置说明,但是就是由于太"详尽"啦,可能会让从未接触过webpack的童鞋们一头雾水。本文希望能从分析webpack模块化运行机制的角度入手,更好的理解一些配置参数的意义。


简单的配置案例

虽然配置项很多,但是我们从最简单的开始,如下是一份最简化的配置样例webpack.config.js

module.exports = {
    //指定打包入口文件的信息
    entry: {
        main : './a.js'
    },
    //指定打包出口文件的信息
    output: {
        filename: "[name].js",
        path : __dirname + '/dist'
    }
};  

a.js文件中的内容

require('./b.js');
module.exports = {
    a : true
};

b.js文件中的内容

module.exports = {
     b:true
};

打包内容

运行webpack命令后,打包出来的文件在dist目录下,文件名为main.js,其内容正是webpack模块化的magic所在,那我们来一勘究竟。去掉webpack内置的一些无意义的注释后,其内容如下:

1: (function(modules) { // webpackBootstrap
2:  // The module cache
3:  var installedModules = {};
4:  // The require function
5:  function __webpack_require__(moduleId) {
6:      // Check if module is in cache
7:      if(installedModules[moduleId])
8:          return installedModules[moduleId].exports;
9:      // Create a new module (and put it into the cache)
10:         var module = installedModules[moduleId] = {
11:         exports: {},
12:             id: moduleId,
13:             loaded: false
14:         };
15:         // Execute the module function
16:         modules[moduleId].call(module.exports, module,  17: module.exports, __webpack_require__);
18:         // Flag the module as loaded
19:         module.loaded = true;
20:         // Return the exports of the module
21:         return module.exports;
22:     }
23:     // expose the modules object (__webpack_modules__)
24:     __webpack_require__.m = modules;
25:     // expose the module cache
26:     __webpack_require__.c = installedModules;
27:     // __webpack_public_path__
28:     __webpack_require__.p = "";
29:     // Load entry module and return exports
30:     return __webpack_require__(0);
31: })
32: ([
33: /* 0 */
34: function(module, exports, __webpack_require__) {
35:     __webpack_require__(1);
36:     module.exports = {
37:         a : true
38:     };
39: },
40: /* 1 */
41: function(module, exports) {
42:     module.exports = {
43:         b:true
44:     };
45: }
46: ]);

模块化分析

从第32行开始是一个module数组,结合我们a.js,b.js中的内容可以发现,我们自己的源代码中写的require都被webpack自动转化为'__webpack_require__(moduleID)'的形式,而moduleID则对应着它在数组中的下标位置,并且都被包装在了一个签名为(module,exports,__webpack_require__)的函数中。这个函数稍后会用来加载我们真正的模块内容。

modules数组作为参数传入到第1行的函数中执行,下面看一下第1-31行这个函数的功能。installedModules是一个jsObject对象,用来存放已经加载的modules,其数据结构为

{
    moduleId : {
            exports : {你的模块内容},
            loaded : boolean// 是否已加载,
            id : moduleId
    }
}

第5-22行是webpack-require的具体实现,通过第30行默认执行__webpack_require__(0)使整个模块化系统跑起来。那我们仔细对比可以发现,0所对应的module其实就是我们a.js文件,也就是webpack.config.js中所配置的entry文件,所以“入口”这个词的意义就体现出来啦。

第7-8行先从installModules找是不是存在moduleId这个模块呀,找到了就直接返回这个module的exports内容,找不到就新建一个空的模块内容

{
    moduleId : {
        exports : {},
        loaded : false,
        id : moduleId
    }
 }

然后放在installModules中,并通过函数第16-17行加载这个moduleId的内容,加载过程就是调用传入的modules中下标为moduleId的函数,所以webpack需要将我们的模块都包装成一个可以链接执行的函数。加载结束后,将loaded标为true,并返回module.exports。

结合我们的例子,整个流程就是,首先加载a.js模块(入口模块),由于在installModules找不到,就立马新建一个内容为空的模块,然后调用34-38去真正加载a.js模块,在发现a.js中依赖b.js模块,就立马去加载b.js模块(b.js模块的加载过程同a.js),待b.js加载完成后,再执行a.js中的后续过程,然后通过module.exports暴露自己模块的内容。


模块的循环依赖问题

进一步让我们来思考下,这个模块加载系统会不会也存在循环依赖的问题。循环依赖是指aModule依赖bModule的加载完成,同时bModule也依赖aModule的加载完成。这种问题在大多数模块加载系统中都存在,例如angularjs内置的模块加载系统,如果出现这种情况,它就会直接抛出一个 "cdep"的异常。

那我们大胆的来测试下,我们在b.js的头部添加require('./a.js')来构成循环依赖的条件,重新打包后,确实会抛出 “Module not found: Error: a dependency to an entry point is not allowed”的错误,但这个错误只是说不能require入口文件,还是没有很清晰的揭示循环依赖的问题。那我们就不依赖它,在其内部构建一个足以体现这个问题的循环依赖。我们在同一目录下新建一个c.js文件,内容随便写写好啦,但是记得require('./b.js'),譬如

require('./b.js');
module.exports = {
    c : true
};

同时b.js中的头部写入require('./c.js'),重新打包,发现一点问题都没有,再随便写一个html引入main.js,页面也是正常打开,没有一点错误,难道webpack已经解决了循环依赖的问题?其实不然,更准确的说,它只是解决了一部分,在某种情况下,依旧会出来这个问题。如图,

在第1步进行之前,如上所述,installModules已经插入了一个空的b模块内容,然后通过第2步require的回来之后,7-8行将这个空模块返回,就不需要再次调用__web_require(2)__产生循环。但是,由于你获得的是个空模块内容,假设你要操作另一模块中的某个内容或方法,必然会抛出 XX is undefined or XX is not a function 的错误。为了验证这点,我们将b.js和c.js中的内容稍微变化一下。

b.js

require('./c.js');

module.exports = function(string){
  console.log('log '+string+' from b');
};

c.js

var test = require('./b.js');

test("test");

module.exports = {
    c: true
};

打包正常,但是运行一下可以看到确实抛出了异常

这也验证了webpack确实没有完全解决循环依赖的问题,但是循环依赖问题也促使我们要保持一个思想,尽量的使模块间的依赖关系呈树状结构,这对于大型项目来说是很必要的。

日记本
Web note ad 1