八、构建自动化

我们已经看到了如何使用 Jasmine 测试从头开始创建应用。但是随着应用的增长和文件数量的增加,管理它们之间的依赖关系可能会变得有些困难。

例如,我们在投资模型和股票模型之间有依赖关系,它们必须以适当的顺序加载才能工作。所以我们尽力而为;我们命令加载脚本,以便一旦投资被加载,股票就可用:

<script type="text/javascript" src="src/Stock.js"></script>
<script type="text/javascript" src="src/Investment.js"></script>

但这很快就会变得麻烦和难以管理。

另一个问题是应用用来加载所有文件的请求数量。到目前为止,它有 13 个不同的文件,所以有 13 个请求。

所以我们这里有一个悖论;尽管将代码分成小模块有利于代码的可维护性,但对客户端性能不利,因为在客户端,单个文件要好得多。

完美的世界应该同时满足以下两个要求:

  • 在开发中,我们有一堆包含不同模块的小文件
  • 在生产中,我们有一个包含所有这些文件内容的文件

显然我们需要的是某种构建过程。有许多不同的方法可以通过 JavaScript 来实现这些目标,但是我们将重点关注 RequireJS。

要求

RequireJS 是一个 AMD 实现。 AMD ( 异步模块定义)是一个关于如何用 JavaScript 编写模块的标准。

还有Node.js使用的 CommonsJS 等其他规范,都可以。但是 AMD 不同于其他公司,它在开发和生产之间无缝地在浏览器中工作。

它是基于浏览器环境的细节创建的,在浏览器环境中,事情不能总是同步,加载不同的模块可能需要稍后完成的请求。

在我们进一步开始 RequireJS 上的项目设置之前,我们首先需要了解 AMD 模块的结构。

模块定义

第 3 章测试前端代码中,我们已经看到了如何使用带有 IIFE 的模块模式来组织我们的代码。AMD 模块建立在相同的原则上:一个文件和一个函数。然而,我们调用 AMD define函数传递回调函数作为参数,而不是使用 IIFE。稍后,一旦另一个模块需要这个函数参数,RequireJS 就会调用它。

这里有一个简单的模块定义,没有任何依赖性:

define(function () {
  function MyModule() {};
  return MyModule;
});

这与我们迄今所做的非常相似。以下示例显示了如果使用第 3 章测试前端代码中的约定编写代码的方式:

(function () {
  function MyModule() {};
  return MyModule;
})();

依赖性呢?到目前为止,一切都是全局可用的,因此我们将依赖项作为参数传递给模块,如下所示:

(function ($) {
  function MyModule() {};
  return MyModule;
})(jQuery);

但是当您开始在项目中使用 RequireJS 时,将不再有全局变量。那么我们如何将这些依赖关系引入到我们的模块中呢?

如果你仔细看我们简单的模块定义;它正在返回模块值作为它的最后一条语句:

define(function () {
  function MyModule() {};
  return MyModule;
});

所以如果 required js 知道一个模块值,我们所要做的就是询问 required js。让我们再次参考依赖示例,但这次是作为 AMD 模块:

define(['jquery'], function ($) {
  function MyModule() {};
  return MyModule;
});

我们选择需要什么模块,所以 RequireJS 为我们加载它,一旦加载完成,它就用 jQuery 值调用模块定义函数。很酷吧?

您可以根据需要向 dependencies 数组传递任意多的依赖项,一旦它们可用,它们的值将以相同的顺序作为参数传递给函数。

项目设置

设置需求非常简单。创建该库时考虑了易用性;它不需要 HTTP 服务器或编译步骤就能工作(和其他解决方案一样)。您只需要开始下载一个 JavaScript 文件并执行一些小的配置。

对于这个例子,我们使用的是 2.1.6 版本,所以继续从http://requirejs.org/docs/release/2.1.6/minified/require.js下载它,并将其放在项目的lib文件夹下。

接下来,我们需要更改我们的SpecRunner.html文件来开始使用 RequireJS。您将注意到的第一件事是我们不再对 HTML 文件有任何 JavaScript 依赖。相反,我们引用 requires js 源,并指定一个特殊的 HTML 属性来告诉 requires js 哪个是我们的主 JavaScript 文件。从那里,所有依赖关系都在每个模块上声明:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>
  <link rel="shortcut icon" type="img/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
  <script src="src/RequireConfig.js"></script>
  <script data-main="spec/SpecRunner" src="lib/require.js"></script>
</head>
<body>
</body>
</html>

看到这个非常干净的 HTML 文件有点酷。剩下的就是 CSS 和 requires js 引用。

新的 SpecRunner。JS 文件

