JavaScript模块化

208 阅读3分钟

如何理解模块化?

模块化其实是一种约束或者说是一种规范。在JavaScript中每个js文件都可以看作是一个模块,每个模块都是通过某种固定的方式倒入,按照某种固定的方式报漏出或抛出模块中的内容。

传统的js 开发中的问题

我们都知道JavaScript这门语言诞生的初衷仅仅是作为一门脚本语言运行在浏览器中,但是随着JavaScript的发展,近些年来JavaScript运用越来越广泛,不单单是作为浏览脚本语言使用,就连服务端也开始使用起来(NodeJs)。这就会带来很多问题,在传统的开发中,我们如果想要在页面中引入JavaScript代码,往往是通过下面的方式

<script src="zepto.js"></script>
<script src="jhash.js"></script>
<script src="fastClick.js"></script>
<script src="iScroll.js"></script>
<script src="underscore.js"></script>
<script src="handlebar.js"></script>
<script src="datacenter.js"></script>
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>

类似于上面的这种引入方式就会造成以下几个问题:

  • 难维护: 如果一个页面比较庞大,引入的js文件比较多,就会造成依赖的文件比较繁琐,特别不容易维护
  • 污染全局变量: 按照上面的这种方式,所有的js中定义的变量都会挂载到window对象上。
  • 变量重名: 不同文件中的文件如果变量名相同,后面的就会覆盖前面的。
  • 引入顺序: 如果多个文件存在依赖关系,必须要保证加载顺序。如a.js中引用了b.js中的变量,必须是b.js先引入,不然直接报错。

模块化的解决方案

为了解决上面存在的问题,于是广大开发者就开始考虑是不是可以设计出一种规范来避免这些问题。下面看个例子

// moduleA.js

var a = [1,2,3,4,5];
// moduleB.js
var b = a.concat([5,6,7,8]);
// moduleC.js
var c = b.join('-');
// index.js
console.log(a);
console.log(b);
console.log(c);

上面的代码中4个文件,不同的依赖关系,moduleB依赖moduleA,moduleC依赖moduleB,index依赖了moduleA、moduleB、moduleC,按照传入的引入方式必须要按照它们的依赖关系引入,否则就会出错

<script src="./moduleA.js"></script>    
<script src="./moduleB.js"></script>
<script src="./moduleC.js"></script>
<script src="./index.js"></script

1、使用闭包和立即执行函数

早期开发者们为了解决上面的全局变量污染和变量重名问题,使用了闭包和立即执行函数,上面的例子我们使用必包和立即执行函数来实现如下

// moduleA.js
var mudule_a = (function() {
    return {
        a: [1,2,3,4,5]
    }
})();
// moduleB.js
var module_b = (function (mudule_a) {
    return {
        b: module_a.a.concat([5,6,7,8])
    }
})(module_a);
// moduleC.js
var mudule_c = (function(module_b){
    return {
        c: b.join('-')
    }
})(module_b)
// index.js
;(function(mudule_a, mudule_b, mudule_c){
    console.log(mudule_a.a);
    console.log(mudule_b.b);
    console.log(mudule_c.c);
})(mudule_a, mudule_b, mudule_c)

这种方法下仍然需要在入口处严格保证加载顺序:

<script src="./moduleA.js"></script>    
<script src="./moduleB.js"></script>
<script src="./moduleC.js"></script>
<script src="./index.js"></script

这种方式的意义在于

  • 各个js文件之间避免了变量命名重复,避免全局污染。
  • 模块与外部的连接通过立即执行函数穿参,语义化更好,清晰地知道有哪些依赖。

但是在引入js文件上仍然需要按照依赖关系的顺序引入。

2、使用模块化开发插件

有了上面的闭包和立即执行函数之后,一些开发人员开始利用闭包和立即执行函数开发插件。

// demo.js

;(function($){
    var LightBox = function(){
        // ...
    };
    
    LightBox.prototype = {
        // ....
    };
    
    window['LightBox'] = LightBox;
})($);

使用的时候

<script src="demo.js"></script>
<script type="text/javascript">
    var lightbox = new LightBox();
</script>

但是上面的这种插件的开发方式并没有做什么根本的改动。也没有完全解决实际问题,比如文件的依赖顺序。

3、CommonJS实现模块化

CommonJS是一种规范,这种规范来源于NodeJS。关于CommonJS的详细规范,可以阮一峰老师的这篇文章CommonJS规范,这里我简单的使用CommonJS来描述一下上文中的例子

// moduleA.js
const a = [1,2,3,4,5];
module.exports = {
    a
}
// moduleB.js
const module_a = require('./moduleA');
const b = module_a.a.concat([5,6,7,8]);
module.exports = {
    b
}
// moduleC.js
const module_b = require('./moduleB');
const c = module_b.b.join('-');
module.exports = {
    c
}
// index.js
const module_a = require('./moduleA');
const module_b = require('./moduleB');
const module_c = require('./moduleC');
console.log(module_a.a);
console.log(module_b.b);
console.log(module_c.c);

CommonJS的特点

  • 通过require对象引入模块
  • 通过module.exports导出模块
  • 所有文件同步加载
  • 缓存机制,只要require一次,这个模块就会被缓存
  • 一定是运行在Node环境上的
  • 模块编译本质上是沙箱编译
  • 不需要关注文件的引入顺序

沙箱编译: require进来的js模块会被Module模块注入一些变量,使用立即执行函数编译,看起来就好像:

(function (exports, require, module, __filename, __dirname) {
    ...
})();

上面的代码其实类似于这样:

// moduleA.js
const a = [1,2,3,4,5];
module.exports = {
    a
}

// moduleB.js
const module_a = require('./moduleA');
const b = module_a.a.concat([5,6,7,8]);
module.exports = {
    b
}

// 等价于
let module_a = (function(){
   module.exports = [1,2,3,4,5];
   return module.exports
})()

看起来require和module好像是全局对象,其实只是闭包中的入参,并不是真正的全局对象。

未完待续...