【翻译】RequireJS 的基本原理

本文翻译来源:http://www.telerik.com/blogs/requirejs-fundamentals
此翻译如有冒犯之处,请联系我 zshaojia@gmail.com 进行删除,且本文不会用于商业用途。

如果你是一个 JavaScript 开发者,也许会见过各种各样的有关于 JavaScript 的模块。模块化开发是JavaScript的一个很热门的话题,而且会成为潮流。事实上,如何解决这个问题对于你的代码编写是最基本的。

为什么使用模块

JavaScript 是一个很缺乏模块化开发的脚本语言

哇!那这意味着什么?为啥缺乏模块化开发?

如果你第一时间想到这,很好你站在一个很好的队伍。Derick Bailey 对于模块化这个概念作了如下诠释:

模块化就是将一组有关联性的东西封装或打包起来,它里面包含了对象、方法、变量,或者其它任何东西。创建模块封装展示它统一的管理方式,我们可以将一些小功能打包起来,以方便放在其它大的模块里使用。

根据这个想法:在 .NETJava,你可以使用类 (classes) 创建一个项目,之后你得到一个编译好的模块,这个模块包含你已声明类。现在你需要在其它项目使用这个模块,那它非常容易地使用,只需将它关联或引入项目里即可。这就是模块封装的概念,但模块在JavaScript 世界里是完全缺失的。

引起这个问题,大致有一下两点:

  1. JavaScript 一直没有创建模块的语法
  2. JavaScript 没有一个语法让你加载一个已定义的模块,你不得不浪费时间去管理那些乏味的 script 标签名和全局依赖引用

创建模块

我们来看下这个问题通常是如何产生的。假设我们有个简单的应用需要使用到 color palette widget。功能是在 color palette 移动选择器去改变页面的颜色。

更多有关于 Color Picker widgets 有趣的例子,我没有全部展现,可以参考这里

我们看下在 JavaScript 中使用的场景。

Color Experiments Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// open a document ready function
$(function () {

// function that just changes the background color
var setBackground = function (color) {
$(document.body).css("background-color", color);
}

// function to handle the pallete color selection
var setColor = function (e) {
// the color object contains all the hex, rgba, and hsl
// conversions and utilities
var color = e.sender.color().toBytes();

// set the color
setBackground(e.value);
};

// select and create the color pallete
var colors = $("#colors").kendoFlatColorPicker({
change: setColor,
value: "#fff"
}).getKendoFlatColorPicker();

});

当我看到上面的代码,我意识到可以将它抽取成两个不同的对象.

  1. 一个用于改变页面颜色的公共方法 utility
  2. 一个 color palette 的选择事件

JavaScript 中,可以有几个方法去创建一个实体,但最基本的方法是使用 Immediately Invoked Function Expression (IIFE),会给你返回一个对象。我们就按照这个思路开始写 utility 方法去改变页面背景的颜色。

开始创建一个简单的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 变量声明在方法的外面是全局的变量
// 创建了一个 `APP` 的名称空间
var APP = window.APP || {};
// use a function that executes right away and assign
// whatever it returns to the utils variable
APP.utils = (function() {
// return an object
return {
// declare the function to change the background color
setBackground: function(color) {
$(document.body).css("background-color", color);
}
};
}());

我意识到你可能不太喜欢在一个对象里只用到一个操作,但它对于简化总体的想法很有帮助,稍微忍受下我。现在我们有了 utils 对象,它封装好 setBackground 方法和其它可能封装好的其他方法,我们只需这样使用它

1
2
// set the background color to blue
APP.utils.setBackground("#336699");

下一步,我们可以封装 color palette widget 为一个模块,这个模块需要访问到 utils 对象,固需要使用到一样的名称空间 APP

