require.js使用详细说明

JavaScript模块化

当前的网页已经开始逐渐变成"互联网应用程序", 在网页中插入的JavaScript代码越来越庞大, 越来越复杂. 网页越来越像桌面程序了, 需要一个团队分工协作, 进度管理, 单元测试等等......开发者不得不使用软件工程方法, 管理网页的业务逻辑.

JavaScript模块化编程, 已经成为一个迫切的需求. 理想情况下, 开发者只需要实现核心的业务逻辑, 其他都可以加载别人写好的模块.

但是JavaScript不是一种模块化的编程语言, 它不支持"类", 更不用说"模块"了.

JavaScript社区做了很多努力, 在现有的运行环境下, 实现"模块"的效果. 这里总结了当前JavaScript模块化编程的最佳实践, 说明如何投入使用.

原始写法

模块就是特定功能的一组方法.
只要把不同的函数简单的放在一起, 就算是一个模块.

function a1(){
    // code here
}
function a2(){
    // code here
}

上面的函数a1()和a2(), 组成一个模块, 使用的时候, 直接调用就行了.

但是这样的做法有一个很明显的缺点: "污染"了全局变量, 无法保证不与其他模块发生变量名冲突, 而且模块成员之间看不出直接关系.

对象写法

为解决上面的问题, 可以把模块写成一个对象, 所有的模块成员都放到这个对象里面.

var module = new Object({
    _count: 0,
    a1: function(){},
    a2: function(){},
});

上面的函数a1()和a2(), 都封装在module对象中, 使用的时候, 直接调用这个对象的属性.

module.a1();

但是, 这样的写法会暴露所有模块成员, 内部状态也可以被外部随意改写. 比如, 外部代码可以直接改变内部的_count变量的值, 也可以更改a1函数的定义.

module._count = 13;

立即执行函数写法

为了解决上面的问题, 可以使用立即执行函数的写法, 来解决不暴露私有成员的目的.

var module = (function(){
    var _count = 0;
    var a1 = function(){};
    var a2 = function(){};
    return {
        a1: a1,
        a2: a2
    }
})();

使用上面的写法, 外部代码无法读取到内部的_count变量.
这种写法就是JavaScript模块的基本写法. 但还需要再对这种写法进行一些加工才能算是真正的模块化编程.

放大模式

如果一个模块很大, 必须分成多个部分, 或者一个模块需要继承另一个模块, 这时就有必要采用"放大模式".

var module = (function(m)(){
    m.a3 = function(){};
    return m;
})(module);

上面的代码为module模块添加一个新方法a3, 然后返回新的module模块.

宽放大模式

在浏览器环境中, 模块的各个部分通常都从网络中获取的, 获取的这部分资源你可能无法确定何时以及哪个文件先加载到, 如果采用上面的写法, 第一个执行的部分有可能加载一个不存在的空对象, 这时就要采用"宽放大模式".

var module = (function(m)(){
    m.a3 = function(){};
    return m;
})(window.module || {});

与"放大模式"相比, "宽放大模式"就是"立即执行函数"的参数可以是空对象.

输入全局变量

独立性是模块的重要特点, 模块内部最好不与程序的其他部分直接交互.
为了在模块内部调用全局变量, 必须显式地将其他变量输入模块.

var module = (function($, backbone){
    // code here
})(jQuery, Backbone);

上面的module模块需要使用jQuery和Backbone库, 就把这两个库当作参数输入module, 这样做除了保证模块的独立性, 还使得模块之间的依赖关系变得明显.

模块规范

想一下, 为什么模块很重要?

因为有了模块, 我们就可以更方便地使用别人的代码, 想要什么功能, 就加载什么模块.

但是这样的做得有一个前提条件, 就是大家都必须以同样的方式编写模块, 否则就会乱套.

目前, 通行的JavaScript模块规范共有两种: CommonJS和AMD(CMD与AMD很相似, 就不介绍, 有需要的, 可以去了解一下sea.js).

CommonJS

2009年, 美国程序员Ryan Dahl创造了Node.js项目, 将JavaScript语言用于服务器端编程.这标志"JavaScript模块化编程"正式诞生. 因为在浏览环境下, 没有模块也不是特别大的问题, 毕竟网页程序的复杂性有限; 但是在服务器端, 一定要有模块, 与操作系统和其他应用程序互动, 否则根本没法编程.

Node.js的模块系统, 就是参照CommonJS规范实现的. 在CommonJS中, 有一个全局性方法去require(), 用于加载模块. 假到有一个数学模块math.js, 就可以像下面这样加载:

var math = require('math');

然后, 就可以调用模块提供的方法了

var math = require('math');
math.sub(3,2);

本文主要讲浏览器端的JavaScript编程, 不涉及Nodejs, 所以对CommonJS不做过多的介绍, 有兴趣的话可以去搜索下CommonJS规范.

在这里大家只要晓得, require()用于加载模块就行了.

浏览器环境

有了服务器模块后, 大家可能会想要让客户端也有模块. 而且最好两者能够兼容.

但是, 由于一个重大的局限, 使得CommonJS规范不适用于浏览器环境, 还是上一节的代码, 如果在浏览器中运行, 会是一个很大的问题.

