最近学习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确实没有完全解决循环依赖的问题,但是循环依赖问题也促使我们要保持一个思想,尽量的使模块间的依赖关系呈树状结构,这对于大型项目来说是很必要的。