二、配置 Grunt
在本章中,我们将介绍启动和运行 Grunt 所需的步骤。我们首先介绍 Node.js 和 npm,因为它们是用于构建 Grunt 基础的关键技术。我们回顾 Node.js 模块及其与 Grunt 的关系,然后介绍 Grunt 环境的基础知识,包括 package.json 和 Grunfile.js 文件。设置好后,我们将继续配置 Grunt。我们将研究各种方法和策略,使 Grunt 最好地传达我们的构建。
安装
在本节中,我们将介绍如何安装和使用 Grunt 的关键组件 Node.js 和 npm。我们将简要介绍每一个,以及它们的核心概念。
随后,我们将介绍 Grunt 本身的简单安装。
Node.js
尽管本书主要关注 Grunt,但我们也将深入了解 Node.js 的世界(http://gswg.io#node)相当经常。考虑到 Grunt 是作为 Node.js 模块编写的,Grunt 任务和插件也是 Node.js 模块,我们必须了解 Node.js 及其包管理器 npm 的基础知识(http://gswg.io#npm).
Ryan Dahl 在 2009 年初启动了 Node.js 项目,因为对业界 web 服务器的现状感到失望。当时,由于 RubyonRails 框架,用 Ruby(Mongrel 和 Thin)编写的 web 服务器非常流行。Ryan 意识到用 Ruby 编写一个真正快速的 web 服务器是不可能的。Ruby 的低效率实际上是由该语言的阻塞性造成的,这意味着——在 web 服务器的上下文中——它不能有效地使用可用的硬件。当程序在等待给定的输入/输出(I/O)任务(如从硬盘读取或向 web 服务器发出网络请求)时,导致 CPU 暂停时,称为程序阻塞。
设置 Grunt
在许多编程语言中,块是固有的。JavaScript 和 Node.js 可以通过其事件执行模型避免阻塞问题。该模型允许 JavaScript 程序异步执行代码。也就是说,可以编写 JavaScript 程序中的 I/O 任务,这样它们就不会阻塞,从而实现高度的效率。
下表来自 Ryan Dahl 最初的 Node.js 演示(http://gswg.io#node-2009 年末的演示文稿)显示了 I/O 的主要类型
操作和使用的平均访问时间,相应的 CPU 数量
在每次 I/O 操作期间可能使用的循环:I/O 操作
CPU 周期
L1
3 个周期
L2
14 个周期
内存
250 个周期
磁盘
41000000 次循环
网络
240000000 次循环
根据这个表,通过阻塞任何磁盘或网络访问上的 CPU,我们在程序中引入了巨大的低效率;因此,在构建任何处理系统 I/O 的应用时,使用 Node.js 是向前迈出的一大步,这是当今大多数应用的特点。
总的来说,了解这种语言是如何工作的,它的优势在哪里,它的劣势在哪里,以及为什么 JavaScript 不是许多人以前称之为“玩具”的语言,在遍历 JavaScript 领域时将是非常有价值的。有关有用 JavaScript 资源的列表,请参见第 5 章、高级 Grunt。
要安装 Node.js,首先我们访问 Node.js 下载页面:http://gswg.io#node-下载。到达后,您将看到下表中的下载选项:
[30]
第二章
在撰写本文时,Node.js 的最新版本是 0.10.22。这很可能会改变,但不要害怕!下载页面始终包含 Node.js 的最新稳定版本。
在 Windows 和 Mac 上,安装程序是安装 Node.js 的最简单方法。
但是,有些人可能更喜欢使用操作系统软件包管理器,因为它们通常提供更统一的安装、卸载方法,最重要的是提供升级方法。例如,如果您在 Mac 电脑上,使用 homebrew 安装 Node.js 也非常简单,它还提供了在新版本发布时使用命令:brew upgrade Node 轻松升级到新版本的额外好处。要阅读有关通过软件包管理器安装 Node.js 的更多信息,请参阅 http://gswg.io#node-使用包管理器。此页面包含 Mac、Windows、Ubuntu 和各种其他 Linux 发行版的安装指南。我们将在第 5 章、高级 Grunt中的开发工具部分了解更多关于自制的信息。
现在我们已经安装了 Node.js,它附带了 npm;我们应该可以访问节点和 npm 可执行文件,因为它们现在应该位于系统的路径中。
以下命令应将每个可执行文件的版本打印到控制台:node--version 和 npm--version,这两个命令应显示刚刚下载并安装的 node.js 版本。在撰写本文时,我的输出看起来像:$node——版本
v0.10.22
$npm——版本
1.3.14
这证实了我们已经正确设置了 Node.js,现在可以使用它了!
模块
在研究 npm 之前,我们首先需要了解 Node.js 模块系统的基础知识。Node.js 模块系统是CommonJS的实现
规格 CommonJS 描述了 JavaScript 程序需要(或导入)其他 JavaScript 程序到其上下文中的简单语法。JavaScript 缺少的这一特性通过简化分离关注点的过程,极大地帮助创建模块化系统。在 Node.js 中,所有 JavaScript 文件都可以看作是单独的模块。因此,除此之外,我们将交替使用术语:文件和模块。我们可能也听说过在模块的位置使用术语包,这可能会令人困惑。不过,请放心,我们将在下一节中介绍有关 npm 的包。
[31]
设置 Grunt
CommonJS 1.1.1 规范可在 http://gswg.io#commonjs. 本规范描述了以下变量的使用:
•模块–表示模块本身的对象。模块对象包含导出对象。对于 Node.js,它还包含元信息,例如 id、父级和子级。
•exports–一个普通 JavaScript 对象,可以对其进行扩充,以向其他模块公开功能。exports 对象作为对 require 的调用的结果返回。
•require–一个函数用于导入模块,返回相应的导出对象。
对于 Node.js,可以使用相对路径或绝对路径通过文件名导入模块。当使用 npm(它将模块存储在 node_modules 目录中)时,还可以通过模块名称导入模块,我们将在下一小节中看到更多内容。对于 web 浏览器,CommonJS 的另一个实现可能需要 URL 模块。
CommonJS 规范还包含以下示例代码,为清晰起见,对其进行了轻微修改:
//代码示例 01 模块
//program.js
var inc=要求('./增量')。增量;var a=1;
console.log(inc(a));
//increment.js
变量添加=需要('./数学')。添加;
导出。增量=函数(b){
返回 add(b,1);
};
//math.js
导出.add=函数(c,d){
返回 c+d;
};
在本例中,我们将使用 program.js 作为入口点或“main”文件。因为我们知道 require 将返回所需文件的 exports 对象,所以很容易看到它的作用。从 program.js 开始,我们可以看到它调用 require(')/
增量”)。调用此 require 函数时,它会同步执行 increment.js 文件。依次调用 increment.js 模块需要('./math')。
js 文件使用 add 函数扩充其导出对象。
[32]
第二章
一旦 math.js 文件完成执行,require 将返回 math.js 模块的 exports 对象,从而允许 increment.js 使用 add 函数。
随后,increment.js 将完成其执行并将其导出对象返回到 program.js。最后,program.js 使用其新的 inc 函数将变量 a 从 1 增加到 2。现在,当我们使用 Node.js 运行 program.js 时,我们应该看到以下结果:
$node program.js
2
这个例子的一个重要特点是分离了这个模块化所提供的关注点。请注意,program.js 模块没有 add.js 模块的概念,但它完成了大部分工作。在计算机科学中,抽象功能并不是一个新概念;通过 Node.js 实现 CommonJS,它为用户提供了一种用 JavaScript 编写模块化程序的简单方法。我们可以将此功能放在一个文件中,但如果我们扩展 math.js 以包含每个常见的数学函数,它的大小和复杂性将快速增长。通过将模块拆分为子模块,我们将分离程序的关注点,将其从单个大型复杂程序转换为多个小型简单程序。许多小程序协同工作的思想是 Node.js 的基础之一。这有助于我们避开大型单片库,如 jqueryv1.x.x,从而进入 Node.js。这种大小的库将被分成更小的模块,允许用户只使用他们需要的。有关 Node.js 模块系统的正式文档,请访问 http://gswg.io#node-模块。
npm
如前所述,npm 是 Node.js 包管理器。自从 Node.js 版本 0.6.3 发布以来,npm 随每个 Node.js 发行版预先打包。npm 提供了以唯一名称将 Node.js 包发布到 npm 存储库的方法。随后,任何知道该唯一名称的人都可以安装这样的软件包。这就是 npm 的本质——从公共存储库共享和检索代码。“什么是包裹?”我们可能会问。在 npm频繁提问常见问题页面(http://gswg.io#npm-what-is-a-package),我们看到以下摘录:
“什么是包裹?
一个包是:
a)包含由package.json文件b)描述的程序的文件夹,包含(a)的 gzip tarball
c)解析为(b)的 url
[33]
设置 Grunt
为简洁起见,删除了 d)点至 g)点,但每个定义都会导致 a)。因此,包最终是包含有效 package.json 文件的任何文件夹。
这是作为 npm 包通过的唯一要求。
在最后一部分中,我们学习了模块,同时又回到了 CommonJS
规格随着 npm 的引入,有必要扩展 CommonJS 定义。npm 常见问题页面概述了以下模块说明(http://gswg.io#npm-什么是模块):
“什么是模块?
模块是可以在 Node.js 程序中加载require()的任何东西。
以下内容都是可以作为模块加载的示例:包含package.json文件的文件夹,其中包含main字段。
包含index.js文件的文件夹。
一个 JavaScript 文件。
因此,除了作为单个 JavaScript 文件外,模块还可以是包含 index.js 文件的任何文件夹,也可以是包含主字段的 package.json 文件的任何文件夹(基本上,主字段允许我们重命名 index.js)。请注意,这两个新定义与包的定义大致一致。包是包含 package.json 文件的文件夹,模块可以是包含包的文件夹。
json 文件或 index.js 文件。所以,为了让某人在他们的程序中使用您的包,它必须加载 require 函数,根据定义,这意味着您的包也必须是一个模块。这就是 Node.js 程序通常被称为“节点模块”而不是“节点包”的原因,因为“模块”更适合大多数场景。
在早期,Node.js v0.1.8 发布后不久,该平台就开始使用上一节概述的基于 CommonJS 的模块系统。它没有经过批准的方法来查找和发布模块。艾萨克·施卢埃特(Isaac Schlueter)看到了这一差距,并着手填补这一差距,于 2009 年 9 月启动了 npm 项目。2010 年初,Ryan 要求 Isaac 加入 Joyent,全职从事 npm 和 Node.js 的工作。2012 年 1 月,Ryan 辞去 Node.js 项目的“看门人”职务,并将权力移交给 Isaac。
一个有趣的事实是,许多人在引用 npm FAQ 中的“npm 不是节点包管理器的首字母缩略词”时相信他们带来了真相。然而,在写这篇文章的那天,艾萨克表现得很幽默,事实并非如此。正如有些人可能会说的那样,他是在“摇摇晃晃”。
npm 有很多特性,不过在本书中,我们将介绍两个最相关的工作流:查找模块和安装模块。
[34]
第二章
查找模块
npm 的搜索功能相当简单;我们键入命令 npmsearch,后跟希望匹配的搜索词。如果我们输入:npm search grunt concat,npm 将返回所有同时匹配 grunt 和 concat 的包。如果术语包含在包描述符(即包)的标题、描述、标记或依赖项中的任何位置,则该术语被视为匹配项。
json 文件。因此,在我们使用 Google 查找模块之前,最好先尝试 npm 搜索,因为 npm 将搜索 npm 网站上没有的元数据,因此不会被 Google 索引。假设我们想找到一个利用 Unix rsync 工具的 Grunt 插件。我们可以试试 npm 搜索 gruntplugin rsync。
在本例中,我们加入了 gruntplugin,根据 Grunt 团队的说法,gruntplugin 是所有 Grunt 插件使用的推荐标签。我们还包括了 rsync,以便将搜索范围缩小到只搜索那些与 rsync 匹配的 Grunt 插件。此命令当前生成:
$npm 搜索 gruntplugin rsync
名称说明
grunt-rsync 一个 grunt 任务,用于访问文件复制和同步grunt-rsync-2 的功能,将文件复制到具有 rsync 的(远程)机器。
支持 target:source映射一旦我们找到了一个可能有用的包,我们就可以用 npm info查看它的包信息,所以在本例中我们使用 npm info grunt rsync。然而,在大多数情况下,我们只是想知道如何使用它。因此,如果包有一个公共 Git 存储库,并且遵循开源最佳实践,那么它应该有一个自述文件
记录其用法的文件。我们可以使用 npm repo 打开此存储库页面
命令。现在,我们已经阅读了有关该软件包的内容,并确定它可能是我们正在搜索的,现在是安装它的时候了。
安装模块
npm install 命令有一个用途:从 npm 存储库下载模块。安装模块时,我们可以在本地或全局安装。
如果要在另一个模块或应用中使用模块,我们会选择本地安装;如果要将模块用作命令行工具,我们会选择全局安装。
[35]
设置 Grunt
当我们安装 Node.js 时,为 npm“二进制文件”创建了一个文件夹,并将其添加到您的系统路径中。这允许 npm 通过在此目录中放置一个符号链接来全局安装模块,该链接指向 package.json 文件的 bin 字段中指定的文件。我们在这里说“二进制文件”,因为术语二进制文件通常意味着某种已编译的机器代码;然而,在本例中,npm 二进制文件只是一个 JavaScript 文件。例如,如果我们想全局安装 express 模块,我们将使用命令:npm install-g express。
在 Grunt 环境中,我们将主要使用 npm 安装在特定 Grunt 环境中本地使用插件。假设我们正在开发一个 jQuery 插件,我们希望缩小我们的源代码。我们可以通过 grunt contrib uglify 插件实现这一点。为了在 Gruntfile.js 文件中使用这个插件,我们必须首先使用以下命令在本地安装它:npm install grunt contrib uglify。这将把新下载的模块放在当前包的 node_modules 文件夹中。为了确定当前包,npm 将从当前工作目录向上遍历文件目录树,查找模块描述符–package.json。
如果找到 package.json 文件,则其包含文件夹将用作包根目录;但是,如果找不到,npm 将假定还没有包,并使用当前目录作为包根目录。一旦确定了包根目录,将创建一个 node_modules 文件夹(如果还不存在的话),最后,我们要安装的模块将放在其中。为了帮助巩固这一点,请考虑以下目录结构:
//代码示例 02 npm 安装目录
└── 项目
├── A.
│ └── B
│ └── C
│ └── 重要的.js
└── package.json
如果我们从 c 目录运行 npm install grunt contrib uglify,项目目录将用作包根目录,因为它包含 package.json。
$cd 项目/a/b/c
$npm 安装 grunt contrib uglify
完成后,前面的命令将生成以下目录结构:
└── 项目
├── A.
│ └── B
│ └── C
│ └── 重要的.js
[36]
第二章
├── 节点单元
│ ├── 粗声粗气
│ │ └── ...
│ └── ...
└── package.json
但是,如果在 npm 安装此相同命令之前删除 package.json,则会导致以下目录结构:
└── 项目
└── A.
└── B
└── C
├── 重要的.js
└── 节点单元
├── 粗声粗气
│ └── ...
└── ...
这种计算节点 _modules 目录放置位置的模式与 require 函数用于查找模块的模式兼容。当我们希望使用新安装的模块时,我们使用模块名称(而不是文件名)调用 require 函数。require 函数将在当前目录中查找 node_modules 文件夹,如果它不在那里,它将检查父目录。它将继续搜索目录树,直到找到节点\模块文件夹或到达驱动器的根目录。因此,我们总是需要一个安装模块的位置,即使它实际上被放置在目录树上的许多文件夹中。
现在我们已经安装了 grunt contrib uglify,我们可以在 Gruntfile.js 文件中使用:grunt.loadNpmTasks(“grunt contrib uglify”)加载此模块的 grunt 任务。loadNpmTasks 函数以与 require 函数类似的方式搜索 node_modules 文件夹。一旦找到,它将在里面寻找所需的模块。最后,它将加载模块任务目录中的所有文件。
这就是单个模块(Grunt 插件)可以提供多个任务的方式。
Grunt
最后,我们可以安装 Grunt!Grunt命令行界面(CLI)作为一个单独的模块发布,其一个重要原因是:允许我们在一台机器上、多个项目上使用 Grunt 的各种向后不兼容版本,而不必担心。之所以可以这样做,是因为 grunt cli 模块在当前目录或其父目录中搜索 grunt 实例(grunt 模块)(同样,类似于 require 函数)。
[37]
设置 Grunt
这意味着我们可以提取一个遗留的 Grunt 项目(v0.3.x)并在命令行上运行 Grunt(实际上是 Grunt cli 模块)。然后,导航到另一个 Grunt 项目(v0.4.x)并再次运行 Grunt;两者都将无缝运行。
考虑到这一点,我们应该能够了解为什么要在全局和本地安装 grunt cli。
首先,我们将使用以下命令安装 grunt cli:$npm install-g grunt cli
应该注意的是,在 Mac 和 Linux 上,全局安装模块时可能会收到权限错误。为了解决这个问题,我们可以预先设置 sudo,例如 sudonpm 安装
-格朗特。但是,模块能够在安装时执行任意代码;因此,使用 sudo 可能被认为是不安全的。
为了防止这种情况,最好在不使用 sudo 的情况下重新安装 Node.js。
有关此主题的更多信息,请参阅此 GitHub 要点(http://gswg.io#npm-艾萨克·施卢特的《无苏多》。
接下来,我们将找到希望在其中使用 Grunt 的项目,并将使用以下命令:
$cd 我的项目/
$npm 安装 grunt
然而,请注意,当再次设置此特定项目时,我们希望不必手动记住我们使用的每个模块。解决此问题的一个方法是将 node_modules 文件夹与项目一起保存。
在某些情况下,这可能没问题,但是,npm 是为容纳和服务模块而构建的。在下一节中,我们将看到使用我们的包 npm 的更好的解决方案。
json 文件和依赖项字段。
项目设置
现在我们已经安装了 Node.js、npm 和 Grunt,我们已经准备好创建第一个 Grunt 环境。假设我们已经建立了一个网站,现在我们想使用 Grunt 缩小我们的资产。在本节中,我们将了解两个必需的文件:package.json 文件和 Gruntfile.js 文件,以及推荐的目录结构。
[38]
第二章
package.json
package.json 文件是一个包描述符;它用于存储有关模块的所有元数据,如名称、版本、描述、作者、存储库等。它是有效使用 npm 所需的单个文件,因此,package.json 文件也可以被视为“npm 文件”。正如文件扩展名所示,它必须采用JavaScript 对象表示法(JSON数据格式。如果 JSON 语法无效,npm 将在读取此文件时显示错误。在我们的项目中使用 package.json 文件有很多好处。这些包括:通过定义依赖项字段,使重新安装依赖项变得容易;允许我们通过定义名称和版本字段将模块发布到 npm,并通过定义脚本对象存储与包相关的公共脚本。
对于使用 Grunt 的项目,dependencies 属性将是 package.json 文件最有用的功能。当我们运行命令:npm install(没有继续的包名)时,npm 将查找我们的 package.json,解析它,然后安装 dependencies 属性中列出的每个模块。
在使用 dependencies 属性查看示例 package.json 文件之前,了解所有 npm 包的版本控制方式非常重要。汤姆·普雷斯顿·沃纳(Tom Preston Werner)在 2009 年末提出了语义版本控制规范(SemVer),这是因为他将其描述为“依赖地狱”——这种情况出现在由许多较小系统构建的大型系统中。SemVer 网站(http://gswg.io#semver)包含以下简短摘要:
“给定版本号MAJOR.MINOR.PATCH,当您进行不兼容的 API 更改时,增加1.MAJOR版本;当您以向后兼容的方式添加功能时,增加2.MINOR版本,和
3。补丁版本,当您修复向后兼容的错误时。
预发布和构建元数据的附加标签可作为MAJOR.MINOR.PATCH格式的扩展提供。
虽然模块发布者不需要严格遵守 SemVer,但 npm 确实要求他们确保版本号的格式正确,并在每次发布时递增。
[39]
设置 Grunt
以下是 package.json 文件的一个非常简单的示例:
//代码示例 03 npm 安装
{
“依赖项”:{
“grunt”:“0.4.2”
}
}
在本例中,当我们在这个 package.json 文件旁边执行:npm install 时,它相当于执行:npm installgrunt@0.4.2. 后跟版本的@符号告诉 npm 安装特定版本的 grunt 模块。格伦特是上述规则中罕见的例外。同样,请访问 SemVer 网站:
“主版本零(0.y.z)是初始开发,任何东西都可能在的任何时间发生变化,公共 API 不应该被认为是稳定的。”
由于 Grunt 目前的版本为 0.4.2,并且也遵循 SemVer,Grunt 团队认为 Grunt 仍在开发中,因为 API 尚未冻结。有些人不同意这个决定,因为 Grunt 在整个 Web 开发行业中被广泛使用;然而,这是一个相对无关紧要的细节。由于 Grunt 没有主版本可供使用,因此对于向后不兼容的更改,次版本将递增。因此,从 Grunt 的 0.3.x 版到 0.4.x 版所做的更改是不兼容的。为了防止主版本和次版本的自动升级,我们将使用以波浪号为前缀的版本。波浪形符号(~)表示任何近似版本;这是 npm 的一个特性,它也可以被看作是 SemVer 的一个补充,以减轻向后不兼容的更改。tilde 前缀告诉 npm,它只能升级给定的包,以增加补丁版本。
例如,如果我们首先安装了 grunt 模块的 0.3.5 版本,同时在 package.json 文件中指定:“grunt”:“latest”,那么随后的 npm 安装将生成最新版本。如前所述,从版本 0.3.5 到 0.4.2(最新版本)的更改将对我们的构建引入突破性的更改。但是,如果我们改为指定近似版本:“grunt”:“~0.3.5”,后续的 npm 安装只会将我们升级到与 0.3.x 匹配的版本,因此目前,它将生成 0.3.17 版本。
因此,我们应始终指定准确或近似版本,我们不应使用“最新”或“*”(表示任何版本)。为了帮助我们实现最佳实践,npm 中添加了一个方便选项,正好适合这种情况。
启动项目时,以及我们第一次安装模块时,我们可以使用--save 选项,除了安装模块外,该选项还将使用刚刚安装的模块及其最新近似版本自动更新 package.json 文件。
[40]
第二章
例如,如果我们从一个空的 package.json 文件开始,如下所示:
{}
然后,如果我们执行:npm install--save grunt grunt contrib uglify,它当前会将以前的空 package.json 文件更新为以下代码:
{
“依赖项”:{
“咕噜”:“~0.4.2”,
“咕噜咕噜作怪”:“~0.2.2”
}
}
如本例所示,当从 npm 安装模块时,我们还可以一次包含多个模块名称 npm 安装模块 1 模块 2 模块 3。
当我们使用 npm 安装时,npm 将检索给定模块及其依赖项,并重复此步骤,直到检索到所有依赖项。如果我们将 grunt 模块作为依赖项发布模块,那么依赖于我们模块的其他模块也将被迫下载 grunt。然而,Grunt 是一个构建工具;它构建并将我们的源代码转换为最终产品。这一最终产品应该发布供公众使用。因此,在本例中,grunt 模块是一个仅用于开发的依赖项,实际上属于 package.json 文件的 devdependences 字段。幸运的是,还有一个
--save dev 选项,它将执行与--save 选项完全相同的操作,但它将使用 devDependencies 字段,而不是将依赖项列在 dependencies 字段中。
当我们向 package.json 文件中添加越来越多的字段时,重新设置它可能会很烦人,我们可能会尝试简单地将 package.json 文件复制并粘贴到新项目中。然而,构建我们自己的包是一个很好的实践。
每个项目的 json 文件。这可以通过另一个 npm 特性 npminit 命令轻松完成。npm init 是一个用于创建 package.json 文件的设置“向导”。
一旦执行,npm 将提示我们输入每个公共字段。这是一个很好的习惯,因为在大多数情况下,所有项目中唯一相似的字段是 author 字段,并且也有一个 npm 命令:npm config set init。
author.name“Jaime Pillora”。这将为所有后续 npm 初始化设置默认作者。我们尤其不应该在启动新项目时复制和粘贴依赖项字段。没有代码可中断的时间是升级到每个包的最新版本的最佳时间。
[41]
设置 Grunt
总之,当启动一个新的 Grunt 项目时,首先我们应该创建我们的包。
json 文件,然后我们应该添加我们的依赖项(Grunt 以及我们正在使用的 Grunt 插件),并锁定它们的当前(和最新)版本,例如,npm install——save dev Grunt Grunt contrib uglify。
完成后,我们应该有一个 package.json 文件,该文件类似于以下文件:
//代码示例 04 包 json
{
“名称”:“gswg-2-04-package-json”,
“版本”:“0.1.0”,
“存储库”:https://github.com/jpillora/gswg-examples.git",
“作者”:“詹姆·皮洛拉,
“依赖性”:{
“grunt contrib 丑恶”:“~0.2.2”,
“咕噜”:“~0.4.2”
},
“许可证”:“麻省理工”
}
通过将 package.json 文件的内容粘贴到位于的 package.json 验证器工具中,我们可以确认我们已经创建了一个有效的 package.json 文件 http://gswg.io#package-json 验证器。
gruntile.js
正如命令 npm install 查找 package.json 文件并在没有它的情况下失败一样,grunt 命令也将使用类似的方法查找 Gruntfile.js 文件。一旦找到,Grunt 将使用 Grunt 全局对象调用此文件。GrunFile.js 文件可以看作是我们的构建初始值设定项–它将定义配置和设置任务。但是,它不会包含运行生成的指令。一旦 Gruntfile.js 完成执行,就会自动发生这种情况。但是,我们可以自定义运行的内容,并且可以通过命令行提供额外的选项,正如我们将在第 3 章、中使用 Grunt看到的那样。
在 Grunt 网站的入门页面上(http://gswg.io#grunt-Grunt 团队将以下代码描述为 Gruntfile.js 文件“wrapper”:module.exports=function(Grunt){
//在这里做呼噜相关的事情
};
[42]
第二章
我们可以将此语法视为所有 Gruntfile.js 文件运行所需的神奇“包装”;然而,了解其目的是有利的。如果我们回忆一下 CommonJS 的摘要,我们会记得 module.exports 对象是作为另一个需要它的模块的结果返回的。因此,在这段代码中,我们只是提供了一个带有单个参数的函数。然后 Grunt 将使用 Grunt 对象作为单个参数调用我们的函数。grunt 对象是我们用来与 grunt 交互的对象。也就是说,它是 Grunt 的应用编程接口(API)——它包含已经公开(或导出)供公众使用的方法。grunt 对象包含更新和检索配置的方法(grunt.config)、加载和注册任务的方法(grunt.task)、读取和写入文件的方法(grunt.file)等等。grunt 对象还包含常用函数的别名,例如,grunt.config.init 可以通过 grunt.initConfig 调用,grunt.task.registerTask 可以通过 grunt 调用。
注册任务。
阅读有关 Grunt API 的更多信息,请访问 http://gswg.io#grunt-api。
由于 Grunt 是开源的,当我们不确定某个特定功能时,我们可以随时访问存储库并阅读源代码 http://gswg.io#grunt-源代码。每个要素集都有自己的文件。例如,grunt.config 模块及其方法可以在 lib/grunt/config.js 文件中找到。
我们提供的“包装器”函数的目的是初始化 Grunt 配置以在任务中使用,并加载和分组任务以与命令行一起使用。
现在我们有了一个 package.json 文件并安装了模块,我们刚刚完成了本书开头的代码示例 01 minify 的前奏。
在这里,我们将找到一个简单的 Gruntfile.js 文件,该文件利用 grunt contrib uglify 插件和执行此构建的预期输出。在关于配置任务的下一节中,我们将介绍更复杂的用例。
目录结构
现在我们已经创建了 package.json 和 Gruntfile.js 文件并安装了我们的包,我们应该有以下目录结构:
//代码示例 05 目录结构
.
├── Grunfile.js
├── package.json
└── 节点单元
[43]
设置 Grunt
├── 咕哝
└── 粗声粗气
另外,如果我们将项目放在版本控制系统(VCS)中,我们需要记住排除 node_modules 文件夹。例如,对于 Git,我们还将包含一个.gitignore 文件,其中包含一个行节点\ u 模块。
根据项目的类型,源文件的结构可能不同,但是,Grunt 的常见用例是将源文件转换为构建(或输出)文件。因此,通常情况下,我们会将所有源文件包含在一个名为 src 的文件夹中,然后创建另一个文件夹构建来容纳此构建的结果。这种清晰的分隔非常重要,因为构建文件夹可以被视为临时文件夹:它可以随时替换为一组新文件。因此,重要的是我们不要混淆源代码和构建文件。
如果我们要向项目中添加一个测试套件,那么除了源文件之外,我们还将拥有测试文件。这些测试文件运行我们的构建文件,以确保它们按预期运行。最后,一旦我们添加了与项目相关的文件,就应该留下一个类似于以下目录结构的目录结构:
//代码示例 05 目录结构
.
├── Grunfile.js
├── package.json
├── 节点单元
│ ├── 咕哝
│ └── 粗声粗气
├── 建筑
├── src
├── 测验
└── .gitignore
此外,通过从 VCS 中排除 build 文件夹,我们强制所有使用此项目的开发人员执行构建。这将突出显示任何依赖于机器的构建问题,并确保尽早解决这些问题。一旦到位,我们就有一个简单的设置指南来开始我们的新项目。运行 npm 安装,然后运行 grunt。
配置任务
Grunt 配置可以看作是单个 JavaScript 对象,但是,我们将使用 Grunt 提供的函数来获取和设置属性,而不是赋值。
[44]
第二章
我们在第一章中简要介绍了 Grunt 配置,介绍了 Grunt,展示了 Grunt.initConfig、Grunt.config.get 和 Grunt 的简单用法。
config.set 函数。grunt.initConfig 函数(如前所述,它的别名来自 grunt.config.init)接受一个对象,然后将该对象用作配置的起点,而 grunt.config.get 和 grunt。
config.set 函数用于获取和设置单个属性。我们还可以使用更短的 grunt.config 函数,它的工作原理类似于 jQuery getter 和 setter。
当使用一个参数调用时,它将化名为 grunt.config.get,使用两个参数时,它将化名为 grunt.config.set。以下示例中的每一行在功能上是等效的:
grunt.config.init({foo:{bar:7});
grunt.config.set('foo.bar',7);
grunt.config('foo.bar',7);
需要注意的是,对 grunt.config.init(或 grunt.initConfig)的调用将删除所有先前的配置。
Grunt 专注于以声明方式定义构建。因为我们使用构建工具来提高效率,对于我们的团队和我们自己来说,构建是可管理和可访问的,这一点很重要。如果我们的构建工具只是一个包含许多步骤的长 shell(或批处理)脚本,那么每个过程都必须按顺序定义。它的长度将使其他人(以及我们未来的自己)难以理解,迫使我们反向设计步骤。然而,如果我们的构建是由声明性步骤组成的,我们可以这样理解:“我想使用这些选项将这些CoffeeScript文件编译到这个文件夹中”。例如:
咖啡:{
汇编:{
档案:{
“build/app.js”:“src/scripts/***/.coffee”
},
选项:{
是的
}
}
}
因此,我们可以将 Grunt 配置视为一种声明式定义我们希望如何运行命令式 Grunt 任务的方法。这就是 Grunt 的光芒所在,也是它如此流行的原因——它有助于抽象“如何”并关注“什么”。
[45]
设置 Grunt
Grunt 配置方法可以在任何可以访问 Grunt 对象的地方使用,但是,在大多数情况下,我们只在任务和多任务中使用配置。
如前所述,Grunt 任务只是函数。例如,假设我们有一个 Grunt 任务来检查 app.js 文件中的 console.log 语句。此 consoleCheck 任务可能如下所示:
//代码示例 06 配置获取集
//任务/console-check.js
module.exports=函数(grunt){
registerTask('consoleCheck',function(){
//加载 app.js
var contents=grunt.file.read('./src/app.js');
//搜索 console.log 语句
if(contents.indexOf('console.log(')>=0)
grunt.fail.warn(“”console.log(“在“app.js”中找到”);
});
};
但是,我们可能希望在另一个项目中重用此任务。为了便于重用,我们将此任务概括为字符串检查任务,允许我们定义要查找的文件和字符串:
//代码示例 06 配置获取集
//tasks/string-check.js
module.exports=函数(grunt){
registerTask('stringCheck',function()){
//如果未提供配置,则失败
grunt.config.requires('stringCheck.file');
grunt.config.requires('stringCheck.string');
//检索文件名并加载它
var file=grunt.config('stringCheck.file');
var contents=grunt.file.read(文件);
//检索要搜索的字符串
var string=grunt.config('stringCheck.string');if(contents.indexOf(string>=0))
grunt.fail.warn(“+file+”中的“+string+”);
});
};
[46]
第二章
首先,在新的 stringCheck 任务中,我们将使用 grunt.config.requires 来确保配置存在,接下来我们将检索此配置,最后我们将搜索字符串并显示结果。现在,我们可以通过提供以下配置来配置此任务以执行其原始目的:
//代码示例 06 配置获取集
//Grunfile.js
grunt.initConfig({
stringCheck:{
文件:'./src/app.js',
字符串:'console.log''
}
});
使用 console.log(在我们的 app.js 文件中)运行此示例时,我们应该看到以下输出:
$grunt
运行“stringCheck”任务
警告:“console.log(“在“/src/app.js”中找到”
使用--force 继续。
由于警告而中止。
在输出的最后一行,我们看到 Grunt 由于警告而被中止。
由于我们在任务中使用了 grunt.fail.warn 函数,我们看到了使用--force 标志继续的提示;然而,如果我们使用 grunt.fail.fatal 函数,我们将无法忽略我们的新任务,直到我们删除了有问题的字符串。请参阅代码示例以查看可运行版本。
还要注意,这是一种检查源代码的幼稚方法。例如,当我们的字符串被注释掉时,此任务将错误地失败。为了解决这个问题,我们需要使用 JavaScript 解析器来提取代码的抽象语法树(AST),然后在该树中搜索我们选择的语法。
配置多任务
继续执行字符串检查器任务,我们很可能希望检查多个文件。代替文件字符串,我们可以首先考虑使用文件数组;但是,如果我们想在一个文件中检查多个字符串,该怎么办?我们还应该将 string 属性转换为数组吗?如果我们只想在某些文件中查找某些字符串呢?使用数组是不够的。
[47]
设置 Grunt
输入多任务。多任务允许我们使用配置目标解决前面概述的假设问题。目标为我们提供了配置单个任务的多个运行的方法。如果要将字符串检查器任务转换为多任务,其配置可能如下所示:grunt.initConfig({
stringCheck:{
目标 1:{
文件:'./src/app.js',
字符串:'console.log''
},
目标 2:{
文件:'./src/util.js',
字符串:eval('
}
}
});
我们可以回忆一下第 1 章、介绍 Grunt的类似配置,代码示例也使用了 target1 和 target2。这些通用名称旨在强化目标名称可以任意设置的概念。因此,我们应该设计目标名称,以提高构建的可读性。Internet 上的许多示例显示任务名称,例如 dist(distribution 的缩写)、build 和 compile。虽然这些可能很好地描述了目标,但对于一个新来者来说,很难区分配置的哪些部分必须是静态的,哪些部分可以是动态的。例如,在前面的代码片段中,我们可以使用 app 和 util 而不是 target1 和 target2。这些逻辑名称允许我们使用可读的命令,如:
$grunt stringCheck:应用
$grunt stringCheck:util
在第 3 章中,我们将使用 Grunt学习更多关于运行和创建我们自己的多任务的知识。
配置选项
定义自定义任务的选项是很常见的;因此,任务和多任务都将其函数上下文设置为任务对象,该对象具有可用的选项函数。调用时,它按名称查找任务的配置,然后查找选项对象。例如:
grunt.initConfig({
我的任务:{
[48]
第二章
选项:{
酒吧:7
},
傅:42
}
});
registerTask('myTask',function()){
this.options();//{bar:7}
});
此功能在多任务中最有用,因为我们可以定义任务范围的选项对象,该对象可能会被特定于目标的选项覆盖。例如:grunt.initConfig({
我的多任务:{
选项:{
傅:42,,
酒吧:7
},
目标 1:{
},
目标 2:{
选项:{
酒吧:8
}
}
}
});
由于 target1 没有定义 options 对象,检索其选项将产生:{foo:42,bar:7}。但是,当我们检索 target2 选项时,它的 bar 选项将覆盖任务选项,结果对象将是:{foo:42,bar:8}。我们将在第 3 章中使用 Grunt介绍更多关于任务对象的内容。
配置文件
绝大多数 Grunt 任务将执行某种类型的文件操作。为了满足这个需求,Grunt 使用预定义的对象结构以及文件“globbing”(或通配符文件选择)来生成一个简洁的 API 来描述文件。运行多任务时,将检查其配置是否存在此文件模式,并尝试将所描述的文件与在这些位置可以找到的文件相匹配。一旦完成,它将把所有匹配的文件放在任务的文件数组中(任务上下文中的 this.files)。
[49]
设置 Grunt
接下来,我们将介绍描述各种用例文件的各种方法。
但是,首先,我们将讨论匹配文件意味着什么。Grunt 中的文件匹配使用由 Isaac Schlueter 编写的模块:node glob。文件 globbing 起源于 70 年代的 Unix,当时发明了一种简单的语言来允许文件的通配符选择。例如,*.txt 将同时匹配 a.txt 和 b.txt。下面是 Grunt 文档的摘录,描述了 Grunt 中可用的 globbing 选项(http://gswg.io#grunt-全球化):
“虽然这不是关于全局模式的全面教程,但要知道在文件路径中:
*匹配任意数量的字符,但不匹配/
? 匹配单个字符,但不匹配/
匹配任意数量的字符,包括/*,只要它是路径部分*中的唯一内容
{}允许以逗号分隔的“或”表达式列表
! 在一个模式的开头将否定匹配大多数人需要知道的是foo/.js将匹配所有以*结尾的文件
.js在foo/子目录中,但foo/*.js将匹配所有以结尾的文件
.js位于foo/子目录及其所有子目录中。“
接下来,我们将回顾代码示例 07 配置文件的部分内容。此示例包含一个任务 showTargetFiles,该任务显示其每个目标的文件数组:
//注册多任务(每个目标运行一次)
registerMultiTask('showTargetFiles',function(){
//显示“文件”数组
this.files.forEach(函数(文件){
console.log(“源:+file.src+”->”+
“目的地:“+file.dest”);
});
});
基于此任务,我们将注意到 files 数组中的每个文件都包含 src 和 dest 属性。本例中的 src 属性是将输入 glob 与文件系统进行匹配的输出。以下每个目标示例都包含 src 输入以及此任务的结果。
[50]
第二章
单套源文件
这是紧凑的格式;目标可以使用 src 属性描述一组文件源,以及使用 dest 属性的可选目标文件。
如果没有目标,此格式通常用于只读任务,如代码分析,如字符串检查器任务:
目标 1:{
src:['src/a.js','src/b.js']
}
我们可以使用前面描述的{}语法来缩短前面的示例,以表示 a 或 b。我们还将添加一个目标属性:target1:{
src:'src/{a,b}.js',
dest:'dest/ab.js'
}
注意,在第二个示例中我们省略了数组,因为它不是必需的,因为我们只指定了一个 glob 字符串。如果要定义多组源文件,则需要使用 files 属性。
多套源文件
要描述具有单个目标的多个源集,我们可以使用“文件数组格式”。例如,在描述多个文件连接时,此格式可能很有用:
目标 1:{
档案:[
{src:'src/{a,b,c}.js',dest:'dest/abc.js'},
{src:'src/{x,y,z}.js',dest:'dest/xyz.js'}
]
}
我们可以使用更为压缩的“Files object format”(文件对象格式)获得相同的结果,其中 Files 属性现在是一个对象而不是一个数组,每个键都是目标,每个值都是源,如下所示:target1:{
档案:{
'dest/abc.js':'src/{a,b,c}.js',
'dest/xyz.js':'src/{x,y,z}.js'
}
}
[51]
设置 Grunt
此外,当我们使用带有 src 和 dest 的对象指定一组文件时,我们可以选择使用一些附加选项;其中一个选项将允许我们映射目录而不是文件。
将源目录映射到目标目录通常我们希望将一组源文件转换为同一组目标文件。在本例中,我们基本上选择了一个源目录和一个目标目录。这在编译 CoffeeScript(或任何其他源代码到源代码的编译)时非常有用,我们希望维护目录结构,同时仍然通过选择的转换运行每个单独的文件。这是使用展开选项完成的。例如,如果我们想将所有的.js 源文件压缩成一个.min.js 文件的结果集,我们可以手动将每个文件从一个目录映射到另一个目录:
目标 1:{
档案:[
{src:'lib/a.js',dest:'build/a.min.js'},
{src:'lib/b.js',dest:'build/b.min.js'},
{src:'lib/subdir/c.js',dest:'build/subdir/c.min.js'},
{src:'lib/subdir/d.js',dest:'build/subdir/d.min.js'},
],
}
但是,每次添加文件时,我们都需要添加另一行配置。
使用展开选项,将根据源文件的路径和其他选项自动生成每个匹配源文件的目标。
此处,target2 等同于 target1,但当我们添加新的源文件时,它们将自动由我们的“***.js”全局字符串匹配,并映射到相应的目标文件:
目标 2:{
档案:[
{
扩展:真,
cwd:'lib/',
src:“***.js”,
dest:'build/',
分机:'.min.js'
},
],
}
[52]
第二章
以下是网站上提供的其他选项 http://gswg.io#configuring-在任何文件对象内使用的任务:
“展开设置为true以启用以下选项:cwd所有src匹配都是相对于(但不包括)此路径的。
src模式匹配,相对于cwd。
目的地目的地路径前缀。
ext用生成的目的地路径中的该值替换任何现有扩展。
展平从生成的目的地路径中删除所有路径部分。
重命名此函数针对每个匹配的src文件调用(扩展名重命名并展平后)。传入dest和匹配的src路径,此函数必须返回新的dest值。如果同一个dest被多次返回,则使用它的每个src将被添加到该源的数组中。“
有关每个文件配置方法的演示,请参阅代码示例 07 配置文件。
模板
在前面的配置部分中,我们介绍了 grunt.config 的使用。这里,我们将介绍使用特殊的“getter”和“setter”API 修改简单对象的原因之一。当我们使用 grunt.config.set 或 grunt.initConfig 在配置中设置值时,我们可以使用 grunt 模板系统重用配置的其他部分。例如,如果我们定义了一些属性:
//代码示例 08 模板
grunt.initConfig({
foo:‘c’,
酒吧:“bd”,
巴兹:“ae”
});
然后,如果我们运行任务:
//代码示例 08 模板
registerTask('default',function()){
grunt.log.writeln(grunt.config.get('bazz');
});
[53]
设置 Grunt
我们应该看到:
$grunt
运行“默认”任务
abcde
当我们使用 grunt.config.get(“…”)时,内部 grunt 正在使用 grunt。
process 函数递归解析每个模板(也就是说,我们可以在其他模板中包含模板)。当我们希望在一组文件上执行许多任务时,Grunt 模板是最有用的。我们可以定义这个集合一次,然后使用 Grunt 模板多次重复使用它。例如,使用以下配置:
//代码示例 09 模板数组
grunt.initConfig({
foo:['a.js','b.js','c.js','d.js',],
巴兹:“”
});
我们前面的任务返回:
运行“默认”任务
['a.js'、'b.js'、'c.js'、'd.js']
使用 grunt.config.get 函数检索 bazz 属性时,它不会返回字符串,因为 bazz 只包含对 foo 的模板引用。取而代之的是 foo 数组。
总结
现在,我们已经在机器上安装了 Node.js 和 npm。我们在全球安装了 npm 模块 grunt cli,为我们提供 grunt 可执行文件。我们了解 Node.js 中模块如何工作的基本前提,并且知道如何从 npm 中查找和安装模块。最后,我们了解 package.json 文件和 Gruntfile.js 文件的用途。在下一章中,我们将学习如何使用 Grunt 创建我们自己的任务。
[54]
版权属于:月萌API www.moonapi.com,转载请注明出处