初涉模块化

早期的JavaScript发展初期只是为了少量的页面交互逻辑,且功能(逻辑)简单,代码量少,甚至于早期的Web是没有前端这个说法的,后端顺便一写JS。
随着时间发展,进入Web2.0时代,CPU等硬件性能的提升也使得浏览器性能得到了提升,很多页面交互逻辑迁移到了客户端(浏览器),加上新技术不断涌现(Ajax),JQuery等前端库层出不穷,代码量日益膨胀。

这时候JS作为动态语言的定位就显得捉襟见肘,没有类的概念,没有模块,简单的代码组织不足以驾驭如此大规模的代码。

模块

从最简单的开始:

function step1() {

};
function step2() {

};
step1();
step2();

这是上古时期的JS书写方式,亦或者初学JavaScript的新手书写JS的方法,当然也属于面向过程式的。

当逻辑交互增多,一个JS文件显然不够了,需要引入多个JS文件,并且这种书写的JS问题也很明显。

  1. 函数都是在global下定义,别人可以随意的修改操控这些全局函数,污染了全局变量
  2. 如果有人要是在另一个文件的人也定义了一个step函数会如何?会发生命名冲突

为了解决如上问题,对象的写法应运而生,可以把所有的模块成员封装在一个对象中。

var Moudle = {
    value1: 1,
    value2: 2,
    method1: function() {
        /*do something*/
    },
    method2: function() {
        /*do somethinlg*/
    }
}

调用时,只要保证模块名唯一即可。
当然,这并没有从根本上解决这个问题,外部依然可以随意修改内部成员。

Moudle.value1 = 10;

这样会产生安全问题。


后来又有人使用立即执行的函数表达式,也叫IIFE模式。

var Moudle = (function() {
    var a = 1;
    var b = 2;
    function method1() {
        console.log(a);
    };
    function method2() {
        console.log(b);
    };

    return {
        method1: method1,
        method2: method2
    }
})();

Moudle.method1();       // 1
Moudle.method2();       // 2

这样在模块外部无法修改我们没有暴露出来的变量、函数。
上述做法就是我们模块化的基础


AMD/CMD/CommonJS是什么?

CommonJS / Node.js

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。
为什么JS可以在服务器端运行就标志着模块化编程到来呢?因为,在浏览器下,没有模块也不是很致命的问题,毕竟网页程序的复杂度有限。但对于服务端就不一样了,如果没有模块与操作系统底层或者其他应用程序互动,根本无法编程。

Node.js是CommonJS规范的实现

node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。

  1. 根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块获取,除非定义为global对象的属性。
  2. 模块输出:模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入对象。
  3. 加载模块:加载模块使用require()方法,该方法获取一个文件并执行,返回文件内部的module.exports对象。

模块定义a.js

var myModule = {
    a: 10,
    sayA: function() {
        return this.a;
    }
}
module.exports = myModule;

加载模块

var myModule = require("./a.js");
console.log(myModule.sayA());    // 10

CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}
require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,唯一的导出口;module对象就代表模块本身。


AMD / require.js
背景

在CommonJS规范的Node.js诞生后,服务端的模块化概念已经形成,很自然的,大家就想要客户端(浏览器)模块。而且最好两者都兼容,一个模块不用改,在两端都可以运行。
但是有一个缺陷,使得CommonJS规范不适用于浏览器环境。

var myModule = require("./a.js");
console.log(myModule.sayA());    // 10

第二行的sayA()方法,在第一行的a模块之后运行,必须得等到a.js加载完成后才能使用方法,如果无法加载或者加载没有完成那就会造成阻塞,直到a.js加载完成后续的代码才会执行。即:它们是同步的

同步加载对于服务器端不是难事,因为资源都存放在服务器端,即用即取,完全可以同步加载,等待的时间就是服务器的硬盘读取时间,这个时间肯定比浏览器快的多。
受到网速限制,如果等待很长时间都未能加载,页面就会“假死”,这对用户来说是相当不友好的。

因此,基于这样的特殊背景,浏览器端的模块,不能采用同步加载,只能采用异步加载,这就是AMD规范的诞生。

require.js是AMD规范的实现

AMD介绍

AMD(Asynchronous Module Definition),中文名异步模块定义,是一个在浏览器端模块化开发的规范。

由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎的RequireJS,实际上AMD是requireJS在推广过程中对模块定义的规范化的产出。

requireJS解决了什么问题呢?

  • 实现JS文件的异步加载,避免网页失去相应。
  • 管理模块之间的依赖性,便于代码的编写和维护。
  <script src="1.js"></script>
  <script src="2.js"></script>
  <script src="3.js"></script>
  <script src="4.js"></script>
  <script src="5.js"></script>
  <script src="6.js"></script>

这样的代码,很多人都应该写过,繁多复杂,并且,还要体现依赖性,如照这样写,说明1.js必定被2,3,4,5,6所依赖。如果依赖关系在复杂一点,可读性,维护将变得很差。

还有一个缺点,就是script标签加载时的阻塞问题。

使用require.js,首先需要引入它。
requireJS

    <script src="./lib/require.js" defer async="true"></script>

当然,加载这个文件也可能使网页失去响应,要么放在body下方;要么写上async属性表明这个文件需要异步加载,避免网页失去响应,IE不支持这个属性,所以写上defer。

加载require.js之后,下一步就是加载自己的代码了,假定我们的代码文件存放在app下,那么写成这样就好了。

    <script src="./lib/require.js" defer async="true" data-main="./app/a.js"></script>

data-main属性的作用是,指定网页程序的主模块,如上代码,就是app目录下的a.js。这个文件会第一个被require.js加载。由于require.js默认的文件后缀名的js,所以可以把main.js简写成main。

