RequireJs已经流行很久了,我们在项目中也打算使用它。它提供了以下功能:
初看起来并不复杂。
在HTML中,添加这样的<script>
标签:
<script src="/path/to/require.js" data-main="/path/to/app/config.js"></script>
通常使用requirejs的话,我们只需要导入requirejs即可,不需要显式导入其它的js库,因为这个工作会交给requirejs来做。
属性data-main
是告诉requirejs:你下载完以后,马上去载入真正的入口文件。它一般用来对requirejs进行配置,并且载入真正的程序模块。
config.js
中通常用来做两件事:
requirejs.config({
baseUrl: '/public/js',
paths: {
app: 'app'
}
});
requirejs(['app'], function(app) {
app.hello();
});
在paths
中,我们声明了一个名为app
的模块,以及它对应的js文件地址。在最理想的情况下,app.js
的内容,应该使用requirejs的方式来定义模块:
define([], function() {
return {
hello: function() {
alert("hello, app~");
}
}
});
这里的define
是requirejs提供的函数。requirejs一共提供了两个全局变量:
另外还可以把require
当作依赖的模块,然后调用它的方法:
define(["require"], function(require) {
var cssUrl = require.toUrl("./style.css");
});
前面的代码是理想的情况,即依赖的js文件,里面用了define(...)
这样的方式来组织代码的。如果没用这种方式,会出现什么情况?
比如这个hello.js
:
function hello() {
alert("hello, world~");
}
它就按最普通的方式定义了一个函数,我们能在requirejs里使用它吗?
先看下面不能正确工作的代码:
requirejs.config({
baseUrl: '/public/js',
paths: {
hello: 'hello'
}
});
requirejs(['hello'], function(hello) {
hello();
});
这段代码会报错,提示:
Uncaught TypeError: undefined is not a function
原因是最后调用hello()
的时候,这个hello
是个undefined
. 这说明,虽然我们依赖了一个js库(它会被载入),但requirejs无法从中拿到代表它的对象注入进来供我们使用。
在这种情况下,我们要使用shim
,将某个依赖中的某个全局变量暴露给requirejs,当作这个模块本身的引用。
requirejs.config({
baseUrl: '/public/js',
paths: {
hello: 'hello'
},
shim: {
hello: { exports: 'hello' }
}
});
requirejs(['hello'], function(hello) {
hello();
});
再运行就正常了。
上面代码exports: 'hello'
中的hello
,是我们在hello.js
中定义的hello
函数。当我们使用function hello() {}
的方式定义一个函数的时候,它就是全局可用的。如果我们选择了把它export
给requirejs,那当我们的代码依赖于hello
模块的时候,就可以拿到这个hello
函数的引用了。
所以:exports
可以把某个非requirejs方式的代码中的某一个全局变量暴露出去,当作该模块以引用。
但如果我要同时暴露多个全局变量呢?比如,hello.js
的定义其实是这样的:
function hello() {
alert("hello, world~");
}
function hello2() {
alert("hello, world, again~");
}
它定义了两个函数,而我两个都想要。
这时就不能再用exports
了,必须换成init
函数:
requirejs.config({
baseUrl: '/public/js',
paths: {
hello: 'hello'
},
shim: {
hello: {
init: function() {
return {
hello: hello,
hello2: hello2
}
}
}
}
});
requirejs(['hello'], function(hello) {
hello.hello1();
hello.hello2();
});
当exports
与init
同时存在的时候,exports
将被忽略。
我遇到了一个折腾我不少时间的问题:为什么我只能使用jquery
来依赖jquery, 而不能用其它的名字?
比如下面这段代码:
requirejs.config({
baseUrl: '/public/js',
paths: {
myjquery: 'lib/jquery/jquery'
}
});
requirejs(['myjquery'], function(jq) {
alert(jq);
});
它会提示我:
jq is undefined
但我仅仅改个名字:
requirejs.config({
baseUrl: '/public/js',
paths: {
jquery: 'lib/jquery/jquery'
}
});
requirejs(['jquery'], function(jq) {
alert(jq);
});
就一切正常了,能打印出jq
相应的对象了。
为什么?我始终没搞清楚问题在哪儿。
经常研究,发现原来在jquery中已经定义了:
define('jquery', [], function() { ... });
它这里的define
跟我们前面看到的app.js
不同,在于它多了第一个参数'jquery'
,表示给当前这个模块起了名字jquery
,它已经是有主的了,只能属于jquery
.
所以当我们使用另一个名字:
myjquery: 'lib/jquery/jquery'
去引用这个库的时候,它会发现,在jquery.js
里声明的模块名jquery
与我自己使用的模块名myjquery
不能,便不会把它赋给myjquery
,所以myjquery
的值是undefined
。
所以我们在使用一个第三方的时候,一定要注意它是否声明了一个确定的模块名。
如果我们不指明模块名,就像这样:
define([...], function() {
...
});
那么它就是无主的模块。我们可以在requirejs.config
里,使用任意一个模块名来引用它。这样的话,就让我们的命名非常自由,大部分的模块就是无主的。
可以看到,无主的模块使用起来非常自由,为什么某些库(jquery, underscore)要把自己声明为有主的呢?
按某些说法,这么做是出于性能的考虑。因为像jquery
, underscore
这样的基础库,经常被其它的库依赖。如果声明为无主的,那么其它的库很可能起不同的模块名,这样当我们使用它们时,就可能会多次载入jquery/underscore。
而把它们声明为有主的,那么所有的模块只能使用同一个名字引用它们,这样系统就只会载入它们一次。
对于有主的模块,我们还有一种方式可以挖墙角:不把它们当作满足requirejs规范的模块,而当作普通js库,然后在shim
中导出它们定义的全局变量。
requirejs.config({
baseUrl: '/public/js',
paths: {
myjquery: 'lib/jquery/jquery'
},
shim: {
myjquery: { exports: 'jQuery' }
}
});
requirejs(['myjquery'], function(jq) {
alert(jq);
});
这样通过暴露jQuery
这个全局变量给myjquery
,我们就能正常的使用它了。
不过我们完全没有必要这么挖墙角,因为对于我们来说,似乎没有任何好处。
在前面引用jquery的这几种方式中,我们虽然可以以模块的方式拿到jquery模块的引用,但是还是可以在任何地方使用全局变量jQuery
和$
。有没有办法让jquery完全不污染这两个变量?
首先尝试一种最简单但是不工作的方式:
requirejs.config({
baseUrl: '/public/js',
paths: {
jquery: 'lib/jquery/jquery'
},
shim: {
jquery: {
init: function() {
return jQuery.noConflict(true);
}
}
}
});
requirejs(['jquery'], function(jq) {
alert($);
});
这样是不工作的,还是会弹出来一个非undefined
的值。其原因是,一旦requirejs为模块名jquery
找到了属于它的模块,它就会忽略shim
中相应的内容。也就是说,下面这段代码完全没有执行:
jquery: {
init: function() {
return jQuery.noConflict(true);
}
}
如果我们使用挖墙角的方式来使用jquery,如下:
requirejs.config({
baseUrl: '/public/js',
paths: {
myjquery: 'lib/jquery/jquery'
},
shim: {
myjquery: {
init: function() {
return jQuery.noConflict(true);
}
}
}
});
requirejs(['myjquery'], function(jq) {
alert($);
});
这样的确有效,这时弹出来的就是一个undefined
。但是这样做的问题是,如果我们引用的某个第三方库还是使用jquery
来引用jquery,那么就会报“找不到模块”的错了。
我们要么得手动修改第三方模块的代码,要么再为它们提供一个jquery
模块。但是使用后者的话,全局变量$
可能又重新被污染了。
如果我们有办法能让在继续使用jquery
这个模块名的同时,有机会调用jQuery.noConflict(true)
就好了。
我们可以再定义一个模块,仅仅为了执行这句代码:
jquery-private.js
define(['jquery'], function(jq) {
return jQuery.noConflict(true);
});
然后在入口处先调用它:
requirejs.config({
baseUrl: '/public/js',
paths: {
jquery: 'lib/jquery/jquery',
'jquery-private': 'jquery-private'
}
});
requirejs(['jquery-private', 'jquery'], function() {
alert($);
});
这样的确可行,但是还是会有问题: 我们必须小心的确保jquery-private
永远是第一个被依赖,这样它才有机会尽早调用jQuery.noConflict(true)
清除全局变量$
和jQuery
。这种保证只能靠人,非常不可靠。
我们这时可以引入map
配置,一劳永逸地解决这样问题:
requirejs.config({
baseUrl: '/public/js',
paths: {
jquery: 'lib/jquery/jquery',
'jquery-private': 'jquery-private'
},
map: {
'*': { 'jquery': 'jquery-private'},
'jquery-private': { 'jquery': 'jquery'}
}
});
requirejs(['jquery'], function(jq) {
alert($);
});
这样做,就解决了前面的问题:在除了jquery-private之外的任何依赖中,还可以直接使用jqurey
这个模块名,并且总是被替换为对jquery-private
的依赖,使得它最先被执行。
在开发过程中,我们常常会创建很多个小js文件,每个文件一个module。这样在开发时比较方便,但在产品环境中,会导致浏览器下载很多次,性能比较慢。为了解决这个问题,requirejs提供了一个叫r.js的工具,可以把多个文件合并成一个或者少数几个。
这样自然而然就会有一个疑问:它们是怎么合并的?仅仅是把两个文件的内容原封不动的合并在一起吗?如果是这样的话,会有问题。
比如,我之前有两个js文件
a.js
define([], function() {
return "aaa";
});
b.js
define([], function() {
return "bbb";
});
它们合并以后,如果是这样的:
one.js
define([], function() {
return "aaa";
});
define([], function() {
return "bbb";
});
那我们引用这个文件时,到底用的是哪个模块呢?
Requirejs聪明的解决了这个问题。它并不是原封不动的合并,而是根据我们在使用每个模块时给的命名,自动加到每个模块的定义处。假设我们分别通过module a
和b
去引用这两个文件的话,上面合并后的代码实际是:
define('a', [], function() {
return "aaa";
});
define('b', [], function() {
return "bbb";
});
这样就算有再多的模块放在同一个文件中,我们也能准确的拿到某一个模块。
TODO: grunt的示例
当一个文件中定义了多个模块(或者如前一个例子中所示,多个模块文件合并在同一个文件之后),我们可能不太清楚一个文件中到底有多少个模块,也无法控制哪些模块是我们想用的。
requirejs又提供了一个叫bundles
的功能。代码如下:
require.config({
paths: {
mybundleSet: 'one'
},
bundles: {
mybundleSet: ['a', 'b']
}
});
这里我们在paths
中,通过mybundleSet
引用了one.js
文件。这个one.js
里包含了多个模块,所以在后面的bundles
配置中,我们又声明将使用其中的a
和b
两个模块。
如果one.js
中缺少a
或b
,代码会出错。如果有其它的模块,但没有在bundles
中声明,也是没有办法使用的。