因为我们已经从文件中移除了所有的 JavaScript 代码,所以它需要在某个地方。

正如你可以通过 RequireJS 脚本标签看到的;它将其主文件设置为spec/SpecRunner.js文件。这是一切开始的地方:

require([
  'jquery',
  'jasmine',
  'jasmine-html'
],
function($, jasmine) {
  var jasmineEnv = jasmine.getEnv();
  jasmineEnv.updateInterval = 1000;

  var htmlReporter = new jasmine.HtmlReporter();
  jasmineEnv.addReporter(htmlReporter);
  jasmineEnv.specFilter = function(spec) {
    return htmlReporter.specFilter(spec);
  };

  $(function () { jasmine.getEnv().execute(); });
});

我们使用require函数,因为我们不需要这个文件作为一个模块可用,并且传递它的所有依赖项(jqueryjasminejasmine-html)。

在 requires 完成加载后,它将调用函数,将所有依赖项作为参数传递。这是我们设置 Jasmine 报告程序并执行规范的地方;一个熟悉的代码曾经在SpecRunner.html文件里面。

要求的配置

为了让这个新的跑步者工作,我们需要告诉 RequireJS 在哪里寻找模块。所以我们创建了一个新的 JavaScript 源文件src/RequireConfig.js。这里我们声明一个名为require的全局对象:

var require = {
  baseUrl: 'src',

  paths: {
    'spec': '../spec',

    'jquery': '../lib/jquery',
    'backbone': '../lib/backbone',
    'underscore': '../lib/underscore',

    'sinon': '../lib/sinon',
    'jasmine': '../lib/jasmine-1.3.1/jasmine',
    'jasmine-html': '../lib/jasmine-1.3.1/jasmine-html',
    'jasmine-jquery': '../lib/jasmine-jquery'
  }
};

我们在需要之前加载这个文件:

<script src="src/RequireConfig.js"></script>
<script data-main="spec/SpecRunner" src="lib/require.js"></script>

当 RequireJS 加载时,它可以读取这个配置,并自行设置。

到目前为止,我们正在设置两个参数:

  • 默认情况下,RequireJS 查找模块的文件夹(baseUrl)被设置为src文件夹
  • 一些路径翻译(paths),允许我们引用不同的库依赖和规范,而不使用相对路径

这允许我们像下面这样使用 jQuery 模块:

define(['jquery'], function ($) {} );

而不是:

define(['../lib/jquery'], function ($) {} );

但是还剩下一个配置;要使用任何模块作为依赖,它需要是一个有效的 AMD 模块。这不是我们所有外部依赖的情况。幸运的是,RequireJS 附带了一个支持非 AMD 模块的解决方案,我们将在下面看到。

使用带有垫片的非 AMD 依赖项

我们唯一一个支持 AMD 的外部依赖是 jQuery,所以我们不需要任何额外的参数来运行它。但是对于所有的余数(Backbone.jsUnderscore.jsJasmineSinon.js,我们需要知道:

  • 它创建了什么全局变量需要导出到任何需要它作为依赖项的模块。
  • 此模块具有的任何依赖关系。一个已知的例子是Backbone.jsUnderscore.js依赖关系。

要修复这些需求,我们需要在RequireConfig.js文件中添加其他配置参数:

var require = {
  // other parameters...

  shim: {
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    },
    'underscore': {
      exports: '_'
    },
    'jasmine': {
      exports: 'jasmine'
    },
    'sinon': {
      exports: 'sinon'
    },
    'jasmine-html': ['jasmine'],
    'jasmine-jquery': ['jasmine']
  }
};

我们必须添加shim属性;让我们以主干为例来了解它是如何工作的:

  • 首先,它告诉我们它依赖于underscorejquery模块
  • 之后应该导出全局Backbone变量

对于请求backbone的的任何模块,requires 将确保underscorejquery模块都已经加载,并且它还将传递对任何define/require功能的Backbone.js依赖的正确值。

测试模块

现在我们已经完成了需求和测试运行程序的设置,是时候调整我们的规范和代码以适应需求了。正如你将会看到的,这将会非常容易。

让我们以投资模型为例。首先,我们需要包装模块定义中的所有规范代码:

define(function () {
  describe("Investment", function() {
    var stock;
    var investment;

    beforeEach(function() {
      stock = new Stock();
      investment = new Investment({ stock: stock });
    });

    it("should be a Backbone.Model", function() {
      expect(investment).toEqual(jasmine.any(Backbone.Model));
    });
  });
});

然后,我们需要通过添加一个带有依赖模块名称的数组来指定规范依赖关系是什么:

define([
 'spec/SpecHelper',
 'backbone',
 'models/Investment',
 'models/Stock'
],
function () {
  describe("Investment", function() {
  });
});

最后,我们添加回调函数参数,以将这些依赖项接收到模块中:

define([
  'spec/SpecHelper',
  'backbone',
  'models/Investment',
  'models/Stock'
],
function (jasmine, Backbone, Investment, Stock) {
  describe("Investment", function() {
  });
});

由于所有规范都要求 Jasmine、其定制匹配器和插件进行正确配置,因此它们都将SpecHelper作为依赖项。

和其他东西一样,我们需要把这个SpecHelper做成一个 AMD 模块:

define([
 'jasmine',
 'jasmine-jquery'
],
function (jasmine) {
  jasmine.getFixtures().fixturesPath = 'spec/fixtures';

  beforeEach(function() {
    this.addMatchers({
      toBeAGoodInvestment: function() {
        var investment = this.actual;
        var what = this.isNot ? 'bad' : 'good';
        this.message = function() {
          return 'Expected investment to be a '+ what +' investment';
        };

        return investment.get('isGood');
      }
    });
  });

  return jasmine;
});

如你所见,它依赖于 Jasmine 及其所有插件(在我们的例子中,只是jasmine-jquery)。

由于它实际上是设置 Jasmine,我们返回jasmine 作为模块值。

由于所有的依赖项都已经就位,我们要做的就是运行这个规范,将它作为依赖项添加到spec/SpecRunner.jsSpecRunner模块中:

require([
  'jquery',
  'jasmine',
  'jasmine-html',
  'spec/models/InvestmentSpec'
],
function($, jasmine) {
  // Spec Runner code...
});

现在,您应该能够通过在浏览器上打开SpecRunner.html文件来运行这个规范。但正如你所料,它应该会失败。

让我们转向投资实施,看看我们如何解决这个问题。因为我们使用的是 IIFE,转换的过程比规范简单得多。

我们已经有了所有的依赖项,我们所要做的就是添加 define 函数和依赖项名称数组。我们没有将投资分配给全局命名空间,而是将其作为模块值返回:

define([
 'backbone',
 'models/stock'
],
function (Backbone, Stock) {
  var Investment = Backbone.Model.extend();

  return Investment;
});

这很容易实现,因为我们已经在使用良好的实践来组织我们的代码。

优化生产

在我们完成将整个代码库移植到 RequireJS 之后,我们准备使用优化器打包和缩小我们的代码。因此,为了实现第二个目标,我们在生产中部署了单个文件。

要使用优化器,你需要Node.js和它的包管理器。安装过程在第 4 章异步测试-AJAX中进行了解释。

为了简化它的使用,我们将在全球范围内安装它,因此它始终可以在您的路径上使用。从计算机上的任何文件夹使用 NPM,安装软件包:

$ npm install -g requirejs

安装完成后,让我们用构建参数创建一个构建配置文件。在项目根目录下添加一个名为Build.js的新文件:

({
  mainConfigFile: 'src/RequireConfig.js',
  baseUrl: "src",
  out: "build/boot.js",
  name: "Boot"
})

如您所见,它从以前的RequireConfig.js文件中导入参数:

mainConfigFile: 'src/RequireConfig.js',

它还会再次设置baseUrl参数(留空会在使用优化器 2.1.6 版本时导致构建过程中出现问题。).

baseUrl: "src",

它设置构建工件的目的地;一个包含所有打包和缩小的源代码和依赖项的文件:

out: "build/Boot.js",

最后,它指定主文件。对我们来说SpecRunner.htmlspec/SpecRunner.js文件。而在这里,是src/Boot.js的文件:

name: "Boot"

至于引导文件,它要求应用启动:

require([
  'Application'
],
function (Application) {
  Application.start();
});

我们在书中没有涉及到这个 Boot 文件,所以一定要查看附带的源文件以更好地了解它是如何工作的。

一切设定;我们准备运行优化器。在控制台上的projects文件夹中,键入以下命令:

$ r.js -o build.js

你应该看到这样的东西:

Tracing dependencies for: Boot
Uglifying file: code/build/Boot.js

code/build/Boot.js
----------------
code/lib/underscore.js
code/lib/jquery.js
code/lib/backbone.js
code/src/models/stock.js
code/src/models/Investment.js
code/src/models/Stock.js
code/src/plugins/jquery-disable-input.js
code/src/views/NewInvestmentView.js
code/src/views/InvestmentView.js
code/src/views/InvestmentListView.js
code/src/views/ApplicationView.js
code/src/routers/InvestmentsRouter.js
code/src/Application.js
code/src/Boot.js