主模块写法

a.js,把它称为“主模块”,意思是整个网页的入口代码,类似C语言里的main()函数,所有代码都从这儿开始运行。

怎么写a.js?

如果你的a.js不依赖任何模块,那么直接写就行了。

alert("加载成功!");

但很显然存在模块,并且主模块a.js依赖于其他模块,比如你的tab模块、轮播图模块等等。
这时就要用到AMD规范定义的require()函数。

注意,这个不是CommonJS里的require函数了。

  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){

    // some code here

  });

require()函数接受两个参数。

  • 第一个参数:表示所依赖的模块,例子中就是['moduleA','moduleB','moduleC'],即主模块依赖这三个模块;
  • 第二个参数:是一个回调函数,只有前面的所依赖的模块加载完成,回调函数才会被调用,加载的模块会以参数的形式传入函数,从而在回调函数中使用这些模块。

require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

注意: 默认情况下,require.js会假设这三个模块与主模块目录相同,关于目录指定,可以查看官方文档

假设现在主模块依赖moduleA、moduleB、moduleC这三个模块,那么主模块可以这样写:


require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
    var sum = moduleA.str + moduleA.str + moduleC.str;
    console.log(sum);       // " module A module A module C"
});

在CommonJS中,模块的出口完全靠module.exports,在requireJS中,我们又如何定义模块呢?

在之前的例子中,A,B,C三个模块存放在app目录下,require.js假定这三个模块与主模块(a.js)在同一个目录中,然后就可以自动加载他们了。

RequireJS以一个相对于baseUrl的地址来加载所有的代码。 页面顶层<script>标签含有一个特殊的属性data-mainrequire.js使用它来启动脚本加载过程,而baseUrl一般设置到与该属性相一致的目录。

因为我们显式指定了data-main属性,那么baseUrl就会与data-main属性所在目录一制,自动的从目录内加载文件。

当然,我们也可以对模块的加载行为进行自定义。require.config()就写在主模块(a.js)的头部。参数就是一个对象,这个对象的paths属性指定在各个模块的加载路径。

假设现在有两个依赖模块在lib目录下,那么我们可以在主模块头部直接指定每个模块的路径。

require.config({
    paths: {
        "moduleA": "../lib/moduleA",
        "moduleB": "../lib/moduleB",
        "moduleC": "moduleC",
    }
})

require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
    var sum = moduleA.str + moduleA.str + moduleC.str;
    console.log(sum);       // " module A module A module C"
});

注意,不要写成../lib/moduleA.js

如果写成了,解析目录就会变成你的HTML页面所在的直接目录下。

当然,你也可以指定主机名,也就是直接指定它的绝对URL。

  require.config({

    paths: {

      "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"

    }

  });

require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。

模块如何定义
require.js加载的模块,采用AMD规范,也就是说,模块必须按照AMD的规范来写。

模块必须采用特定的define()函数来定义,如果一个模块不依赖其他模块,那么直接定义了define()函数之中。

比如我们新建一个math模块 —— math.js。

define(function() {
    var add = function(x,y) {
        return x+y;
    };
    return {
        add: add
    };
});

加载方法:

require(["math"],function(math) {
    console.log(math.add(1,1));     // 2
});

如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。

// math.js
define(["myLib"],function(myLib) {
    function foo() {
        console.log(myLib.num);
    }
    return {
        foo: foo
    }
});


主模块a.js依赖math模块,math模块依赖myLib模块

AMD中文网

CMD / Sea.js

CMD(Common Module Definition通过模块定义),CMD规范是国内发展出来的,比如AMD有个requireJS,CMD有个SeaJS,SeaJS要解决的问题

Sea.js推崇一个模块一个文件,遵循统一的写法。

  • 一个文件一个模块,所以经常就用文件名作为模块id
  • CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写。

CMD是懒加载,虽然会一开始就并行加载js文件,但是不会执行,而是在需要的时候才执行

define
define(id?, deps?, factory);

参数factory也有三个参数

function(reuqire, exports, module);
  • require:一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口。
  • exports:一个对象,用来向外提供模块接口。
  • module:上面存储了与当前模块相关联的一些属性和方法。
// 定义模块  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

// 加载模块
seajs.use(['myModule.js'], function(my){

});

总结:

规范实现

CommonJS / Node.js
AMD / Require.js
CMD / Sea.js

针对端

AMD / CMD主要针对浏览器端
CommonJS主要针对服务端

加载区别
// CMD
define(function (require, exports, module) {
    var a = require("./a");
    a.doSomething();
    var b = require("./b");
    b.doSomething();
})

// AMD
define(["./a", "./b"], function (a, b) {
    a.doSomething();
    b.doSomething();
})
主要区别:执行时机 处理不同,注意不是加载的时机或者方式不同。

CMD推崇依赖就近,AMD推崇依赖前置
CMD是延迟执行,AMD是提前执行
加载模块时,都是异步加载

  • AMD因为依赖前置,当所有模块加载好后,就会立即执行,进入require回调函数,执行主逻辑。因为异步原因,模块加载和执行不一定一致,如a模块和b模块,b模块先加载完成,那么会先执行b模块,但是,主逻辑一定是在所有依赖模块加载后才执行的。

  • CMD因为依赖就近,模块加载好后并不执行,只是下载而已,当所有依赖模块加载完成后进入主逻辑,遇到require语句时才会执行对应模块,这样模块执行顺序和书写顺序一致。


js模块化编程之彻底弄懂CommonJS和AMD/CMD!
AMD/CMD/CommonJs到底是什么?它们有什么区别?
前端模块化开发的价值
MODULE?
JavaScript AMD 与 CMD 规范

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

推荐阅读更多精彩内容