模块化 The Palette

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// create the color palette module off the app namespace
// all that we need to return out of this function is an instance
// of the color palette widget
APP.palette = (function() {

// this function is private and not available to utils
// function to handle the pallete color selection
var setColor = function (e) {
// the color object contains all the hex, rgba, and hsl
// conversions and utilities
var color = e.sender.color().toBytes();

// set the color
APP.utils.setBackground(e.value);
};

// select and create the color pallete
var colors = $("#colors").kendoFlatColorPicker({
change: setColor,
value: "#fff"
}).getKendoFlatColorPicker();

// just return the entire widget instance
return colors;
}());

现在代码模块化已经完成,上面的代码相当的累赘,我们可以将它们每个模块抽取为一个文件,即下面两个文件:

  1. utils.js
  2. palette.js

在页面上引用这两个文件即可

1
2
<script src="/Scripts/app/utils.js"></script>
<script src="/Scripts/app/palette.js"></script>

我们不需任何内嵌的脚本,因为只要它们在页面包含我们的模块和模块在执行所关联依赖的模块即可,但这样的处理方式还是存在很大的缺陷。

  1. utils.js 文件必须关联在第一位,因为其他模块依赖到它。
  2. 这两个文件已引入了,但只是一个无效果的页面,想象一下你到底需要引入多少个文件在一个完整的企业应用里。
  3. 你可能会使用如 ASP.NET MVC Bundling 编译工具,但当你的应用还需要你引入其他的模块,而这些模块又依赖其他的模块时。在你的脑袋里可以一直保持着这种变化,但你的APP也会变得臃肿起来,你会发现它越来越难管理。

对于上面那个问题,最终 RequireJS 出现了

RequireJS 的核心思想是允许你指定一个 JavaScript 文件定义哪些是依赖文件,这样你就可以很确定当你的代码执行时,那些依赖文件是已经引入的。

安装

RequireJS 只是一 JavaScript 文件,它根本解决了 JavaScript 文件之间依赖的问题。你可以在 RequireJS 网站下载,或者使用自己的包管理工具下载。我使用 ASP.NET 工作,所以我用 NuGet 获得它。

1
PM> Install-Package RequireJS

下载后,会得到2个文件在下载的文件夹里,我们只需 require.js 这个文件即可。其它文件如 r.js 是串联和缩减你的文件,我们会简单说下它。

通常你会看到 RequireJS 项目结构,所以你会把你的应用放到一个 app 的文件夹下。你的 JavaScript 文件会放到 mylibs 文件夹下,第三方脚本文件放在 libs 文件夹下,这不是信仰什么的,只是建议,这个结构会帮助你很舒服地管理那些 JavaScript 文件。

主要配置

当我开始使用 RequireJS 时,我需要创建一个 main 文件作为条目,我将它命名为 main.js ,把它放在app的目录下和文件夹 mylibs 同级。在 main.js 会定义 require 方法,这个方法是 require.js 定义的。在 mian.js 文件里我声明了两个我需要加载的文件,它会根据那些文件的路径注入进来,这样就可以执行它们的方法。 RequireJS 只接受 JavaScript 文件,所以不需要带有文件的 .js 后缀。

main.js

1
2
3
4
5
6
7
8
require([
"mylibs/utils",
"mylibs/palette"
], function(utils, palette) {

// the app is loaded...

});

现在我把 RequireJS 引入到我的 html 页面,和通过 data-main 指定 mian.js 文件。
1
<script src="/Scripts/require.js" data-main="/Scripts/app/main.js"></script>

程序运行后,RequireJS 已经把那两个文件加载进去了,如果你打开浏览器工具会看到那两个文件已经加载好了。

指定依赖关系

现在开始一些有趣的东西,palette.js 依赖 utils.js。缺少它,palette.js 就运行不了,我着手修改这些文件让 RequireJS 清楚知道那些依赖关系。我在那些文件上使用 define 方法。define 方法是 RequireJS 的方法,它需要两个参数:

  1. 需要依赖的 JavaScript 文件路径的数组
  2. 这个方法会在确保 RequireJS 加载完那些依赖的文件后才执行