这意味着build/Boot.js被创造了。

您可以看一下它,应用代码及其依赖项的打包和缩小版本,准备进行部署!

使用 PhantomJS 进行无头测试

还记得我们在介绍中说过,我们可以在不需要浏览器窗口的情况下执行 Jasmine 吗?为此,我们将使用 PhantomJS,一个可编写脚本的无头 WebKit 浏览器(与 Safari 的渲染引擎相同)。

下载并安装幻影 JS

PhantomJS 将二进制发行版发布到视窗、苹果和 Linux。所以开始就像下载一个 zip 文件并执行phantomjs命令一样简单。

前往http://phantomjs.org/download.html提供的 PhantomJS 下载页面,下载适合您平台的发行版。

下载后,可以将可执行文件放在project文件夹的根目录下。

不打开浏览器运行测试

有了可执行文件,我们将下载一个小脚本,允许用 PhantomJS 运行 Jasmine 规范。

可以从https://github . com/ariya/phantomjs/blob/master/examples/run-jasmine . js下载,放在project文件夹的根目录下。

搞定了。现在,您可以在命令行中运行您的规范,如下所示:

$ phantomjs run-jasmine.js SpecRunner.html

您将在控制台上看到结果,没有打开浏览器窗口:

'waitFor()' finished in 493ms.

Passing 60 specs

所以现在我们可以直接从控制台运行并获得规范结果。这将允许一些令人敬畏的自动化,正如我们接下来将看到的。

咕哝

Grunt 是一个用来创建和自动化项目任务的 JavaScript 工具。它解决了与针对 Java 的 Ant、针对 Ruby 的 Rake 等相同的问题,但使用的是 JavaScript 语言。

它被 Node.js 社区广泛使用,并且在各种各样的 JavaScript 项目中获得了很大的吸引力。

我们要用它让我们的生活变得更简单,更自动化,这样我们就可以更专注于开发时间!

安装

Grunt 也是一个Node.js包,所以一旦你安装了Node.js(如第四章异步测试-AJAX中所述),它的安装就非常简单了。

打开一个终端,调用Node.js的包管理器来安装 grunt 的命令行界面,并使其在全球可用:

$ npm install -g grunt-cli

在项目的根文件夹中,安装 grunt 库:

$ npm install grunt

一旦完成,您将有一个咕噜命令可用。

项目设置

为了使我们的项目咕噜兼容,我们需要创建一个咕噜文件。它很像一个 Makefile 或 Rakefile,包含任务定义,但在 JavaScript 中。

咕噜文件应命名为Gruntfile.js,并应放在项目的根文件夹中,包含以下框架:

module.exports = function(grunt) {
  grunt.initConfig({});
};

这就是了。我们已经准备好开始向我们的项目添加有用的任务。

需要一个优化器任务

我们将从为优化程序创建一个任务开始。为了更简单,我们将使用一个咕噜咕噜的插件grunt-contrib-requirejs

既然是也是Node.js包,我们可以用 NPM 命令安装。在项目根文件夹中,执行以下命令:

$ npm install grunt-contrib-requirejs

这将安装软件包,并使其可用于我们的咕噜文件。

我们需要做的第一件事是通过添加一个新行将这个插件的任务加载到我们的咕噜文件中:

module.exports = function(grunt) {
  grunt.initConfig({});
  grunt.loadNpmTasks('grunt-contrib-requirejs');
};

这将加载一个名为requirejs的新任务。但是在开始使用之前,我们需要进行一些配置,比如将相同的参数传递到我们的Build.js文件中。

grunt.initConfig中添加新条目:

grunt.initConfig({
  requirejs: {
    compile: {
      options: {
        mainConfigFile: 'src/RequireConfig.js',
        baseUrl: "src",
        name: "Boot",
        out: "build/Boot.js"
      }
    }
  }
});

您可以注意到,它与我们在Build.js文件中的配置参数相同。由于我们将从现在开始使用 grunt,您甚至可以删除旧文件。

一切就绪,我们可以运行第一个繁重的任务并构建我们的项目:

$ grunt requirejs

这将执行构建,并创建结果工件,与直接使用优化脚本的方式相同。

但是我们还是可以做得更好。我们可以将这个 requirejs 任务设为默认值,方法是在我们将 requirejs 任务加载到我们的咕噜文件中后添加另一个配置参数:

grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.registerTask('default', ['requirejs']);

现在,我们所要做的,执行构建,就是调用 grunt。

$ grunt

一个 Jasmine 任务

我们不想在没有确保规范通过的情况下生成构建工件,毕竟我们不想将 bug 发送到我们的生产环境中。

