使用 Require.Js 的模块化 JavaScript






4.97/5 (36投票s)
了解如何使用 AMD 库 Require.js 构建结构良好的模块化 JavaScript
下载演示项目: RequireJsDemos.zip
引言
我不知道有多少人会使用大量的 JavaScript 库,但我们在某些项目中发现自己使用了相当多的库。链接 JavaScript 文件带来的一些奇怪问题是,我们偶尔会遇到依赖问题,即某个 JavaScript 源文件依赖于另一个尚未加载(因此不可用)的 JavaScript 源文件。
幸运的是,有一个出色的库可以处理相互链接的 JavaScript 依赖关系和模块加载,它被称为: RequireJs
问题
您可能会问 RequireJs 试图解决什么问题。嗯,让我们考虑一个最常见的例子,即自定义 jQuery 插件,其中 jQuery 必须在插件工作之前加载。如果我们坚持使用标准的 script 标签,就像这样
<script src="scripts/myCooljqueryPlugins.js" type="text/javascript"></script>
<script src="scripts/jquery.js" type="text/javascript"></script>
我们立刻就遇到了问题,我们的自定义 jQuery 插件将无法工作,因为 jQuery 本身还没有被加载。当然,我们可以将 jQuery 的 script 标签移到自定义 jQuery 插件之前,这确实可以解决这次的问题。然而,这是一个非常孤立的小用例,在大型应用程序中并不是真正的常态,因为我们可能有许多相关的文件,它们都有依赖关系。这就是 RequireJs 可以提供帮助的地方。想了解更多吗?请继续阅读。
一个可能的解决方案
现在您知道了问题,那么有什么可能的解决方案呢?嗯,正如我所说,本文将使用 RequireJs 作为解决上述问题的一个可行方案。 RequireJs 除了解决依赖关系之外,还提供异步模块和文件加载。在我看来,RequireJs 使用模块是一个非常好的方面,因为它为开发人员提供了一个指导性的推动,促使他们创建具有单一职责的模块,这永远不会是坏事。使用 RequireJs 我们可以指定诸如
- 定义模块
- 需要一个模块
- 模块的依赖项(通过一个名为 RequireJs 的配置,称为 shim)
- 模块路径
演示
本节将概述几个不同的 RequireJs 示例,从一个典型的简单用例开始,然后构建到稍微更复杂的示例。
简单演示
这是简单的基础 101 示例,它说明了使用 RequireJs 的基本原理。稍后将对此进行扩展,但它确实说明了我们将在后续示例中遇到的一些关键点。
这是该项目在 Visual Studio 中的结构外观
引导应用程序使用 Require.js
在使用 RequireJs 时,您必须做的一件事是告诉它哪个文件将用于整体配置。这是一个简单的例子
<head>
<title>My Sample Project</title>
<!-- data-main attribute tells require.js to load
scripts/main.js after require.js loads. -->
<script data-main="scripts/main" src="scripts/require.js"></script>
</head>
您可以看到 data-main
属性指向一个主文件,该文件负责设置 RequireJs 的配置。让我们看一个简单的例子,好吗?
(function() {
require(["app/data", "helper/util", "helper/logger"], function (data, util, logger) {
//This function is called when scripts/helper/util.js is loaded.
//If util.js calls define(), then this function is not fired until
//util's dependencies have loaded, and the util argument will hold
//the module value for "helper/util".
alert('util module : ' + util.doubleItMethod(10));
//This function is called when scripts/app/data.js is loaded.
//If data.js calls define(), then this function is not fired until
//data's dependencies have loaded, and the util argument will hold
//the module value for "app/data".
alert('data module [0] : ' + data.storedData[0]);
alert('data module [1] : ' + data.storedData[1]);
logger.logThePair();
});
})();
我们在这里可以看到使用了 require()
函数,这是 RequireJs 提供的。第一个参数(可以是数组,如所示)指定此函数拥有的依赖项。现在让我们将注意力集中在我们有一个依赖项的模块之一。
下面的代码片段是 logger
模块,注意上面我们是如何说明我们 require()
了 logger 模块的,这是 RequireJs 提供的,然后我们可以自由地使用模块的代码。上面可以看到,当我们在 logger 模块中使用 logThePair()
函数时
define(function (require) {
var pair = require(".././app/keyValuePairs");
return {
logThePair: function () {
alert("color: " + pair.color + ", size: " + pair.size);
}
};
});
在此示例中,我们使用 define()
定义了一个模块,并且我们还引入了 RequireJs,它得到了特殊的处理,RequireJs 知道如何处理它。我们现在可以在其他地方使用这个定义的模块,通过另一个 require()
调用,就像我们上面写的那样
require(["app/data", "helper/util", "helper/logger"], function (data, util, logger) {
....
}
这里要注意的一点是,路径也包含在内,“helper/logger”,所以 require 会查找一个与该路径匹配的文件并使用它。我们也可以这样做指定路径
define(function (require) {
var pair = require(".././app/keyValuePairs");
return {
logThePair: function () {
alert("color: " + pair.color + ", size: " + pair.size);
}
};
});
我们稍后将看到更多关于路径配置的内容
使用 require () 与 define()
我们可以同时使用 require()
和 define()
来加载模块依赖项。 require()
函数用于立即运行,而 define()
用于定义可以从多个位置使用的模块。
与 jQuery 一起使用
既然我们已经看到了一个使用 RequireJs 的简单示例,让我们继续我们的旅程。让我们看看创建这样一个示例需要什么,即我们实际上有库依赖项,例如一个需要 jQuery 才能工作的 jQuery 插件。
这是该项目在 Visual Studio 中的结构外观
和以前一样,我们开始(我们总是会这样做)告诉 RequireJs 哪个主 JavaScript 代码文件来引导应用程序
<!DOCTYPE html>
<html>
<head>
<title>jQuery+RequireJS Sample Page</title>
<script data-main="scripts/app" src="scripts/lib/require.js"></script>
</head>
<body>
<h1>jQuery+RequireJS Sample Page</h1>
<input type="text" maxlength="150" id="txt" />
<input type="button" id="btn" value="click me" />
</body>
</html>
其中 scripts/app JavaScript 文件看起来是这样的
// Place third party dependencies in the lib folder
//
// Configure loading modules from the lib directory,
// except 'app' ones,
requirejs.config({
"baseUrl": "scripts/lib",
"paths": {
"app": "../app"
},
"shim": {
"jquery.appender": ["jquery"],
"jquery.textReplacer": ["jquery"],
}
});
// Load the main app module to start the app
requirejs(["app/main"]);
这次我们可以看到一些新的东西。我们可以看到我们实际上正在为某些事情配置 RequireJs,例如
- BaseUrl:指定所有脚本的基础路径,您仍然可以使用 RequireJs 与相对路径一起使用,但这是它的基础路径
- Paths:是一个命名路径的映射,我们在其中指定一个广为人知的名称和一个路径
- Shim:是一个文件及其依赖项的映射。它的作用是向 RequireJs 提供有关所需模块依赖项的提示,这样 RequireJs 就会知道以正确的顺序加载它们。请记住,RequireJs 使用一种技术,意味着模块是异步加载的。从上面的例子可以看出,“jquery.appender”和“jquery.textReplacer”依赖于 jQuery。因此 RequireJs 将首先加载它。我们似乎只是用这个来替换 HTML 中正确 JavaScript 导入的麻烦,但 RequireJs 确实给我们带来了异步加载模块的好处
最后一点是,我们通过告诉 RequireJs 哪个是应该运行的初始应用程序文件来启动整个应用程序。这是通过以下行完成的
// Load the main app module to start the app
requirejs(["app/main"]);
让我们花点时间来检查一下那个文件
require(["jquery", "jquery.appender", "jquery.textReplacer"], function ($) {
//the jquery.appender.js plugin have been loaded.
$(function () {
$(document).ready(function () {
$('#btn').click(function () {
var old = $('#txt').val()
var rev = reverseString(old);
$('h1').replaceTextWithFade('Old text was : ' + old);
$('body').appendData(rev);
});
});
});
function reverseString(str) {
if(typeof str !== 'string') {
throw new Error('reverseString(str) accepts only strings.');
return;
}
var strArr = str.split('');
var reversed = strArr.reverse();
return reversed.join('');
};
});
可以看出,它使用了 jQuery 和两个自定义 jQuery 插件,这些插件通过 RequireJs 的魔力满足了对 jQuery 的依赖,所以我们现在可以在请求它们作为必需品的 JavaScript 中安全地使用它们了。
仅为完整起见,这里是上面代码使用的自定义 jQuery 插件之一
(function ($) {
$.fn.appendData = function (data) {
this.each(function () {
return this.append('<p>' + data + '</p>');
});
}
}(jQuery));
RequireJs 魔法般地确保在 jQUery 本身加载之前不会加载此项,届时它将被提供给这些插件
与 ASP .NET MVC (4) 一起使用
我还想演示如何在 ASP .NET MVC 应用程序(我选择了 MVC 4)中使用 RequireJs,以下是我的做法。
这是该项目在 Visual Studio 中的结构外观
我使用了上面看到的相同的引导程序“app.js”代码,以及我们刚才看到的相同的两个自定义 jQuery 插件。
现在您知道我们正在使用一些我们刚才看到的东西,让我们继续处理 ASP .NET MVC 4 特定的部分,好吗?
它从 Bundle.Config 文件开始,对我来说是这样的:
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/lib").Include(
"~/Scripts/Lib/require.js"));
bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
}
}
然后我们有一个主布局页面,它像这样使用 bundles
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/lib")
<script type="text/javascript" src="~/Scripts/app.js"></script>
@RenderSection("scripts", required: false)
</head>
<body>
.....
.....
.....
</body>
</html>
然而这次,由于我们依赖于 ASP .NET MVC 4 的打包功能,我们需要为 RequireJs 的引导程序提供单独的链接,如下所示
<script type="text/javascript" src="~/Scripts/app.js"></script>
其中 app.js
与前面的示例相同。
从那里开始,所有剩下的就是声明您想要的每个页面的脚本(这对于任何 ASP .NET MVC 应用程序都很典型)
@{
ViewBag.Title = "Home Page";
}
....
....
....
....
@section Scripts {
<script src="~/Scripts/Controller/Home/Index.js"></script>
}
....
....
....
....
我们可以创建一个标准的 JavaScript 文件,如下所示
require(["jquery", "common/Utils", "jquery.appender", "jquery.textReplacer"], function ($,
common) {
$(document).ready(function () {
$('#mainDiv').append('<p>Index page is working with Require.js</p>');
alert('Type some text into the text box and then click the button');
$('#btn').click(function () {
var old = $('#txt').val()
var rev = common.reverseString(old);
$('#appendToDiv').appendData(rev);
});
});
});
它特定于控制器/页面组合,并将被注入到布局页面的“Scripts”部分。
与 TypeScript 一起使用
Typescript 是新秀,是微软试图为 JavaScript 提供面向对象特性的尝试。Typescript 本身有一个编译器,它将 Typescript 文件(*.ts)编译成 JavaScript 文件(*.js)。我认为为了完整起见,我也应该涵盖 Typescript,尽管这只涵盖了如何将 RequireJs 与 Typescript 一起使用。
这是该项目在 Visual Studio 中的结构外观
在开始之前,我应该提到,在使用 Typescript 时,您**绝对**想安装 Web Essential Visual Studio Extension,它使处理 Typescript 变得更加容易。它为 Visual Studio 提供了 Typescript 项模板。当编译过程启动时,它还会为 Typescript 文件创建 JavaScript。
TypeScript 定义文件
关于 Typescript 的一个优点是,它会在保存时(使用 Web Essential Visual Studio Extensions 安装后)报告错误(如果未加载适当的定义文件)。这是一个例子:
点击放大图
幸运的是,有适用于所有主流 JavaScript 库的优秀定义文件集可以提供帮助。
您只需要访问 DefinitelyTyped 网站,该网站为所有主流 JavaScript 库提供了 Typescript 的定义文件。这是一个带有正确定义文件加载的示例,请看编译错误如何消失。哦,我所做的就是从 DefinitelyTyped 网站 包含正确的定义文件,并将它们放入项目中,然后将它们拖到 Typescript 文件上。
点击放大图
在使用 Typescript 和 RequireJs 时,我还有最后一件事想提,那就是一个必须提供的命令行参数才能正确使用 RequireJs。
这就是模块类型,可以是“commonJS”或“amd”。“commonJS”用于 Node 开发,“amd”用于 RequireJs 之类的操作。顺便说一下,amd 代表“Asynchronous Module Definition”(异步模块定义)。
这里有一个非常好的文章,提供了更多关于“commonJS”和“amd”模块化 JavaScript 之间差异的信息
http://addyosmani.com/writing-modular-js/
幸运的是,安装 eb Essential Visual Studio Extension 为我们省了很多事,因为我们只需要在 Visual Studio 中设置一个选项,它就会为我们发出正确的命令行。
好了,背景知识就到这里,让我们看一些代码吧。
这是原始的 HTML 演示页面
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>TypeScript HTML App</title>
<link rel="stylesheet" href="app.css" type="text/css" />
<script data-main="scripts/app" type="text/javascript"
src="scripts/lib/require.js"></script>
</head>
<body>
<h1 id="header">TypeScript HTML App</h1>
<div id="content" />
</body>
</html>
可以看出,我们使用了之前的内容,通过链接到一个 scripts/app 引导程序文件。这个引导程序文件如下所示。它在很大程度上与之前的示例相同,但我们当然可以使用 Typescript 的一些特性,例如 lambda(() =>)。您可以通过点击下面的图片来查看生成的 JavaScript 的更大版本。正如我所说,它在很大程度上与之前的示例相同。
点击放大图
现在我们有了引导程序,让我们看看主要的 app.js 文件。
我想向您展示 Typescript 编译器根据我们之前讨论的用于选择模块类型的命令行选项所做的不同之处,然后再讨论使用 RequireJs 的细节(尽管如果您只阅读下面的代码,您会清楚地看到它)。
使用 AMD 模块命令行
这就是启用 AMD 命令行参数时将编译的内容(JavaScript 在右侧)。
点击放大图
不使用 AMD 模块命令行 (CommonJS 风格)
这就是禁用 AMD 命令行参数时将编译的内容(JavaScript 在右侧)。
点击放大图
看看有多大的不同。事实上,使用“amd”命令行参数会导致 Typescript 编译器实际使用 RequireJs 语法。
总之,在 Typescript 文件中使用 RequireJs 非常简单,您所要做的就是确保 Typescript 文件知道正确的 RequireJs 定义文件(我们之前已经讨论过),然后开始使用 RequireJs。
这里有一个小例子
require(['jquery'], ($) => {
var el = $('#content');
var greeter = new gt.Greeter(el);
greeter.start();
});
我使用的是 Typescript 网站上显示的 Greeter
示例的一个变体,我们在这里简单地更新一个 DOM 元素。需要注意的一点是,由于我也想使用 jQuery 来选择 HtmlElement
并将其传递给 Greeter
,因此您被迫使用 Typescript 类型 any
,因为它不将 jQuery 包装的 HtmlElement
识别为 HtmlElement
类型。
总之,这是 Greeter 的代码
点击放大图
就这些
好了,就到这里,希望您喜欢,并且可能在过程中学到了一些东西。