一个 RequireJS Palette 模块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
define([
"mylibs/utils"
], function (utils) {

// this function is private and not available to sliders or utils
// function to handle the pallete color selection
var setColor = function (e) {
// the color object contains all the hex, rgba, and hsl
// conversions and utilities
var color = e.sender.color().toBytes();

// set the color
utils.setBackground(e.value);
};

// select and create the color palette
var colors = $("#colors").kendoFlatColorPicker({
change: setColor,
value: "#fff"
}).getKendoFlatColorPicker();

// just return the entire widget instance
return colors;

});

utils 模块没有依赖其他模块

1
2
3
4
5
6
7
8
define([], function () {
return {
// declare the function to change the background color
setBackground: function (color) {
$(document.body).css("background-color", color);
}
};
});

有没注意到那些 IIFF 的定义已经消失了?没有了 APP 的变量/名称空间。RequireJS 会处理你所定义的模块供你使用,你任何时候可以使用那些定义好的模块,只需执行 require("Your Module Name") 即可。我也可以在 main 中完全移除 utils.js,只加载 palette.jsRequireJS 会加载 palette.js 时,发现其依赖 utils.js 会自动第一个去加载。
1
2
3
4
5
6
7
require([
"mylibs/palette"
], function() {

// the app is running...

});

第三方库

在页面上我还会用到 Kendo UIJquery,但它们没包含在我的 RequireJS 配置中,这会有什么问题吗?当然不会啦,但你可以将第三方的库通过 RequireJS 包含进来,这样就可以使用简称去访问它们各自的方法。

配置路径

你可以将第三方的包配置在 mian.jspath 里,path 变量是用来指向那些库的路径。如果你想使用 CDN 去加载那些库,你可以定义使用本地的路径当 CDN 访问不到的话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require.config({
paths: {
// specify a path to jquery, the second declaration is the local fallback
jquery: ["//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery",
"../Scripts/jquery.1.9.1.min"]
}
});

require([
"mylibs/palette"
], function(jquery, palette) {

// the app is running...

});

你会把 jQuery 放在 main 里,但很多人则选择新加一个 app.js 文件,然后把自己定义模块放到里面去,app.js 用来加载你所有的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require.config({
paths: {
// specify a path to jquery, the second declaration is the local fallback
jquery: [ "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min",
"../Scripts/jquery.1.9.1.min"]
}
});

require([
'app'
], function(jquery, app) {

// this loads jquery and your app file

});

当然你也可以在 palette.js 指定依赖 jQuery,你也可以任何一个文件指定依赖项,但 RequireJS 会知道如何加载这些文件而不重复。因为我在 palette.js 之前里指定 jQuery,但并不意味着 RequireJS 都会加载 jQuery

第三方库依赖其他库

有些第三方类库会依赖其它一些第三方库,如 Kendo UI 依赖 jQuery。然而不同的 Kendo UI 模块在 RequireJS 上会指定内部依赖,但不会指定依赖 jQuery,尽管很容易在一条线上去请求。在这里我添加了一个本地的 Kendo UI 的依赖,使用 shim 方法让 RequireJS 知道在加载 Kendo UI 之前必须先加载 jQuery

Kendo UI 指定依赖 jQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require.config({
paths: {
// specify a path to jquery, the second declaration is the local fallback
jquery: [ "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min",
"../Scripts/jquery.1.9.1.min"],
kendo: [ "//cdn.kendostatic.com/2013.1.319/js/kendo.web.min",
"../kendo/2013.1.319/kendo.web.min"]
},
// inform requirejs that kendo ui depends on jquery
shim: {
"kendo": {
deps: ["jquery"]
}
}
});

require([
'app'
], function(jquery, kendo, app) {

// this loads jquery and your app file

});