所以让我们创建一个新的咕噜任务来运行我们的规范,看看我们如何在构建之前让它运行。

有特定的 grunt 插件来运行 Jasmine 规范,但是我们将在这里使用不同的方法。我们想使用我们之前指定的同一个跑步者。

要从咕噜内部执行 PhantomJS 命令,我们将使用grunt-exec插件;

通过从project文件夹调用熟悉的 NPM 命令来安装它:

$ npm install grunt-exec

另外,在咕噜文件中添加它的任务,就像我们在requirejs插件中做的那样:

grunt.loadNpmTasks('grunt-exec');

接下来,我们需要设置一个新任务来运行 PhantomJS。因此,向 grunt 配置对象添加一个新条目:

grunt.initConfig({
  exec: {
    jasmine: {
      command: 'phantomjs run-jasmine.js SpecRunner.html'
    }
  }
});

现在,我们已经准备好使用咕噜咕噜的命令开始运行我们的规范:

$ grunt exec:jasmine

这将调用 PhantomJS 并显示我们的规范结果。

那么,我们如何确保在构建之前运行规范呢?我们可以创建一个名为构建的新任务,将其设为默认,并将其设置为同时运行 Jasmine 和 RequireJS 任务:

grunt.registerTask('build', ['exec:jasmine', 'requirejs']);
grunt.registerTask('default', ['build']);

通过调用 grunt,它将自动为我们运行所有这些任务:

$ grunt

观察变化并运行规范

如果你认为这很酷,这里有一个想法,“如果我每次修改一个文件,我的规范都会运行,这不是很棒吗?”

我们能做到吗?当然可以,用另一个咕噜插件。

我们将使用grunt-contrib-watch,一个观察文件变化的插件,并作为结果运行指定的任务。

首先,安装插件(总是在项目根目录。):

$ npm install grunt-contrib-watch

接下来,将其加载到咕噜文件中:

grunt.loadNpmTasks('grunt-contrib-watch');

最后,设置插件来观察我们的sourcespeclib文件夹的变化,结果运行 Jasmine 任务:

grunt.initConfig({
  watch: {
    scripts: {
      files: ['src/**/*.js', 'spec/**/*.js', 'lib/**/*.js'],
      tasks: ['exec:jasmine']
    }
  }
});

现在,您可以调用 grunt 的监视任务,并在每次更改这些目录中的任何文件时查看您的规范运行情况:

$ grunt watch

现在这是发展涅槃!

管理 NPM 属地

项目中有很多 NPM 依赖项,我们总是手工安装它们。但是我们希望这个包安装过程自动化,尤其是如果我们希望其他人在同一个项目中做出贡献。

每个Node.js项目都可以有一个带有项目定义的package.json文件。我们将在项目的根目录下创建这个文件。

基本上,它只是项目的简单描述,带有名称和版本:

{
  "name": "investment-tracker",
  "description": "Jasmine Testing Example Application",
  "version": "0.0.1"
}

但是它也可以用来定义它的依赖关系:

{
  "name": "investment-tracker",
  "description": "Jasmine Testing Example Application",
  "version": "0.0.1",
  "dependencies": {
    "express": "3.x"
  },
  "devDependencies": {
    "grunt": "0.4.x",
    "grunt-cli": "0.1.x",
    "grunt-exec": "~0.4.0",
    "grunt-contrib-requirejs": "~0.4.0",
    "grunt-contrib-watch": "~0.4.3",
    "grunt-contrib-jshint": "~0.4.3",
    "grunt-contrib-connect": "~0.3.0",
    "requirejs"
  }
}

作为一个生产依赖项,我们已经在第 4 章异步测试-AJAX中表达了我们用来运行网络服务器的框架。

作为开发依赖,我们在开发机器上有我们需要的一切,比如 grunt 和它的所有插件。

因此,如果您来到一台新机器上,需要安装所有这些依赖项,只需在项目根文件夹上键入:

$ npm install

并且所有那些依赖项都会为你安装。

总结

在这一章中,我希望已经向您展示了自动化的力量,以及我们如何使用脚本使我们的生活变得更容易。您已经了解了 RequireJS,以及如何使用它来管理模块之间的依赖关系,并帮助您生成产品代码(打包和缩小)。

您还看到了如何无头运行您的规范,以及如何在构建之前运行它们,以保证没有中断规范进入生产。

我们也看到了如何让规范自动运行,让我们始终专注于代码编辑器。

请务必查看与 Travis-CI 持续集成的奖励章节,了解如何在每次推送至项目的源代码库中时在云中运行您的规范。