var math = require('math');
math.sub(3,2);

第二行的math.sub(3, 2), 在第一行require('math')之后运行, 因此必须等到math.js加载完成, 也就是说, 如果加载时间过长, 整个应用可能都停在那里.

这对服务器来说, 不是一个问题, 因为所有的模块都存放在本地硬盘中, 可以同步加载完成, 等待时间也就是硬盘的读取时间. 但, 对于浏览器来说, 这却是一个很大的问题, 因为模块都放在服务器端, 等待时间完成取决于网速的快慢, 可能要等很长时间, 浏览器处于"假死"状态. 因此, 浏览器端肯定是不可以用"同步加载"的方式, 只能采用"异步加载". 这也就是AMD规范诞生的背景.

AMD

AMD是"Asynchronous Module Definition"的缩写, 即"异步模块定义'. 它采用异步方式加载模块, 模块的加载不影响它后面语句的执行. 所有依赖这个模块的语句, 都定义在一个回调函数中, 等加载完成之后, 这个回调函数才会运行.

AMD也采用require()语句加载模块, 但是不同于CommonJS, 它要求两个参数:

require([module], callback);

第一个参数[module], 是一个数组, 里面的成员就是要加载的模块, 第二个参数callback, 则是加载成功之后的回调函数, 如果前面的代码改写成AMD形式, 就是下面这样的:

require(['math'], function(math){
    math.sub(3, 2);
});

math.sub()与math模块加载不是同步的, 浏览器不会发生"假死", 很显然, AMD比较适合浏览器环境.

目前, 主要有两个JavaScript库实现了AMD规范: require.js和curl.js.

下面对require.js进行详细的介绍, 如果对curl.js有兴趣的话, 大家可以自行去问问度娘.

require.js加载模块

为什么要使用require.js?

最初, 所有的JavaScript代码都写在一个文件里面, 只要加载一个文件就够了. 可是后来, 代码越来越多, 一个文件显然不行, 必须拆分, 依次加载. 下面的网页代码, 你肯定不会陌生:

<script type="text/javascript" src="a1.js"></script>
<script type="text/javascript" src="b1.js"></script>
<script type="text/javascript" src="c1.js"></script>
<script type="text/javascript" src="d1.js"></script>
<script type="text/javascript" src="e1.js"></script>
<script type="text/javascript" src="f1.js"></script>
<script type="text/javascript" src="g1.js"></script>
<script type="text/javascript" src="h1.js"></script>

上面的代码依次加载多个js文件, 但这样的写法有很大的缺陷:

  • 加载js文件时, 浏览器会停止网页渲染, 加载文件越多, 网页失去响应的时间就会越长.
  • 由于js文件之间是有依赖关系的, 必须严格保证加载顺序. 依赖性最大的模块一定要最后加载, 当依赖关系很复杂的时候, 代码的编写和维护会变得很困难.

require.js就是为了解决这些问题而产生的.

  • 实现js文件的异步加载, 避免网页失去响应.
  • 管理模块之间的依赖性, 便于代码的编写和维护.

加载require.js

在官网下载最新版的require.js, 下载地址: http://www.requirejs.org.

下载后, 假定把它放在js/libs目录下, 用下面的代码加载:

<script type="text/javascript" src="js/libs/require.js"></script>

在加载 这个文件本身也可能因为网络或服务器问题, 不能成功加载, 而造成网页失去响应. 解决的方法有两个, 一个是把它放在网页的底部加载, 另一个是写成下面这样:

<script type="text/javascript" defer async="true" src="js/libs/require.js"></script>

async属性表明文件需要异步加载, 避免网页失去响应. IE不支持这个属性. 只支持defer, 所以把defer也写上.

自定义主模块

加载require.js后, 就要加载自己的代码了. 假设main.js是你的主"模块", 也放在js/libs目录下, 那么只需要写成下面这样的:

<script type="text/javascript" src="js/libs/require.js" data-main="js/libs/main.js></script>

data-main属性的作用是, 指定网页程序的主模块. 在上例中, 就是js/libs目录下的main.js. 这个文件会第一个被require.js加载, 并且文件具有最高优先级的调用. 由于require.js默认的文件后缀名是.js, 所以可以把main.js简写成main.

main.js是"主模块", 意味着是整个网页的js入口代码, 有点像C语言的main()函数, 所有代码都从这儿开始运行.

如果我们的代码不依赖任何其他模块, 那么可以直接写入JavaScript代码.
要这么写, 也就没法有必要要用require.js. 实际的情况是, 主模块依赖于其他模块, 这就需要使用AMD规范定义的require函数了.

//main.js
require(['ModuleA', MmoduleB', 'ModuleC'], function(modulea, moduleb, modulec){
    // some code here;
});

require函数有两个参数, 其中:

  • 第一个参数, 是一个数组, 表示所依赖的模块.即"ModuleA", "ModuleB, "ModuleC".
  • 第二个参数, 是一个回调函数, 当指定的所有模块都加载成功后, 才会被调用. 加载的模块以参数形式传入该函数, 从而在回调函数中使用这些模块.