现在可以在页面上移除 jQueryKendo UIscript 标签,让你的模块去指定需要他们。例如:在 palette 的文件中,现在改成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define([
"jquery",
"kendo",
"mylibs/utils"
], function ($, kendo, utils) {

// this function is private and not available to sliders or utils
// function to handle the pallete color selection
var setColor = function (e) {
// the color object contains all the hex, rgba, and hsl
// conversions and utilities
var color = e.sender.color().toBytes();

// set the color
utils.setBackground(e.value);
};

// select and create the color pallete
var colors = $("#colors").kendoFlatColorPicker({
change: setColor,
value: "#fff"
}).getKendoFlatColorPicker();

// just return the entire widget instance
return colors;
});

应用已经全部配置好,可以部署了。可以通过 as-is 去部署,但不太推荐这样子做。你需要压缩和串联你的 script 文件在发布的时候,这就是 r.js 的作用了。

我先告诉你需要安装 NodeJS

在使用 r.js 之前,需要安装 NodeJS,如果你没有 Node 或让你很不安,那只需把它看作是一个实用的命令行插件,可以帮助你自动化管理一些工作流,无论在操作系统或平台。在这案例,我需要的用到 NodeJS 运行 r.js 文件,该文件将在 JavaScript 平台上执行构建工作。

完成安装 NodeJS 后,就可以运行 r.js 去构建和优化那些 Require 模块文件。

强大的优化

RequireJS 优化是很完美的,但毕竟它不知需要优化那些 js,你需要给到它一个文件去告诉它那些文件需要压缩在你的项目里。这个文件你可以随便命名,但我习惯命名为 build.js 和把它放在最顶级的目录里,它不需要公开指定一个地址。

1
2
3
4
5
6
7
8
9
({
baseUrl: "./Scripts/app",
paths: {
jquery: "empty:",
kendo: "empty:"
},
name: "main",
out: "Scripts/main-built.js"
})

你注意大,我给 Kendo UIjQuery 提供的 pathempty ,因为需要告诉 RequireJS 这些是远程的 script 只是关联,不需要编译。

然后在 build.js 文件的路径下,通过 Node 运行 r.js 即可。

1
2
// run from the project directory
node r.js -o build.js

运行完后会产生一个 main-build.js 文件,把在项目中包含的 JavaScript 文件进行了串联和压缩成了这个文件。所以可以修改一下 data-mainmain-build.js 文件,代替 appmain.js

ASP.NET 的特殊处理

我(原作者)一直是 ASP.NET 的工作者,为了简化我(原作者)的工作流程,做了些改变。

发布的时候,加载正确的 JavaScript

我不想不停的更改我的 script 脚本,当我是在开发版本和发布新版本的时候。我将在 homecontroller 去嗅出当前我的开发模式,然后在 html 里使用相应的脚本语言

1
2
3
4
5
6
7
8
9
10
public ActionResult Index()
{

#if DEBUG
ViewBag.useBuild = false;
#else
ViewBag.useBuild = true;
#endif

return View();
}

按条件加载不同的文件
1
2
3
4
5
@if (ViewBag.useBuild) {
<script src="@Url.Content("~/Scripts/require.js")" data-main="@Url.Content("~/Scripts/main-built.js")"></script>
} else {
<script src="@Url.Content("~/Scripts/require.js")" data-main="@Url.Content("~/Scripts/app/main.js")"></script>
}

整合优化过的JavaScript到发布版本上,我也不想每次都要跳到控制台上去去执行优化当我发布的时候,这是你可以指定一个预先编译,在 Project / Project Settings / Build Events 里设置。我只需要当我发布一个版本时它自动帮我执行优化先。

当发布时,先执行脚本

1
if $(ConfigurationName) == Release node $(ProjectDir)/r.js -o $(ProjectDir)/Scripts/build.js

完成

模块化 JavaScript 是你迈向成功的一步,它让你更好创建和维护你庞大的应用。也可以下载 Kendo UI ,使用 RequireJS 开始创建一些非常模块化的应用。

其他资源
你可以在我们的ASP.NET MVC Examples repo 下载到这个例子,我也非常推荐你去看下这些资源。