JavaScript 现代 Web 开发框架教程(一)
一、Bower
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-0662-1_1) contains supplementary material, which is available to authorized users.
伟大的事情是由一系列小事情集合起来完成的。—文森特·梵高
包管理的概念,也称为依赖性管理,并不新鲜。此类别中的工具为开发人员提供了一种管理项目所依赖的各种第三方库的机制。广泛使用的例子包括
- NPM:node . js 的包管理器
- composer:PHP 中的依赖管理工具
- pip:PyPA 推荐的安装 Python 包的工具
- NuGet:微软开发平台的包管理器,包括。网
虽然包管理并不是一个新概念,但是最近才开始被广泛采用的一个实践是将这一概念应用于前端 web 素材的管理,这些素材是作为现代 web 应用的构建块的 JavaScript 库、样式表、字体、图标和图像。随着构建现代 web 应用的基础变得越来越复杂,对这种结构的需求变得越来越明显。曾经依赖于一小部分定义广泛的“一刀切”的第三方库(例如 jQuery)的 Web 应用,现在发现自己正在使用许多更小的库,每个库都有严格定义的用途。这种方法的好处包括更小的模块更容易测试,以及父应用的灵活性更高,可以更容易地扩展第三方库或在必要时完全替换它们。
本章旨在帮助您快速入门并使用 Bower,这是一个前端包管理器,其根源在于 Twitter 的开源计划。涵盖的主题包括
- 安装和配置 Bower
- 将 Bower 添加到项目中
- 查找、添加和删除包
- 语义版本控制
- 管理依赖链
- 创建 Bower 包
入门指南
与 Bower 的所有交互都通过命令行工具进行,该工具可以通过 npm 安装。如果您还没有安装 Bower,那么您应该在继续之前安装它,如清单 1-1 所示。
Listing 1-1. Installing the bower Command-Line Utility via npm
$ npm install -g bower
$ bower --version
1.3.12
Note
Node 的软件包管理器(npm)允许用户在两种环境中安装软件包:本地或全局。在本例中,bower安装在全局上下文中,该上下文通常是为命令行工具保留的。
配置 Bower
Bower 是通过一个(可选的)JSON 文件基于每个项目进行配置的,该文件存在于项目的根文件夹.bowerrc中。出于介绍的目的,我们将只查看这个文件中最频繁更改的设置(参见清单 1-2 )。
Listing 1-2. The .bowerrc File from This Chapter’s Sample Project
// example-bootstrap/.bowerrc
{
"directory": "./public/bower_components"
}
默认情况下,Bower 会将项目的依赖项存储在bower_components文件夹中。您可能想要更改这个位置,而directory设置允许您这样做。
清单
Bower 为开发人员提供了一个单一的入口点,从这里可以找到、添加、升级和删除第三方库。随着这些动作的发生,Bower 用项目依赖项的最新列表更新一个被称为“manifest”的 JSON 文件。清单 1-3 显示了本章示例项目的 Bower 清单。在这个例子中,Bower 意识到了一个单一的依赖,即引导 CSS 框架。
Listing 1-3. Bower Manifest for This Chapter’s Sample Project
// example-bootstrap/bower.json
{
"name": "example-bootstrap",
"version": "1.0.0",
"homepage": "https://github.com/username/project
"authors": [
"John Doe <john.doe@gmail.com>"
],
"dependencies": {
"bootstrap": "3.2.0"
}
}
如果我们通过删除public/bower_components文件夹意外地删除了我们项目的所有依赖项,我们可以通过发出一个命令很容易地将我们的项目恢复到它以前的状态,如下所示。这样做会导致 Bower 将其清单与我们项目的当前文件结构进行比较,确定缺少哪些依赖项,并恢复它们。
$ bower install
这种行为的结果是,我们可以选择在版本控制中忽略项目的/public/bower_components文件夹。通过只提交 Bower 的清单,而不是依赖项本身,我们项目的源代码可以保持在一个更干净的状态,只包含与我们自己的工作直接相关的文件。
Note
对于将项目的依赖关系置于版本控制之外是否是一个好主意,人们有不同的看法。一方面,这样做会产生一个更干净的存储库。另一方面,这也打开了潜在问题的大门。)遇到连接问题。普遍的共识似乎是,如果您正在处理一个“可部署的”项目(例如,一个应用,而不是一个模块),提交您的依赖项是首选的方法。否则,将项目的依赖项置于版本控制之外可能是个好主意。
创建新清单
当您第一次在项目中使用 Bower 时,通常最好让 Bower 为您创建一个新的清单,如下所示。之后,如果需要,您可以进一步修改它。
$ bower init
查找、添加和删除 Bower 包
Bower 的命令行工具为定位、安装和删除软件包提供了许多有用的命令。让我们看看这些命令是如何帮助简化管理项目外部依赖关系的过程的。
查找包
Bower 改进您的开发工作流程的主要方法之一是为您提供一个集中的注册表,从中可以找到第三方库。要搜索 Bower 注册表,只需将search参数传递给 Bower,后跟要搜索的关键字,如清单 1-4 所示。在本例中,只显示了返回的搜索结果列表中的一小段摘录。
Listing 1-4. Searching Bower for jQuery
$ bower search jquery
Search results:
jquery git://github.com/jquery/jquery.git
jquery-ui git://github.com/components/jqueryui
jquery.cookie git://github.com/carhartl/jquery-cookie.git
jquery-placeholder git://github.com/mathiasbynens/jquery-placeholder.git
添加包
每个搜索结果都包括注册软件包的名称,以及可以直接访问它的 GitHub 存储库的 URL。一旦我们找到了想要的包,我们就可以将它添加到我们的项目中,如清单 1-5 所示。
Listing 1-5. Adding jQuery to Our Project
$ bower install jquery --save
bower jquery#* cached git://github.com/jquery/jquery.git#2.1.3
bower jquery#* validate 2.1.3 against git://github.com/jquery/jquery.git#*
bower jquery#>= 1.9.1 cached git://github.com/jquery/jquery.git#2.1.3
bower jquery#>= 1.9.1 validate 2.1.3 against git://github.com/jquery/jquery.git#>= 1.9.1
bower jquery#>= 1.9.1 cached git://github.com/jquery/jquery.git#2.1.3
bower jquery#>= 1.9.1 validate 2.1.3 against git://github.com/jquery/jquery.git#>= 1.9.1
bower jquery#>= 1.9.1 install jquery#2.1.3
jquery#2.1.3 public/bower_components/jquery
Note
Bower 不托管任何与其注册表中包含的包相关联的文件;它让 GitHub 来承担这个责任。虽然可以在任何 URL 托管包,但是大多数公共包都可以在 GitHub 上找到。
请注意,在清单 1-5 中,我们将--save选项传递给了 Bower 的install命令。默认情况下,install命令会将请求的包添加到一个项目中,而不会更新它的清单。通过传递--save选项,我们指示 Bower 将这个包永久存储在其依赖项列表中。
清单 1-6 显示了本章示例项目中的 HTML。通过 Bower 将 jQuery 添加到我们的项目后,我们可以像加载任何其他库一样,通过一个script标签来加载它。
Listing 1-6. HTML from Our Sample Project That References the jQuery Package Just Added
// example-jquery/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bower Example</title>
</head>
<body>
<div id="container"></div>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#container').html('<p>Hello, world!</p>');
});
</script>
</body>
</html>
开发依赖性
默认情况下,Bower 安装的任何包都被认为是“生产”依赖项,但是这种行为可以通过传递--save-dev选项来覆盖。这样做会将所有已安装的软件包标记为“开发”依赖项。此类包仅用于开发目的,而非项目的最终用户。
一旦我们准备好将应用部署到生产环境中,我们就可以指示 Bower 只安装生产依赖项,如下所示,从而产生一个更精简的版本,其中不包含最终用户不感兴趣的无关文件。
$ bower install -- production
移除包
移除 Bower 包的过程很简单。和前面的例子一样,我们传递--save参数来更新 Bower 的清单,以反映这一变化:
$ bower uninstall jquery --save
语义版本控制
如果您要安装 jQuery(如清单 1-5 所示),然后查看项目的 Bower 清单的内容,您会看到类似于清单 1-7 的内容。
Listing 1-7. Semantic Version (Semver) Number
"dependencies": {
"jquery": "∼2.1.3"
}
我们在清单 1-7 中看到的版本号 2.1.3(暂时忽略字符)就是所谓的语义版本号(简称 semver)。语义版本化是一种标准,它描述了一种通用格式,开发人员可以使用这种格式为他们的项目分配版本号。格式如下所示:
Version X.Y.Z (Major.Minor.Patch)
语义版本化格式要求开发人员创建明确定义的(通过文档或者通过清晰的、自文档化的代码)API,为用户提供进入库的单一入口点。刚刚起步的新项目通常从版本 0.0.0 开始,并随着新版本的创建而逐步升级。版本号低于 1.0.0 的项目被认为是处于重度开发中,因此,允许对其 API 进行大范围的更改,而不改变其主要版本号。但是,版本号为 1.0.0 或更高版本号的项目受以下一组规则的指导,这些规则决定了应如何更改版本号:
- 当更新导致用户在以前版本中与项目 API 的交互方式发生重大变化时,项目的主版本号应该发生变化。
- 当新特性以向后兼容的方式添加到项目中时,项目的次版本号应该改变(即,现有的 API 没有被破坏)。
- 当引入向后兼容的错误修复时,项目的补丁版本号应该改变。
这些规则为开发人员提供了对任何两个版本之间发生的变化程度的洞察。随着我们的 Bower 清单的增长,以及我们开始向我们的项目添加越来越多的依赖项,这样的洞察力将会被证明是有用的。
Note
清单 1-7 中显示的字符告诉 Bower,无论何时运行install命令,都允许自动安装“相对接近”版本 2.1.3 的 jQuery 未来版本。如果在同一个句子中使用短语“相对接近”和“自动安装”让你毛骨悚然,你并不孤单。最佳实践建议您在引用 Bower 的依赖项时避免使用“∨X . y . z”格式。相反,您最好指定希望包含在项目中的依赖项的确切版本。随着未来更新的发布,您可以手动检查它们,并自行决定是否以及何时更新。本章后面的例子将遵循这个建议。
管理依赖链
开发人员从使用 Bower 中获得的主要好处之一是可以轻松地监控和集成对项目整个依赖链的更新。为了说明这一点,让我们看看本章的示例项目中包含的依赖项列表(参见清单 1-8 )。
Listing 1-8. Installing and Listing the Various Bower Packages Required by Our Sample Project
$ bower install
bower bootstrap#3.2.0 cached git://github.com/twbs/bootstrap.git#3.2.0
bower bootstrap#3.2.0 validate 3.2.0 against git://github.com/twbs/bootstrap.git#3.2.0
bower jquery#>= 1.9.0 cached git://github.com/jquery/jquery.git#2.1.3
bower jquery#>= 1.9.0 validate 2.1.3 against git://github.com/jquery/jquery.git#>= 1.9.0
bower bootstrap#3.2.0 install bootstrap#3.2.0
bower jquery#>= 1.9.0 install jquery#2.1.3
bootstrap#3.2.0 public/bower_components/bootstrap
ε──??″
jquery#2.1.3 public/bower_components/jquery
$ bower list
bower check-new Checking for new versions of the project dependencies..
example-bootstrap#1.0.0 /opt/example-bootstrap
ε□□□□□□□□□□□
ε──??″
多亏了 Bower,我们现在有了一个简单的图表来描述我们的项目所依赖的外部依赖,以及它们之间的关系。我们可以看到,我们有一个 Bootstrap 依赖项,它又有自己对 jQuery 的依赖项。Bower 还打印当前安装的每个组件的特定版本。
Note
许多第三方库不是完全独立的,它们有自己的依赖项。Bootstrap(依赖于 jQuery)就是这样一个例子。当添加这样一个包时,Bower 足够聪明,能够识别这些额外的依赖项,如果它们不存在的话,它会主动将它们添加到您的项目中。然而,需要注意的是,与更复杂的包管理器(例如 npm)不同,Bower 将所有的包存储在一个平面文件夹结构中,这意味着如果不小心的话,您会偶尔遇到版本冲突。
在清单 1-8 中,Bower 告诉我们,比我们的项目目前所依赖的版本(3.2.0)更新的 Bootstrap 版本(3.3.2)已经可用。我们可以通过修改我们项目的清单来引用这个新版本,并重新运行install命令来更新这个依赖,如清单 1-9 所示。
Listing 1-9. Installing Bower Packages After Having Updated the Version of jQuery Our Project Relies On
$ bower install
bower bootstrap#3.3.2 cached git://github.com/twbs/bootstrap.git#3.3.2
bower bootstrap#3.3.2 validate 3.3.2 against git://github.com/twbs/bootstrap.git#3.3.2
bower bootstrap#3.3.2 install bootstrap#3.3.2
bootstrap#3.3.2 public/bower_components/bootstrap
ε──??″
创建 Bower 包
到目前为止,我们的重点是将 Bower 集成到我们自己的项目中。我们已经在我们的项目中初始化了 Bower,并且发现了我们可以如何查找、添加和删除包。然而,在某一点上,你会希望发现自己想要与他人共享自己的包。为此,您需要确保遵循一些简单的准则,从选择有效的名称开始。
选择一个有效的名称
您需要为您的包选择一个在 Bower 的公共注册表中唯一的名称。使用 Bower 的search命令来查找您想要的名称是否可用。其他要求包括
- 该名称应为“slug”格式;比如
my-unique-project。 - 名称应该全部小写。
- 只允许字母数字字符、点和破折号。
- 该名称应以字母字符开头和结尾。
- 不允许连续的点和破折号。
- 确定名称后,相应地更新项目的
bower.json文件的内容。
使用 Semver Git 标签
在本章的前面,我们看了一下语义版本化的概念,这是一个为项目分配有意义的版本号的通用标准。您将希望确保您遵循这个标准,因为这将允许您的软件包的消费者跟踪和集成您未来的更改。
如果您想要共享的包刚刚开始,合适的版本号应该是 0.0.0。当您提交将来的更改并创建新的版本时,您可以根据更新的程度适当地增加该值。当您确定您的项目已经达到了它的第一个“稳定的”里程碑时,将您的版本号更新到 1.0.0 来反映这种状态。
你的项目的每个版本号在 GitHub 上都应该有一个对应的标签。正是 GitHub 标签和您的包的版本之间的这种关系允许消费者在他们的项目中引用特定的版本。
假设您已经将代码提交给 GitHub,参见清单 1-10 中的示例,了解如何创建您的第一个标签。
Listing 1-10. Creating Your First Semver Git Tag
$ git tag -a 0.0.1 -m "First release."
$ git push origin 0.0.1
将包发布到注册表
既然我们已经为我们的包选择了一个合适的名称,并分配了一个版本号(以及 GitHub 上相应的标签),现在是时候将我们的包发布到公共的 Bower registry 了:
$ bower register``my``-``package``-namehttps://github.com/username/``my``-``package``-name.git
Note
请记住,Bower 旨在作为其他开发人员可以在自己的项目中使用的库和组件的集中注册中心。它并不打算作为整个应用的分发机制。
摘要
Bower 是一个简单的命令行工具,它简化了一些与管理前端素材相关的繁琐任务。与来自其他平台的众所周知的包管理器(例如 Node 的 npm)不同,Bower 不是为处理任何一个平台或语言的特定需求而设计的;相反,它倾向于用一种相当通用的方法来处理包管理的概念。创建 Bower 的开发人员有意创建一个非常简单的工具来管理各种各样的前端素材——不仅仅是代码,还有样式表、字体、图像和其他不可预见的未来依赖。
开发外部依赖性很小的普通 web 应用的开发人员可能会发现 Bower 带来的好处没有什么价值。也就是说,琐碎的 web 应用有迅速演变成复杂的 web 应用的趋势,随着这一过程的发生,开发人员通常会逐渐意识到 Bower 的好处。
无论您认为您的项目有多复杂(或多简单),我们都鼓励您尽早考虑将 Bower 集成到您的工作流程中。痛苦的经历告诉我们——项目本身。犯了结构过小的错误,你就有可能造成不断增加的“技术债务”负担,最终你必须为此付出代价。在这些不受欢迎的选择之间取得微妙平衡的过程既是一门科学,也是一门艺术。这也是一个从未完全学会的过程,但必须随着我们的贸易工具的变化而不断调整。
二、Grunt
我很懒。但是是懒人发明了轮子和自行车,因为他们不喜欢走路或搬运东西。—波兰前总统莱赫·瓦文萨
在《编程 Perl》一书中,拉里·沃尔(这种语言的著名创造者)提出了这样一个观点,即所有成功的程序员都有三个重要特征:懒惰、急躁和傲慢。乍一看,这些特点听起来都很消极,但深入一点,你会发现他的说法中隐藏的含义:
- 懒惰:懒惰的程序员讨厌重复自己。因此,他们倾向于投入大量精力来创建有用的工具,为他们执行重复的任务。他们也倾向于很好地记录这些工具,以省去以后回答关于它们的问题的麻烦。
- 不耐烦:不耐烦的程序员已经学会对他们的工具期望过高。这种期望教会他们创建软件,不仅对用户的需求做出反应,而且实际上试图预测这些需求。
- 傲慢:优秀的程序员对自己的工作非常自豪。正是这种自豪感促使他们编写别人不愿批评的软件——这是我们都应该努力争取的工作。
在这一章中,我们将关注这三个特征中的第一个,懒惰,以及 Grunt,一个流行的 JavaScript“任务运行器”,它通过为开发人员提供自动化软件开发中经常出现的重复构建任务的工具包来支持开发人员培养这一特征,例如:
- 脚本和样式表编译和缩小
- 测试
- 林挺
- 数据库迁移
- 部署
换句话说,Grunt 帮助那些努力工作得更聪明而不是更努力的开发人员。如果你对这个想法感兴趣,请继续读下去。当你读完这一章后,你将会很快掌握咕噜。在本章中,您将学习如何执行以下操作:
- 创建可配置的任务,使几乎每个项目都伴随的软件开发的重复方面自动化
- 使用 Grunt 提供的简单而强大的抽象与文件系统交互
- 发布普通插件,其他开发者可以从中受益,也可以为之做出贡献
- 利用 Grunt 现有的社区支持插件库,在撰写本文时已经有超过 4400 个例子
安装咕噜声
在继续之前,您应该确保已经安装了 Grunt 的命令行工具。作为一个 npm 包,安装过程如清单 2-1 所示。
Listing 2-1. Installing the grunt Command-Line Utility via npm
$ npm install -g grunt-cli
$ grunt --version
grunt-cli v0.1.13
Grunt 是如何工作的
Grunt 为开发人员提供了一个工具包,用于创建执行重复性项目任务的命令行工具。这类任务的例子包括 JavaScript 代码的缩减和 Sass 样式表的编译,但是 Grunt 的使用没有限制。Grunt 可以用来创建满足单个项目特定需求的简单任务,也就是您不打算共享或重用的任务,但是 Grunt 真正的强大之处在于它能够将任务打包成可重用的插件,然后其他人可以发布、共享、使用和改进这些插件。在撰写本文时,有超过 4400 个这样的插件。
Grunt tick 由四个核心组件组成,我们现在将介绍这四个组件。
格朗蒂尔
Grunt 的核心是 Gruntfile,一个保存为项目根目录下的Gruntfile.js(见清单 2-2 )的节点模块。在这个文件中,我们可以加载 Grunt 插件,创建我们自己的定制任务,并根据我们项目的需要配置它们。每次运行 Grunt 时,它的第一个任务就是从这个模块中检索行军命令。
Listing 2-2. Sample Gruntfile
// example-starter/Gruntfile.js
module.exports = function(grunt) {
/**
* Configure the various tasks and plugins that we’ll be using
*/
grunt.initConfig({
/* Grunt’s 'file' API provides developers with helpful abstractions for
interacting with the file system. We’ll take a look at these in greater
detail later in the chapter. */
'pkg': grunt.file.readJSON('package.json'),
'uglify': {
'development': {
'files': {
'build/app.min.js': ['src/app.js', 'src/lib.js']
}
}
}
});
/**
* Grunt plugins exist as Node packages, published via npm. Here, we load the
* 'grunt-contrib-uglify' plugin, which provides a task for merging and minifying
* a project’s source code in preparation for deployment.
*/
grunt.loadNpmTasks('grunt-contrib-uglify');
/**
* Here we create a Grunt task named 'default' that does nothing more than call
* the 'uglify' task. In other words, this task will serve as an alias to
* 'uglify'. Creating a task named 'default' tells Grunt what to do when it is
* run from the command line without any arguments. In this example, our 'default'
* task calls a single, separate task, but we could just as easily have called
* multiple tasks (to be run in sequence) by adding multiple entries to the array
* that is passed.
*/
grunt.registerTask('default', ['uglify']);
/**
* Here we create a custom task that prints a message to the console (followed by
* a line break) using one of Grunt’s built-in methods for providing user feedback.
* We’ll look at these in greater detail later in the chapter.
*/
grunt.registerTask('hello-world', function() {
grunt.log.writeln('Hello, world.');
});
};
任务
任务是 Grunt 的基本构件,只不过是通过 Grunt 的registerTask()方法注册了指定名称的函数。在清单 2-2 中,显示了一个简单的hello-world任务,它向控制台打印一条消息。这个任务可以从命令行调用,如清单 2-3 所示。
Listing 2-3. Running the hello-world Task Shown in Listing 2-2
$ grunt hello-world
Running "hello-world" task
Hello, world.
Done, without errors.
多个任务也可以用一个命令按顺序运行,如清单 2-4 所示。每个任务将按照传递的顺序运行。
Listing 2-4. Running Multiple Grunt Tasks in Sequence
$ grunt hello-world uglify
Running "hello-world" task
Hello, world.
Running "uglify:development" (uglify) task
>> 1 file created.
Done, without errors.
我们刚刚看到的hello-world任务是一个基本的、独立的繁重任务的例子。此类任务可用于实现特定于单个项目需求的简单操作,您不打算重用或共享这些操作。然而,大多数时候,你会发现自己不是与独立的任务交互,而是与打包成 Grunt 插件并发布到 npm 的任务交互,以便其他人可以重用它们并为它们做出贡献。
插件
Grunt 插件是可配置任务的集合(作为 npm 包发布),可以在多个项目中重用。存在成千上万个这样的插件。在清单 2-2 中,Grunt 的loadNpmTasks()方法用于加载grunt-contrib-uglify节点模块,这是一个 Grunt 插件,它将一个项目的 JavaScript 代码合并成一个适合部署的小型文件。
Note
可以在 http://gruntjs.com/plugins 找到所有可用的 Grunt 插件列表。名称以contrib-为前缀的插件由 Grunt 背后的开发者官方维护。
配置
Grunt 以强调“配置胜于代码”而闻名:任务和插件的创建,其功能由每个项目中指定的配置定制。正是这种代码与配置的分离,使得开发人员能够创建易于被其他人重用的插件。在这一章的后面,我们将会看到配置 Grunt 插件和任务的各种方法。
向您的项目添加 Grunt
在本章的前面,我们通过将grunt-cli npm 包作为一个全局模块来安装 Grunt 的命令行工具。我们现在应该可以从命令行访问grunt工具了,但是我们仍然需要为我们打算使用它的每个项目添加一个本地grunt依赖项。下面显示了从项目的根文件夹中调用的命令。这个例子假设 npm 已经在项目中初始化,并且一个package.json文件已经存在。
$ npm install grunt --save-dev
我们项目的package.json文件现在应该包含一个类似于清单 2-5 所示的grunt条目。
Listing 2-5. Our Project’s Updated package.json File
// example-tasks/package.json
{
"name": "example-tasks",
"version": "1.0.0",
"devDependencies": {
"grunt": "0.4.5"
}
}
将 Grunt 与我们的项目集成的最后一步是创建一个 Gruntfile(参见清单 2-6 ),它应该保存在项目的根文件夹中。在我们的 Gruntfile 中,有一个方法叫做loadTasks(),这将在下一节中讨论。
Listing 2-6. Contents of Our Project’s Gruntfile
// example-tasks/Gruntfile.js
module.exports = function(grunt) {
grunt.loadTasks('tasks');
};
保持正常的咕噜声结构
我们希望当你读完这一章的时候,你会发现 Grunt 是一个很有价值的工具,可以自动完成你在日常工作流程中遇到的许多重复、乏味的任务。也就是说,如果我们告诉你我们对咕噜声的最初反应是积极的,那我们就是在撒谎。事实上,一开始我们对这个工具很反感。为了帮助解释原因,让我们来看看 Grunt 官方文档中突出显示的 Grunt 文件(参见清单 2-7 )。
Listing 2-7. Example Gruntfile Provided by Grunt’s Official Documentation
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
options: {
separator: ';'
},
dist: {
src: ['src/**/*.js'],
dest: 'dist/<%= pkg.name %>.js'
}
},
uglify: {
options: {
banner: '/*! <%= grunt.template.today("dd-mm-yyyy") %> */\n'
},
dist: {
files: {
'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
}
}
},
qunit: {
files: ['test/**/*.html']
},
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
// options here to override JSHint defaults
globals: {
jQuery: true,
console: true,
module: true,
document: true
}
}
},
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint', 'qunit']
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('test', ['jshint', 'qunit']);
grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
};
清单 2-7 中显示的 Gruntfile 是一个相对简单的项目。我们已经发现这个例子有点笨拙,但是在更大的项目中,我们已经看到这个文件膨胀到这个大小的许多倍。结果是一片混乱,难以阅读和维护。有经验的开发人员绝不会以一种将不相关领域的功能组合到一个单一的整体文件中的方式编写他们的代码,所以我们为什么要以不同的方式处理我们的任务运行程序呢?
保持一个合理的 Grunt 结构的秘密在于 Grunt 的loadTasks()函数,如清单 2-6 所示。在这个例子中,tasks参数引用了一个相对于我们项目的 Gruntfile 的tasks文件夹。一旦这个方法被调用,Grunt 将加载并执行它在这个文件夹中找到的每个节点模块,每次传递一个对grunt对象的引用。这种行为为我们提供了将项目的普通配置组织成一系列独立模块的机会,每个模块负责加载和配置单个任务或插件。清单 2-8 中显示了其中一个较小模块的示例。这个任务可以通过从命令行运行grunt uglify来执行。
Listing 2-8. Example Module (uglify.js) Within Our New tasks Folder
// example-tasks/tasks/uglify.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.config('uglify', {
'options': {
'banner': '/*! <%= grunt.template.today("dd-mm-yyyy") %> */\n'
},
'dist': {
'files': {
'dist/app.min.js': ['src/index.js']
}
}
});
};
使用任务
如前所述,任务是构建 Grunt 的基础——一切都从这里开始。您很快就会发现,Grunt 插件只不过是一个或多个打包到节点模块中并通过 npm 发布的任务。我们已经看到了一些演示创建基本任务的例子,所以让我们来看看一些可以帮助我们充分利用它们的附加特性。
管理配置
Grunt 的config()方法既作为配置的“获取者”又作为配置的“设置者”。在清单 2-9 中,我们看到一个基本的 Grunt 任务如何通过使用这个方法来访问它的配置。
Listing 2-9. Managing Configuration Within a Basic Grunt Task
module.exports = function(grunt) {
grunt.config('basic-task', {
'message': 'Hello, world.'
});
grunt.registerTask('basic-task', function() {
grunt.log.writeln(grunt.config('basic-task.message'));
});
};
Note
在清单 2-9 中,“点符号”用于访问嵌套的配置值。同样,点符号可以用来设置嵌套的配置值。如果 Grunt 在配置对象中遇到一个不存在的路径,Grunt 将创建一个新的空对象,而不会抛出错误。
任务描述
随着时间的推移,项目有增加复杂性的趋势。伴随着这种额外的复杂性,通常会出现新的繁重任务。随着新任务的增加,经常很容易忘记哪些任务是可用的,它们做什么,以及它们是如何被调用的。幸运的是,Grunt 通过为我们的任务分配描述,为我们提供了解决这个问题的方法,如清单 2-10 所示。
Listing 2-10. Assigning a Description to a Grunt Task
// example-task-description/Gruntfile.js
module.exports = function(grunt) {
grunt.config('basic-task', {
'message': 'Hello, world.'
});
grunt.registerTask('basic-task', 'This is an example task.', function() {
grunt.log.writeln(grunt.config('basic-task.message'));
});
grunt.registerTask('default', 'This is the default task.', ['basic-task']);
};
通过向registerTask()方法传递一个额外的参数,Grunt 允许我们为正在创建的任务提供描述。当从命令行请求帮助时,Grunt 会很有帮助地提供这些信息,如清单 2-11 所示,其中包括 Grunt 提供的信息的摘录。
Listing 2-11. Requesting Help from the Command Line
$ grunt --help
...
Available tasks
basic-task This is an example task.
default This is the default task.
...
异步任务
默认情况下,普通任务应该同步运行。任务的函数一返回,就被认为完成了。然而,有时您会发现自己在一个任务中与其他异步方法交互,这些方法必须首先完成,然后您的任务才能将控制权交还给 Grunt。这个问题的解决方案如清单 2-12 所示。在一个任务中,对async()方法的调用将通知 Grunt 它异步执行。该方法将返回一个回调函数,在我们的任务完成后调用。在此之前,Grunt 将暂停任何附加任务的执行。
Listing 2-12. Asynchronous Grunt Task
// example-async/tasks/list-files.js
var glob = require('glob');
module.exports = function(grunt) {
grunt.registerTask('list-files', function() {
/**
* Grunt will wait until we call the done() function to indicate that our
* asynchronous task is complete.
*/
var done = this.async();
glob('*', function(err, files) {
if (err) {
grunt.fail.fatal(err);
}
grunt.log.writeln(files);
done();
});
});
};
任务相关性
复杂的工作流程最好被认为是一系列协同工作以产生最终结果的步骤。在这种情况下,指定一个任务需要一个或多个单独的任务在它之前通常会很有帮助,如清单 2-13 所示。
Listing 2-13. Declaring a Task Dependency
// example-task-dependency/tasks/step-two.js
module.exports = function(grunt) {
grunt.registerTask('step-two', function() {
grunt.task.requires('step-one');
});
};
在这个例子中,step-two任务要求step-one任务在继续之前先运行。任何直接调用step-two的尝试都会导致错误,如清单 2-14 所示。
Listing 2-14. Grunt Reporting an Error When a Task Is Called Before Any Tasks on Which It Depends Have Run
$ grunt step-two
Running "step-two" task
Warning: Required task "step-one" must be run first. Use --force to continue.
Aborted due to warnings.
多任务
除了基本任务之外,Grunt 还支持它所谓的“多任务”多重任务很容易成为 Grunt 最复杂的方面,所以如果你一开始就发现自己很困惑,你并不孤单。然而,在回顾了几个例子之后,它们的目的应该开始变得清晰起来——这时,你就可以很好地掌握 Grunt 了。
在我们继续之前,让我们先看一个简单的例子(参见清单 2-15 ),它展示了一个简单的多任务及其配置。
Listing 2-15. Grunt Multi-Task
// example-list-animals/tasks/list-animals.js
module.exports = function(grunt) {
/**
* Our multi-task’s configuration object. In this example, 'mammals'
* and 'birds' each represent what Grunt refers to as a 'target.'
*/
grunt.config('list-animals', {
'mammals': {
'animals': ['Cat', 'Zebra', 'Koala', 'Kangaroo']
},
'birds': {
'animals': ['Penguin', 'Sparrow', 'Eagle', 'Parrot']
}
});
grunt.registerMultiTask('list-animals', function() {
grunt.log.writeln('Target:', this.target);
grunt.log.writeln('Data:', this.data);
});
};
多任务非常灵活,因为它们被设计为支持单个项目中的多个配置(称为“目标”)。清单 2-15 所示的多任务有两个目标:mammals和birds。该任务可以针对特定目标运行,如清单 2-16 所示。
Listing 2-16. Running the Grunt Multi-Task Shown in Listing 2-15 Against a Specific Target
$ grunt list-animals:mammals
Running "list-animals:mammals" (list-animals) task
Target: mammals
Data: { animals: [ 'Cat', 'Zebra', 'Koala', 'Kangaroo' ] }
Done, without errors.
多任务也可以在没有任何参数的情况下被调用,在这种情况下,它们被执行多次,每个可用的目标执行一次。清单 2-17 显示了在没有指定目标的情况下调用这个任务的结果。
Listing 2-17. Running the Multi-Task Shown in Listing 2-15 Without Specifying a Target
$ grunt list-animals
Running "list-animals:mammals" (list-animals) task
Target: mammals
Data: { animals: [ 'Cat', 'Zebra', 'Koala', 'Kangaroo' ] }
Running "list-animals:birds" (list-animals) task
Target: birds
Data: { animals: [ 'Penguin', 'Sparrow', 'Eagle', 'Parrot' ] }
在这个例子中,我们的多任务运行了两次,每个可用目标运行一次(mammals和birds)。注意在清单 2-15 中,我们在多任务中引用了两个属性:this.target和this.data。这些属性允许我们的多任务获取当前运行的目标的信息。
多任务选项
在多任务的配置对象中,存储在options键下的任何值(参见清单 2-18 )都会得到特殊处理。
Listing 2-18. Grunt Multi-Task with Configuration Options
// example-list-animals-options/tasks/list-animals.js
module.exports = function(grunt) {
grunt.config('list-animals', {
'options': {
'format': 'array'
},
'mammals': {
'options': {
'format': 'json'
},
'animals': ['Cat', 'Zebra', 'Koala', 'Kangaroo']
},
'birds': {
'animals': ['Penguin', 'Sparrow', 'Eagle', 'Parrot']
}
});
grunt.registerMultiTask('list-animals', function() {
var options = this.options();
switch (options.format) {
case 'array':
grunt.log.writeln(this.data.animals);
break;
case 'json':
grunt.log.writeln(JSON.stringify(this.data.animals));
break;
default:
grunt.fail.fatal('Unknown format: ' + options.format);
break;
}
});
};
多任务选项为开发人员提供了一种为任务定义全局选项的机制,然后可以在目标级别覆盖这些选项。在本例中,列出动物('array')的全局格式是在任务级别定义的。目标mammals已经选择覆盖这个值('json'),而任务birds没有。因此,mammals将显示为 JSON,而birds由于继承了全局选项,将显示为数组。
你将会遇到的绝大多数 Grunt 插件都可以配置为多任务。这种方法提供的灵活性允许您在不同的情况下以不同的方式应用相同的任务。一个经常遇到的场景涉及到为每个构建环境创建单独的目标。例如,在编译应用时,您可能希望根据是针对本地开发环境进行编译还是准备发布到生产环境来修改任务的行为。
配置模板
Grunt 配置对象支持模板字符串的嵌入,模板字符串可以用来引用其他配置值。Grunt 喜欢的模板格式遵循 Lodash 和下划线工具库的格式,这将在后面的章节中详细介绍。关于如何使用该功能的示例,请参见清单 2-19 和清单 2-20 。
Listing 2-19. Sample Gruntfile That Stores the Contents of Its Project’s package.json File Under the pkg Key Within Grunt’s Configuration Object
// example-templates/Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
'pkg': grunt.file.readJSON('package.json')
});
grunt.loadTasks('tasks');
grunt.registerTask('default', ['test']);
};
Listing 2-20. A Subsequently Loaded Task with Its Own Configuration That Is Able to Reference Other Configuration Values Through the Use of Templates
// example-templates/tasks/test.js
module.exports = function(grunt) {
grunt.config('test', {
'banner': '<%= pkg.name %>-<%= pkg.version %>'
});
grunt.registerTask('test', function() {
grunt.log.writeln(grunt.config('test.banner'));
});
};
清单 2-19 显示了一个样例 Gruntfile,它使用几个与文件系统交互的内置方法之一来加载项目的package.json文件的内容,这些方法将在本章后面详细讨论。这个文件的内容存储在 Grunt 配置对象的pkg键下。在清单 2-20 中,我们看到一个任务能够通过使用配置模板直接引用这些信息。
命令行选项
可以使用以下格式将附加选项传递给 Grunt:
$ grunt count --count=5
清单 2-21 中的例子展示了一个普通任务如何通过grunt.option()方法访问这些信息。调用该任务的结果如清单 2-22 所示。
Listing 2-21. Simple Grunt Task That Counts to the Specified Number
// example-options/tasks/count.js
module.exports = function(grunt) {
grunt.registerTask('count', function() {
var limit = parseInt(grunt.option('limit'), 10);
if (isNaN(limit)) grunt.fail.fatal('A limit must be provided (e.g. --limit=10)');
console.log('Counting to: %s', limit);
for (var i = 1; i <= limit; i++) console.log(i);
});
};
Listing 2-22. Result of Calling the Task Shown in Listing 2-21
$ grunt count --limit=5
Running "count" task
Counting to: 5
1
2
3
4
5
Done, without errors.
提供反馈
Grunt 提供了许多在任务执行过程中向用户提供反馈的内置方法,其中一些您已经在本章中看到过。虽然我们不会在这里列出所有的例子,但是在表 2-1 中可以找到一些有用的例子。
表 2-1。
Useful Grunt Methods for Displaying Feedback to the User
| 方法 | 描述 | | --- | --- | | `grunt.log.write()` | 将消息打印到控制台 | | `grunt.log.writeln()` | 向控制台打印一条消息,后跟一个换行符 | | `grunt.log.oklns()` | 将成功消息打印到控制台,后跟一个换行符 | | `grunt.log.error()` | 向控制台输出一条错误消息,后跟一个换行符 | | `grunt.log.subhead()` | 将粗体消息打印到控制台,后跟换行符 | | `grunt.log.debug()` | 仅当`--debug`标志通过时,才打印一条消息到控制台 |处理错误
在任务执行过程中,可能会出现错误。当他们这样做时,知道如何恰当地处理他们是很重要的。当遇到错误时,开发人员应该利用 Grunt 的error API,它很容易使用,因为它只提供了两种方法,如表 2-2 所示。
表 2-2。
Methods Available via Grunt’s error API
与文件系统交互
作为一个构建工具,Grunt 的大多数插件都以这样或那样的方式与文件系统交互,这并不奇怪。鉴于其重要性,Grunt 提供了有用的抽象,允许开发人员用最少的样板代码与文件系统进行交互。
虽然我们不会在这里列出所有的方法,表 2-3 显示了 Grunt 的file API 中最常用的几种方法。
表 2-3。
Useful Grunt Methods for Interacting with the File System
| 方法 | 描述 | | --- | --- | | `grunt.file.read()` | 读取并返回文件的内容 | | `grunt.file.readJSON()` | 读取文件内容,将数据解析为 JSON,并返回结果 | | `grunt.file.write()` | 将指定的内容写入文件,必要时创建中间目录 | | `grunt.file.copy()` | 将源文件复制到目标路径,必要时创建中间目录 | | `grunt.file.delete()` | 删除指定的文件路径;递归删除文件和文件夹 | | `grunt.file.mkdir()` | 创建一个目录,以及任何缺失的中间目录 | | `grunt.file.recurse()` | 递归到一个目录中,对找到的每个文件执行回调 |源-目标映射
许多与文件系统交互的繁重任务严重依赖于源-目的地映射的概念,这种格式描述了一组要处理的文件和每个文件对应的目的地。构建这样的映射可能会很乏味,但是谢天谢地,Grunt 提供了解决这一需求的有用快捷方式。
想象一下,您正在处理一个根目录下有一个public文件夹的项目。在这个文件夹中是项目部署后将通过 Web 提供的文件,如清单 2-23 所示。
Listing 2-23. Contents of an Imaginary Project’s public Folder
// example-iterate1
.
ε──??″
ε──??″
■??]
■??]
ε──??″
如您所见,我们的项目有一个包含三个文件的images文件夹。了解了这一点,让我们看看 Grunt 可以帮助我们遍历这些文件的几种方式。
在清单 2-24 中,我们发现一个单调的多任务,类似于我们最近被介绍的那些。这里的关键区别是在我们的任务配置中有一个src键。Grunt 特别关注包含这个键的多任务配置,我们很快就会看到。当出现src键时,Grunt 在我们的任务中提供一个this.files属性,该属性提供一个数组,该数组包含通过node-glob模块找到的每个匹配文件的路径。该任务的输出如清单 2-25 所示。
Listing 2-24. Grunt Multi-Task with a Configuration Object Containing an src Key
// example-iterate1/tasks/list-files.js
module.exports = function(grunt) {
grunt.config('list-files', {
'images': {
'src': ['public/**/*.jpg', 'public/**/*.png']
}
});
grunt.registerMultiTask('list-files', function() {
this.files.forEach(function(files) {
grunt.log.writeln('Source:', files.src);
});
});
};
Listing 2-25. Output from the Grunt Task Shown in Listing 2-24
$ grunt list-files
Running "list-files:images" (list-files) task
Source: [ 'publimg/cat1.jpg',
'publimg/cat2.jpg',
'publimg/cat3.png' ]
Done, without errors.
src配置属性和this.files多任务属性的结合为开发人员提供了一个简洁的语法来迭代多个文件。我们刚刚看到的这个人为的例子相当简单,但是 Grunt 也为处理更复杂的场景提供了额外的选项。让我们来看看。
与清单 2-24 中用于配置我们的任务的 src 键相反,清单 2-26 中的例子演示了文件数组的使用——这是一种稍微冗长但更强大的选择文件的格式。这种格式接受额外的选项,允许我们更好地调整我们的选择。特别重要的是扩展选项,您将在清单 2-27 中看到。由于使用了扩展选项,请密切注意输出与清单 2-26 的不同之处。
Listing 2-26. Iterating Through Files Using the “Files Array” Format
// example-iterate2/tasks/list-files.js
module.exports = function(grunt) {
grunt.config('list-files', {
'images': {
'files': [
{
'cwd': 'public',
'src': ['**/*.jpg', '**/*.png'],
'dest': 'tmp',
'expand': true
}
]
}
});
grunt.registerMultiTask('list-files', function() {
this.files.forEach(function(files) {
grunt.log.writeln('Source:', files.src);
grunt.log.writeln('Destination:', files.dest);
});
});
};
Listing 2-27. Output from the Grunt Task shown in Listing 2-26
$ grunt list-files
Running "list-files:images" (list-files) task
Source: [ 'publimg/cat1.jpg' ]
Destination: timg/cat1.jpg
Source: [ 'publimg/cat2.jpg' ]
Destination: timg/cat2.jpg
Done, without errors.
当expand选项与dest选项配对时,它指示 Grunt 为找到的每个条目遍历一次任务的this.files.forEach循环,在循环中我们可以找到相应的dest属性。使用这种方法,我们可以轻松地创建源-目标映射,用于将文件从一个位置复制(或移动)到另一个位置。
监视文件更改
Grunt 最受欢迎的插件之一grunt-contrib-watch,让 Grunt 能够在创建、修改或删除匹配指定模式的文件时运行预定义的任务。当与其他任务结合使用时,grunt-contrib-watch使开发人员能够创建强大的工作流,自动执行以下操作
- 检查 JavaScript 代码中的错误(即“林挺”)
- 编译 Sass/L 样式表
- 运行单元测试
让我们看几个例子,展示这样的工作流付诸行动。
自动化 JavaScript 林挺
清单 2-28 显示了一个基本的咕噜声设置,与本章中已经显示的相似。注册了一个default任务,作为watch任务的别名,允许我们通过简单地从命令行运行$ grunt来开始观察我们项目中的变化。在这个例子中,Grunt 将观察src文件夹中的变化。当它们发生时,jshint任务被触发,它将扫描我们项目的src文件夹,搜索 JavaScript 错误。
Listing 2-28. Automatically Checking for JavaScript Errors As Changes Occur
// example-watch-hint/Gruntfile.js
module.exports = function(grunt) {
grunt.loadTasks('tasks');
grunt.registerTask('default', ['watch']);
};
// example-watch-hint/tasks/jshint.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.config('jshint', {
'options': {
'globalstrict': true,
'node': true,
'scripturl': true,
'browser': true,
'jquery': true
},
'all': [
'src/**/*.js'
]
});
};
// example-watch-hint/tasks/watch.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.config('watch', {
'js': {
'files': [
'src/**/*'
],
'tasks': ['jshint'],
'options': {
'spawn': true
}
}
});
};
自动化 Sass 样式表编译
清单 2-29 显示了一个例子,Grunt 被指示观察我们项目的变化。然而,这一次,Grunt 被配置为观察我们项目的 Sass 样式表,而不是观察我们的 JavaScript。随着变化的发生,grunt-contrib-compass插件被调用,它将我们的样式表编译成它们的最终形式。
Listing 2-29. Automatically Compiling Sass Stylesheets As Changes Occur
// example-watch-sass/Gruntfile.js
module.exports = function(grunt) {
grunt.loadTasks('tasks');
grunt.registerTask('default', ['watch']);
};
// example-watch-sass/tasks/compass.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-compass');
grunt.config('compass', {
'all': {
'options': {
'httpPath': '/',
'cssDir': 'public/css',
'sassDir': 'scss',
'imagesDir': 'public/images',
'relativeAssets': true,
'outputStyle': 'compressed'
}
}
});
};
// example-watch-compass/tasks/watch.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.config('watch', {
'scss': {
'files': [
'scss/**/*'
],
'tasks': ['compass'],
'options': {
'spawn': true
}
}
});
};
Note
为了让这个示例正常工作,您必须安装 Compass,这是一个开源的 CSS 创作框架。您可以在 http://compass-style.org/install 找到关于如何安装指南针的更多信息。
自动化单元测试
我们关于grunt-contrib-watch的最后一个例子是关于单元测试的。在清单 2-30 中,我们看到一个观察我们项目的 JavaScript 变化的 Gruntfile。随着这些变化的发生,在 Grunt 的grunt-mocha-test插件的帮助下,我们项目的单元测试立即被触发。
Listing 2-30. Automatically Running Unit Tests As Changes Occur
// example-watch-test/Gruntfile.js
module.exports = function(grunt) {
grunt.loadTasks('tasks');
grunt.registerTask('default', ['watch']);
};
// example-watch-test/tasks/mochaTest.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-mocha-test');
grunt.config('mochaTest', {
'test': {
'options': {
'reporter': 'spec'
},
'src': ['test/**/*.js']
}
});
};
// example-watch-test/tasks/watch.js
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.config('watch', {
'scss': {
'files': [
'src/**/*.js'
],
'tasks': ['mochaTest'],
'options': {
'spawn': true
}
}
});
};
创建插件
一个大型的社区支持插件库是 Grunt 真正闪耀的地方——这个库可以让你立即从 Grunt 中受益,而不需要从头开始创建复杂的任务。如果您需要在您的项目中自动化一个构建过程,很有可能有人已经完成了“繁重”的工作(zing!)给你。
在这一节中,您将发现如何通过自己创建的 Grunt 插件来回馈社区。
入门指南
你首先要做的事情之一是创建一个公共的 GitHub 库来存储你的新插件。我们将要引用的例子包含在本书的源代码中。
一旦您的新存储库准备就绪,将其克隆到您的计算机上。接下来,按照本章前面的“将 Grunt 添加到您的项目中”一节中概述的相同步骤,在其中初始化 Grunt。之后,您的新 Grunt 插件的文件结构应该类似于清单 2-31 中所示。
Listing 2-31. File Structure of Your New Grunt Plugin
.
■??]
■??]
■??]
ε──??″
Note
这里要注意的最重要的一点是,创建 Grunt 插件不需要特殊的结构或知识(除了本章已经介绍过的)。这个过程反映了将 Grunt 集成到一个现有项目中的过程——创建一个 Gruntfile 来加载任务以及任务本身。一旦发布到 npm,其他 Grunt 项目将能够加载你的插件,就像本章中提到的其他插件一样。
创建任务
举例来说,让我们创建一个 Grunt 插件,它能够生成一个报告,详细说明一个项目中包含的文件的类型、大小和数量。清单 2-32 中显示了一个演示这个插件配置的例子。
Listing 2-32. Example Demonstrating the Configuration of Our Plugin
// example-plugin/Gruntfile.js
module.exports = function(grunt) {
grunt.config('file-report', {
'options': {
},
'public': {
'src': ['public/**/*']
},
'images': {
'src': ['public/**/*.jpg', 'public/**/*.png', 'public/**/*.gif']
}
});
grunt.loadNpmTasks('grunt-file-reporter');
grunt.registerTask('default', ['file-report']);
};
我们插件的源代码如清单 2-33 所示。在我们的插件中,注册了一个名为file-report的 Grunt 多任务。当被调用时,任务将遍历清单 2-32 中指定的各种目标文件。当它这样做时,插件将编译一个报告,详细说明它找到的文件的类型、数量和大小。
Listing 2-33. Source Code for Our Plugin
// example-plugin/node_modules/grunt-file-reporter/Gruntfile.js
var fs = require('fs');
var filesize = require('filesize');
var _ = require('lodash');
_.mixin(require('underscore.string'));
module.exports = function(grunt) {
var mime = require('mime');
var Table = require('cli-table');
grunt.registerMultiTask('file-report', 'Generates a report of file types & sizes used within a project', function() {
var report = {
'mimeTypes': {},
'largest': null,
'smallest': null
};
var table = new Table({
'head': ['Content Type', 'Files Found', 'Total Size',
'Average Size', 'Largest', 'Smallest']
});
var addFile = function(file) {
if (grunt.file.isDir(file)) return;
var mimeType = mime.lookup(file);
if (!report.mimeTypes[mimeType]) {
report.mimeTypes[mimeType] = {
'count': 0,
'sizes': [],
'largest': null,
'smallest': null,
'oldest': null,
'newest': null
};
}
var details = report.mimeTypes[mimeType];
details.count++;
var stats = fs.statSync(file);
details.sizes.push(stats.size);
if (!details.largest || stats.size > details.largest.size) {
details.largest = { 'file': file, 'size': stats.size };
}
if (!report.largest || stats.size > report.largest.size) {
report.largest = { 'file': file, 'size': stats.size };
}
if (!details.smallest || stats.size < details.smallest.size) {
details.smallest = { 'file': file, 'size': stats.size };
}
if (!report.smallest || stats.size < report.smallest.size) {
report.smallest = { 'file': file, 'size': stats.size };
}
};
var sum = function(arr) {
return arr.reduce(function(a, b) {
return a + b;
});
};
var displayReport = function() {
var totalSum = 0;
var totalFiles = 0;
var totalSizes = [];
_.each(report.mimeTypes, function(data, mType) {
var fileSum = sum(data.sizes);
totalSum += fileSum;
totalFiles += data.sizes.length;
totalSizes = totalSizes.concat(data.sizes);
table.push([mType, data.count, filesize(fileSum),
filesize(fileSum / data.sizes.length),
_.sprintf('%s (%s)', data.largest.file, filesize(data.largest.size)),
_.sprintf('%s (%s)', data.smallest.file, filesize(data.smallest.size)),
]);
});
table.push(['-', totalFiles, filesize(totalSum),
filesize(totalSum / totalSizes.length),
_.sprintf('%s (%s)', report.largest.file, filesize(report.largest.size)),
_.sprintf('%s (%s)', report.smallest.file, filesize(report.smallest.size)),
]);
console.log(table.toString());
};
this.files.forEach(function(files) {
files.src.forEach(addFile);
});
displayReport();
});
};
我们插件的file-report任务生成的输出如图 2-1 所示。
图 2-1。
The output generated by the file-report task
发布到国家预防机制
一旦我们的插件准备好了,我们的 Git 库也用最新的代码更新了,向其他人提供它的最后一步是通过 npm 发布它:
$ npm publish
Note
如果这是您第一次向 npm 发布模块,您将被要求创建一个帐户。
摘要
在这一章中,我们已经了解了 Grunt 如何为开发人员提供了一个强大的工具包,用于自动化许多经常伴随软件开发的重复、乏味的任务。你发现了
- 是什么让 Grunt 滴答作响(任务、插件和配置对象)
- 如何配置任务和插件
- 如何使用 Grunt 提供的许多有用的内置工具来提供用户反馈和与文件系统交互
- 如何创建和分享你自己的 Grunt 插件
相关资源
- Grunt:
http://gruntjs.com - JSHint:
http://jshint.com grunt-contrib-watch:github.com/gruntjs/grunt-contrib-watchgrunt-contrib-jshint:github.com/gruntjs/grunt-contrib-jshintgrunt-contrib-uglify:github.com/gruntjs/grunt-contrib-uglifygrunt-contrib-compass:github.com/gruntjs/grunt-contrib-compassgrunt-mocha-test:github.com/pghalliday/grunt-mocha-test- 语法上令人敬畏的样式表(Sass):
http://sass-lang.com - 指南针:
http://compass-style.org
三、Yeoman
一个人一生中只需要两种工具:让事情进展的 WD-40 和让事情停止的管道胶带。—G. Weilacher
近年来,发展界目睹了各种角色的转换。与本地应用相比,曾经被许多人视为二等公民的 web 应用已经在很大程度上取代了传统的桌面应用,这在很大程度上要归功于现代 Web 开发技术的广泛采用和移动 Web 的兴起。但是随着 web 应用变得越来越复杂,它们所依赖的工具和引导它们存在的步骤也越来越复杂。
这一章的主题是 Yeoman,它是一个流行的项目“脚手架”工具,通过自动化与启动新应用相关的繁琐任务来帮助缓解这个问题。Yeoman 提供了一种创建可重用模板的机制,这些模板描述了项目的初始文件结构、HTML、第三方库和任务运行器配置。这些模板可以通过 npm 与更广泛的开发社区共享,允许开发人员在几分钟内启动遵循一致同意的最佳实践的新项目。
在本章中,您将学习如何:
- 安装约曼
- 利用社区已经发布的约曼生成器
- 用你自己的约曼发电机回馈社区
Note
这一章建立在本书前两章关于鲍尔和咕噜的主题之上。如果您不熟悉这些工具中的任何一个,您可能希望在继续之前阅读该工具的相应章节。
安装约曼
约曼的命令行工具yo可通过 npm 获得。如果您还没有安装 Yeoman,您应该在继续之前安装,如清单 3-1 所示。
Listing 3-1. Installing the yo Command-Line Utility via npm
$ npm install -g yo
$ yo --version
1.4.6
创建您的第一个项目
Yeoman 允许开发人员通过使用可重用的模板快速创建应用的初始结构,Yeoman 称之为“生成器”为了更好地理解这个过程如何改进您的工作流,让我们在专门为本章创建的modernweb生成器的帮助下创建一个新项目。之后,我们将看看这个生成器是如何创建的,为您提供创建和与更广泛的开发社区共享您自己的定制 Yeoman 生成器所需的知识。
我们将使用的生成器将创建一个项目的初始基础,该项目使用以下工具和库:
- Grunt
- Bower
- 框架
- 安古斯
- 浏览
- 指南针
约曼发电机作为全球 npm 模块安装。既然如此,安装我们的生成器的命令应该看起来很熟悉:
$ npm install -g generator-modernweb
Note
这个生成器的名字以generator-为前缀,这是所有约曼生成器都必须遵循的重要约定。在运行时,Yeoman 将通过搜索名称遵循这种格式的全局模块来确定安装了什么(如果有的话)生成器。
现在安装好了我们的发电机,我们可以继续设置我们的第一个项目了。首先,我们创建一个新文件夹来包含它。之后,我们指示 Yeoman 基于我们刚刚安装的生成器创建一个新项目。清单 3-2 展示了这些步骤,以及生成器设计用来提示您的几个问题。
Listing 3-2. Creating Our First Project with the modernweb Generator
$ mkdir my-app
$ cd my-app
$ yo modernweb
? Project Title: My Project
? Package Name: my-project
? Project Description: My awesome project
? Project Author: John Doe
? Express Port: 7000
在回答了生成器的问题之后(您可以放心地接受默认值),Yeoman 将继续创建项目。之后,我们可以使用项目的默认 Grunt 任务轻松地构建和启动它,我们的生成器已经为我们方便地设置了这个任务(参见清单 3-3 )。
Listing 3-3. Our New Project’s Default Grunt Task Will Trigger Various Build Steps and Open the Project Within Our Browser
$ grunt
Running "concat:app" (concat) task
File public/dist/libs.js created.
Running "compass:app" (compass) task
unchanged scss/style.scss
Compilation took 0.002s
Running "browserify" task
Running "concurrent:app" (concurrent) task
Running "watch" task
Waiting…
Running "open:app" (open) task
Running "server" task
Server is now listening on port: 7000
Done, without errors.
如您所见,我们新项目的默认 Grunt 任务为我们执行了几个额外的构建步骤:
- JavaScript 库被编译成一个简单的脚本。
- Sass 样式表被编译。
- 应用本身的源代码通过 Browserify 编译。
- 创建一个 Express 实例来服务我们的项目。
- 各种各样的观察脚本被初始化,它们将在发生变化时自动重新编译我们的项目。
我们项目的默认 Grunt 任务的最后一个动作是在一个新的浏览器窗口中启动我们的项目,如图 3-1 所示。
图 3-1。
Our new project’s home page, opened for us by the default Grunt task
既然我们的新项目已经为进一步开发做好了准备,让我们花点时间熟悉一下我们的生成器为我们准备的各种模板、脚本和简单任务,特别注意这些文件的内容:
bower.jsonGruntfile.jspackage.jsonpublic/index.html
在 Yeoman 对用户提示和模板支持的帮助下(我们将在下一节详细讨论),生成器将我们对其初始问题的回答与我们项目文件的内容进行了适当的合并。例如,我们项目的package.json文件中的name、description和author的值已经为我们设置好了(见清单 3-4 )。
Listing 3-4. Contents of Our Project’s package.json File
// package.json
{
"name": "my-project",
"description": "My awesome project",
"author": "John Doe",
"files": [],
"keywords": [],
"dependencies": {},
"browserify": {
"transform": [
"brfs",
"bulkify",
"folderify"
]
},
"browser": {}
}
子命令
最简单的形式是,发生器充当可配置的项目模板,简化了新项目的创建,但这不是它们的唯一目的。除了协助新项目的初始创建,生成器还可以包含其他命令,项目维护人员会发现这些命令在整个开发过程中都很有用。
在清单 3-2 中,我们使用modernweb生成器创建了一个使用 AngularJS 框架构建的新的单页面应用。如果你不熟悉 Angular,不要担心——这个框架的细节现在并不重要。然而,重要的是项目的public/app/routes文件夹的内容。请注意,在这个位置已经为我们创建了一个名为dashboard的文件夹。该文件夹的内容如清单 3-5 所示。
Listing 3-5. Contents of Our Project’s public/app/routes/dashboard Folder
.
■??]
ε──??″
// public/app/routes/dashboard/index.js
module.exports = {
'route': '/dashboard',
'controller': function() {
},
'templateUrl': '/app/routes/dashboard/template.html',
'resolve': {}
};
// public/app/routes/dashboard/template.html
<div class="well">
Welcome to the "/dashboard" route.
</div>
这个项目的设置使得public/app/routes中的每个文件夹在应用中定义一个不同的“hashbang”路径。在这个例子中,项目的dashboard文件夹定义了一条可以在http://localhost:7000/#/dashboard访问的路线。知道了这一点,假设我们想在我们的应用中添加一个新的users路由。为此,我们可以在适当的位置手动创建必要的文件。或者,我们可以使用生成器提供的附加命令来简化这个过程(参见清单 3-6 )。
Listing 3-6. Example of Calling the route Sub-generator to Automate the Process of Creating New Routes Within Our Angular Application
$ yo modernweb:route users
create public/app/routes/users/index.js
create public/app/routes/users/template.html
Route users created.
运行该命令后,参考项目的/public/app/routes文件夹,注意名为users的新文件夹的存在。在这个文件夹中,我们的 Yeoman 生成器已经为我们创建了合适的文件。如果您碰巧还在运行我们在清单 3-3 中创建的服务器,那么您应该也能够看到为我们启动的观察脚本已经检测到了这一变化,并自动重新编译了我们的应用(参见清单 3-7 )。
Listing 3-7. Grunt Automatically Recompiles Application As Changes Are Made
>> File "public/app/routes/users" added.
Running "browserify" task
Done, without errors.
创造你的第一台发电机
本章的剩余部分将集中于创建一个定制的 Yeoman 生成器——与上一节中用于引导一个围绕 AngularJS(以及其他工具)构建的新项目的生成器相同。之后,您将准备好开始创建您自己的生成器,这将允许您快速启动并运行满足您特定需求的工作流。
约曼发电机是节点模块
约曼生成器只不过是一个简单的节点模块,它遵循约曼规定的准则。因此,创建生成器的第一步是创建一个新的节点模块。清单 3-8 显示了所需的命令,以及生成的package.json文件。
Listing 3-8. Creating a New Node Module to Contain the Contents of Our First Yeoman Generator
$ mkdir generator-example
$ cd generator-example
$ npm init
// generator-example/package.json
{
"name": "generator-example",
"version": "1.0.0",
"description": "An example Yeoman generator",
"files": [],
"keywords": [
"yeoman-generator"
],
"dependencies": {}
}
Note
尽管我们遵循的步骤与本章前面提到的创建modernweb生成器的步骤相同,但是我们为新模块分配了一个不同的名称,以免与已经安装的模块冲突。还要注意在我们模块的关键字列表中包含了yeoman-generator。Yeoman 的网站维护了一个列表,列出了 npm 中所有可用的生成器,使得开发人员可以很容易地找到预先存在的生成器来满足他们的需求。如果一个生成器要包含在这个列表中,它必须包含这个关键字,以及在它的package.json文件中的描述。
与任何其他节点模块一样,Yeoman 生成器可以选择依赖外部依赖项。然而,最起码,每个生成器都必须将yeoman-generator模块指定为本地依赖项。本模块将为我们提供由 Yeoman 提供的核心功能,用于创建用户交互、与文件系统交互以及其他重要任务。使用以下命令将该模块安装为本地依赖项:
$ npm install yeoman-generator --save
子发电机
Yeoman 生成器由一个或多个命令组成,每个命令都可以从命令行单独调用。这些命令被 Yeoman 称为“子生成器”,它们被定义在模块根级的文件夹中。对于一些额外的上下文,请回头参考清单 3-2 ,其中我们通过从命令行运行$ yo modernweb创建了一个基于modernweb生成器的新项目。在那个例子中,我们没有指定命令——我们只是将生成器的名称传递给了 Yeoman。因此,约曼执行了该生成器的默认子生成器,按照惯例它总是被命名为app。我们可以通过运行以下命令完成同样的事情:
$ yo modernweb:app
为了更好地理解这是如何工作的,让我们继续创建生成器的默认app子生成器。我们分四步完成:
Create a folder named app at the root level of our module. Create a folder named templates within our new app folder. Place various files within our templates folder that we want to copy into the target project (e.g., HTML files, Grunt tasks, a Bower manifest, and so forth). Create the script shown in Listing 3-9, which is responsible for driving the functionality for this command. Listing 3-9. Contents of Our Generator’s Default app Command (“Sub-generator”)
// generator-example/app/index.js
var generators = require('yeoman-generator');
/**
* We create our generator by exporting a class that extends
* from Yeoman’s Base class.
*/
module.exports = generators.Base.extend({
'prompting': function() {
/**
* Indicates that this function will execute asynchronously. Yeoman
* will wait until we call the done() function before continuing.
*/
var done = this.async();
/**
* Our generator’s promptmethod (inherited from Yeoman’sBase``
* class) allows us to define a series of questions to prompt the
* user with.
*/
this.prompt([
{
'type': 'input',
'name': 'title',
'message': 'Project Title',
'default': 'My Project',
'validate': function(title) {
return (title.length > 0);
}
},
{
'type': 'input',
'name': 'package_name',
'message': 'Package Name',
'default': 'my-project',
'validate': function(name) {
return (name.length > 0 & /^[a-z0-9\-]+$/i.test(name));
},
'filter': function(name) {
return name.toLowerCase();
}
},
{
'type': 'input',
'name': 'description',
'message': 'Project Description',
'default': 'My awesome project',
'validate': function(description) {
return (description.length > 0);
}
},
{
'type': 'input',
'name': 'author',
'message': 'Project Author',
'default': 'John Doe',
'validate': function(author) {
return (author.length > 0);
}
},
{
'type': 'input',
'name': 'port',
'message': 'Express Port',
'default': 7000,
'validate': function(port) {
port = parseInt(port, 10);
return (!isNaN(port) & port > 0);
}
}
], function(answers) {
this._answers = answers;
done();
}.bind(this));
},
'writing': function() {
/**
* Copies files from our sub-generator’s templates folder to the target
* project. The contents of each file is processed as a Lodash template
* before being written to the disk.
*/
this.fs.copyTpl(
this.templatePath('**/*'),
this.destinationPath(),
this._answers
);
this.fs.copyTpl(
this.templatePath('pkg.json'),
this.destinationPath('package.json'),
this._answers
);
this.fs.delete(this.destinationPath('pkg.json'));
this.fs.copyTpl(
this.templatePath('.bowerrc'),
this.destinationPath('.bowerrc'),
this._answers
);
/**
* Writes a Yeoman configuration file to the target project’s folder.
*/
this.config.save();
},
'install': function() {
/**
* Installs various npm modules within the project folder and updates
* package.json accordingly.
*/
this.npmInstall([
'express', 'lodash', 'underscore.string', 'browserify',
'grunt', 'grunt-contrib-concat', 'grunt-contrib-watch',
'grunt-contrib-compass', 'grunt-concurrent', 'bulk-require',
'brfs', 'bulkify', 'folderify', 'grunt-open'
], {
'saveDev': false
});
/**
* Installs dependencies defined within bower.json.
*/
this.bowerInstall();
},
'end': function() {
this.log('Your project is ready.');
}
});
我们的生成器的app文件夹的内容如图 3-2 所示。
图 3-2。
The contents of our generator’s app folder. The contents of the templates folder will be copied into the target project
在清单 3-9 中,我们的生成器的默认app命令是通过导出一个从 Yeoman 的Base类扩展而来的类来创建的。在这个类中,定义了四个实例方法:
prompting()writing()install()end()
这些方法名在执行过程中起着重要的作用(它们不是随意选择的)。当 Yeoman 运行一个生成器时,它会搜索其名称与下面列出的名称相匹配的原型方法:
initializing():初始化方法(检查项目状态,获取配置)。prompting():提示用户输入信息configuring():保存配置文件。default():名称不在此列表中的原型方法将在此步骤中执行。writing():特定于该发生器的写操作发生在这里。- 冲突在这里处理(由约曼内部使用)。
- 安装程序在这里进行(npm,bower)。
end():最后调用的函数。清理/关闭消息。
一旦 Yeoman 编译了我们的生成器中存在的各种原型方法的列表,它将按照前面列表中显示的优先级执行它们。
Lodash 模板
在清单 3-9 中,Yeoman 的fs.copyTpl()方法用于将文件从子生成器的templates文件夹复制到目标项目。这种方法不同于约曼的fs.copy()方法,因为它也将找到的每个文件作为 Lodash 模板进行处理。清单 3-10 显示了我们的子生成器的templates/pkg.json文件的内容,在以package.json保存到新项目的文件夹之前,将以这种方式进行处理。
Listing 3-10. Contents of Our Sub-generator’s templates/pkg.json File
// generator-example/app/templates/pkg.json
{
"name": "<%= package_name %>",
"description": "<%= description %>",
"author": "<%= author %>",
"files": [],
"keywords": [],
"dependencies": {},
"browserify": {
"transform": [
"brfs",
"bulkify",
"folderify"
]
},
"browser": {}
}
Note
Yeoman 生成器可以根据用户对提示的回答修改它们的行为和改变模板的内容,这一过程开辟了许多令人兴奋的可能性。它允许创建根据用户的特定需求动态配置的新项目。正是 Yeoman 的这一方面,比其他任何方面都更让这个工具真正有用。
我们现在准备使用新的生成器创建我们的第一个项目。为此,请打开一个新的终端窗口,并创建一个文件夹来包含它。接下来,移动到新文件夹并运行生成器,如清单 3-11 所示。
Listing 3-11. Running Our New Generator for the First Time
$ mkdir new-project
$ cd new-project
$ yo example
Error example
You don’t seem to have a generator with the name example installed.
You can see available generators with npm search yeoman-generator and then install the
with npm install [name].
显然,这不是我们希望的结果。为了理解是什么导致了这个错误,回想一下本章前面的内容,当调用 Yeoman 时,它通过搜索已经安装在全局上下文中的名称以generator-开头的模块来定位生成器。因此,约曼目前不知道我们的新发电机的存在。幸运的是,npm 提供了一个方便的命令来解决这个问题。npm link命令在我们的新模块和节点的全局模块文件夹之间创建一个符号链接。该命令在我们新模块的根级别执行(参见清单 3-12 )。
Listing 3-12. Creating a Symbolic Link with the npm link Command
$ npm link
/Users/tim/.nvm/v0.10.33/lib/node_modules/generator-example -> /opt/generator-example
Npm 的link命令在运行它的文件夹和存储全局安装的节点模块的文件夹之间创建一个符号链接。通过运行这个命令,我们在一个 Yeoman 可以找到的位置放置了一个对新生成器的引用。有了这个链接,让我们再次运行我们的生成器(参见清单 3-13 )。
Listing 3-13. Successfully Running Our New Generator for the First Time
$ yo example
? Project Title: My Project
? Package Name: my-project
? Project Description: My awesome project
? Project Author: John Doe
? Express Port: 7000
在回答了生成器的问题后,约曼将继续构建我们的新项目,就像我们在本章前半部分使用的modernweb生成器一样。一旦这个过程完成,运行 Grunt 的默认任务—$ grunt—来构建和启动这个项目。
定义辅助命令
在本章的前半部分,您了解了 Yeoman 生成器可以包含多个命令,这些命令的用途远远超出了新项目的初始创建。modernweb生成器通过包含一个route命令来演示这一点,该命令可以在 Angular 应用中自动创建新路线(参见本章前面的清单 3-6 )。创建这个命令所涉及的步骤与我们创建生成器的默认app命令时所采取的步骤非常相似:
Create a folder named route at the root level of our module. Create a folder named templates within our new route folder. Place various files within our templates folder that we want to copy into the target project. Create the script shown in Listing 3-14, which is responsible for driving the functionality for the route command. Listing 3-14. A route Sub-generator That Automates the Creation of New Angular Routes
// generator-example/route/index.js
var generators = require('yeoman-generator');
/*
Our generator’s default appcommand was created by extending Yeoman’sBaseclass. In this example, we extend theNamedBase class, instead. Doing so alerts Yeoman to the fact that this command expects one or more arguments. For example: $ yo example:route my-new-route
*/
module.exports = generators.NamedBase.extend({
'constructor': function(args) {
this._opts = {
'route': args[0]
};
generators.NamedBase.apply(this, arguments);
},
'writing': function() {
this.fs.copyTpl(
this.templatePath('index.js'),
this.destinationPath('public/app/routes/' + this._opts.route + '/index.js'),
this._opts
);
this.fs.copyTpl(
this.templatePath('template.html'),
this.destinationPath('public/app/routes/' + this._opts.route + '/template.html'),
this._opts
);
},
'end': function() {
this.log('Route ' + this._opts.route + ' created.');
}
});
清单 3-14 中显示的脚本看起来与清单 3-9 中显示的非常相似,主要区别是使用了约曼的NamedBase类。通过创建一个从NamedBase扩展而来的子生成器,我们提醒 Yeoman 这个命令需要接收一个或多个参数。
清单 3-15 展示了我们的生成器的新route命令的使用。
Listing 3-15. Creating a New Angular Route Using Our Generator’s route Command
$ yo example:route users
create public/app/routes/users/index.js
create public/app/routes/users/template.html
Route users created.
可组合性
在创建 Yeoman 生成器时,经常会遇到这样的情况:能够从一个子生成器中执行另一个子生成器是非常有用的。例如,考虑我们刚刚创建的生成器。很容易想象这样一个场景,我们可能希望生成器在运行时自动创建几条默认路由。为了实现这个目标,如果我们能够从生成器的app命令中调用它的route命令,那将会很有帮助。约曼的composeWith()方法就是因为这个原因而存在的(见清单 3-16 )。
Listing 3-16. Yeoman’s composeWith() Method Allows One Sub-generator to Call Another
// generator-example/app/index.js (excerpt)
'writing': function() {
this.fs.copyTpl(
this.templatePath('**/*'),
this.destinationPath(),
this._answers
);
this.fs.copy(
this.templatePath('.bowerrc'),
this.destinationPath('.bowerrc'),
this._answers
);
/*
Yeoman’s composeWith method allows us to execute external generators.
Here, we trigger the creation of a new route named "dashboard".
*/
this.composeWith('example:route', {
'args': ['dashboard']
});
this.config.save();
}
在约曼composeWith()方法的帮助下,简单的子生成器可以相互组合(即“组合”)以创建相当复杂的工作流。通过利用这种方法,开发人员可以创建复杂的多命令生成器,同时避免跨命令使用重复的代码。
摘要
Yeoman 是一个简单但功能强大的工具,它自动化了与启动新应用相关的繁琐任务,加快了开发人员从概念到原型的过程。使用时,它允许开发人员将他们的注意力集中在最重要的地方——应用本身。
据最新统计,已有超过 1,500 个 Yeoman 生成器发布到 npm,这使得开发人员可以轻松地试验他们可能没有经验的各种工具、库、框架和设计模式(例如,Bower、Grunt、AngularJS、Knockout、React)。