这样, require异步加载ModuleA, ModuleB, ModuleC, 浏览器不会失去响应; 指定回调函数, 只有当所有模块都加载成功才会运行, 解决了js依赖性的问题.

假设, 主模块依赖jquery, underscore和backbone三个模块, main.js可以这样写:

require(['jquery', 'underscore', 'backbone'], function($, _underscore, Backbone){
    // some code here;
});

自定义加载

默认情况下, require.js假设你的模块与main.js在同一目录, 文件名分别为jquery.js, underscore.js和backbone.js, 然后自动加载.

使用require.config方法, 我们可以自定义对模块加载的行为. require.config写在主模块(main.js)的顶部. 其参数是一个paths对象, 指定各个模块的加载路径, 如下所示:

require.config({
    baseUrl: 'static/js',
    debug: true,
    paths: {
        jquery: 'jquery-1.8.3.min',
        underscore: 'underscore.min',
        backbone: 'backbone.min'
    }

});

上面的代码表明你的三个模块文件与main.js在同一个js目录里, 如果这些模块在其他目录, 如js/lib目录, 则有两种写法, 要么直接指定基目录baseUrl. 当标明data-main属性但不特别配置baseUrl时, 根目录baseUrl的值默认为data-main指定的文件所在的目录, 当data-main和baseUrl都未指定时根目录的值默认为加载require.js的html文件所在目录, 否则将以配置的baseUrl作为根目录.

require.config({
    baseUrl: 'static/js',
    paths: {
        jquery: 'lib/jquery-1.8.3.min',
        underscore: 'lib/underscore.min',
        backbone: 'lib/backbone.min'
    }

});

require.config({
    baseUrl: 'static/js/lib',
    paths: {
        jquery: 'jquery-1.8.3.min',
        underscore: 'underscore.min',
        backbone: 'backbone.min'
    }

});

如果某个模块在另一台主机上, 也可以直接指定它的网址, 如:

require.config({
    paths: {
        jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min',
    }
});

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

加载AMD规范的模块

require.js加载的模块必须采用AMD规范, 具体来说, 模块必须采用define函数来定义.

- 定义简单键值对
如果模块不依赖于任何模块, 同时只是传递一些简单的name/value, 那么只需传递一个原始的对象给define().

// inside file demo.js
define({
    color: "black",
    size: "32"
});

- 定义函数

// inside file demo.js
define(function(){
    return {
        color: "black",
        size: "32"
    }
});

- 定义带依赖的函数
如果模块有依赖, 那么第一个参数应该是依赖名称的数组集合, 第二个参数应该是一个定义的函数. 一旦所有的依赖都已经加载好, 那么将调用这个函数定义的模块. 这个函数应该返回一个定义的模块的对象. 这些依赖将作为参数传递给定义的参数, 同时按照依赖顺序在参数中列出来.

//demo.js
define(['./cart', '.inventory'], function(cart, inventory){
    return {
        color: 'blue',
        size: 'large',
        addToCart: function(){
            inventory.decrement(this);
            cart.add(this);
        }
    }
});

在上述案例中, demo模块已经创建, 它依赖于cart和inventory两个模块. 上述函数调用中指定了两个参数, cart和inventory, 这些代表了cart和inventory模块, 上述函数直到cart和inventory模块加载完成后才会被调用, 它接收模块作为cart和inventory参数.

- 定义一个模块作为一个函数
模块没有必要一定要返回对象. 函数中任何返回值都是允许的. 下面是一个返回一个函数作为它的模块定义的模块示例:

define(['my/cart', 'my/inventory'], function(cart, inventory){
    return function(title){
        return title ? (window.title = title) : inventory.storeName + "" + cart.name;
    }
});

- 加载非AMD规范的模块
理论上, require.js加载的模块必须符合AMD规范, 即用define函数定义的模块. 但实际情况是, 虽然已经有一部分流行的函数库(如jQuery)符合AMD规范, 更多的库并不符合. 那么require.js是否能够加载非规范的模块呢? 当然可以, 加载非规范模块前, 要先用require.config方法, 定义它们的一些特征.

例如, 上面的三个模块: jquery.js, underscore.js和backbone.js, 其中, jQuery.js符合规范, 而underscore和backbone这两个库不符合. 如果要加载它们的话, 必须先定义它们的特征:

require.config({
    shim: {
        'underscore': {
            exports: '_'
        },
        'backbone': {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone',
        }
    }
});

require.config接受一个配置对象shim, 专门用来配置不兼容的模块. 具体来说, 每个模块要定义:

  • exports值(输出的变量名), 表明这个模块外部调用时的名称.
  • deps数组, 表明该模块的依赖性

比如, jQuery插件可以这样定义:

require.config({
    shim: {
        'underscore': {
            exports: '_'
        },
        'backbone': {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone',
        },
        'jquery.scroll': {
            deps: ['jquery'],
            exports: 'jQuery.fn.scroll'
        }
    }
});

本文只是介绍require.js的简单用法, 若想对require.js有更深入的了解, 可以去github中查看源码及官方文档.

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

推荐阅读更多精彩内容