Yeoman官方教程:用Yeoman和AngularJS做Web应用

预计完成时间:60分钟

在这期的Code Lab中,你将会使用YeomanAngularJS搭建一个功能完整的应用,示例应用将会简短地介绍一些关于Yeoman、GruntBower的特点。如果你有一些编程经验的话,这期的CodeL Lab会很适合你。

认识YEOMAN

Yeoman的logo是一个戴帽子的男人。Yeoman通过集成了三种工具来提升你的工作效率:

  • yo是一个用于构建特定框架的生态系统的代码工具,我们称之为生成器(generator)。在以前的文章里我提过一些繁琐的任务,使用yo就能处理一些那样的任务。
  • 多亏了Yeoman团队和grunt-contrib的帮助,在项目的创建、预览以及测试可以使用grunt
  • bower用于依赖管理,使用它以后你再也不用手动地去一个个下载你的前端库了。

只需要一两个命令,Yeoman就能给你的应用提供一个代码模板(或者一个像模块一样的独立的代码片段)、编译Sass以及在当前的目录启动一个简易的服务器。它还能做单元测试,压缩以及合并你的CSS、JS、HTML还有图片等等功能。 你能使用npm安装生成器,现在可用的生成器数量已经超过了450,这其中很多都是由开源社区主持编写的。比较受欢迎的有generator-angulargenerator-backbonegenerator-ember

 

今日YEOMAN应用:使用AngularJS构建的Todo应用

AngularJS是一个用于开发动态Web应用的JavaScript框架。如果要开发一个Web应用,AngularJS能够操作HTML使之 动态地发生改变,而不是一个单纯的静态文档,它提供了像数据绑定和依赖注入(DI)这样的高级特定来简化应用的开发。 如果想要了解更多关于 AngularJS的信息,请看文档。 现在让我们开始着手做下面这个Todo应用吧。

项目源码

这个练习的源码你可以在http://bit.ly/yoangular的’angular-localStorage-todos’文件夹下找到。记住!你需要在目录下执行

1
2
$ npm install
$ bower install

后再测试这个应用。如果你觉得这些步骤太麻烦了,这里有几个指向’angular-localStorage-todos/app’’目录中的文件的链接,你可以使用直接它们:)

 

设置

在安装Yeoman之前,你需要确认以下配置:

  • Node.js 版本在0.10以上
  • npm 版本在1.3.7以上

安装好Node之后,你就可以用命令行来安装Yeoman了。 注意:大部分情况下Yeoman是要通过命令行来操作的,不同的系统执行以下命令的地方不太一样:Mac下请使用终端,Linux下使用shell,Windows下使用Cygwin

1
$ npm install --global yo

如果你看到了’permission errors’或者’access errors’,你需要在这条命令前面加上’sudo’。通过

1
$ yo --version && bower --version && grunt --version

命令来检查是不是所有东西都已经安装好了。在执行完上述命令后,你应该会看到有四个版本号会被打印出来:

  • Yeoman
  • Bower
  • Grunt
  • Grunt CLI(Grunt的命令行界面)

适用本教程的Yeoman, Bower和Grunt版本

现在的版本迭代太快了,这个教程的测试还是在Yeoman1.1.2, Bower1.2.8以及grunt-cli v0.1.11和grunt0.4.2下完成的。如果你在新的版本上做这个教程的时候出现了问题,就开一个issue来讨论,请不要有什么顾虑。

安装Yeoman生成器

在传统的Web开发流程中,你可能会花很多时间在配置代码模板、下载依赖还有手动组件项目文件结构上。而Yeoman就是来简化这个流程的!前面说 的那些繁重的工作都会被交给Yeoman来完成。让我们来试试用Yeoman来创建一个AngularJS项目吧! 用下面这行命令进入Yeoman的菜单:

1
$ yo

用键盘的上下键来操作菜单,当选项’install a generator’被高亮的时候按下回车键。 接下来我们需要寻找一个合适的生成器。搜索’angular’的话,你会得到很多搜索结果。这些生成器都是由许多Yeoman开源社区贡献的。在这个例子 里,我们使用的是’generator-angular’。当选中了’generator-angular’后,按下回车执行安装,它所依赖的Node包 就会开始被下载了。

如果你知道要安装的生成器的名字,你可以直接用npm来安装:

1
$ npm install -g generator-angular

下面是一张预览图:

这个例子使用的generator-angular版本

技术更新的如此之快!这个教程是在generator-angular 0.7.1环境下测试的。在安装完Yeoman和Angular生成器后,在命令行中运行’yo’你就可以查看版本号了。如果你在新版本的 generator-angular做这个教程时候出现了问题,请开一个issue,我们很乐意帮助你。或者你可以直接安装0.7.1这个版本:

1
$ npm install -g generator-angular@0.7.1

 

使用生成器搭建你的应用

你可以在Yeoman的菜单中操作已经安装好的生成器:

1
$ yo

等一下!不要直接就运行生成器了。重新创建一个新的项目目录,生成器会在这个目录下生成出你的项目文件的。

1
2
$ mkdir mytodo
$ cd mytodo

执行’yo’,选中’Run the Angular generator’,运行生成器。当你比较熟悉Yo的时候,就可以不通过菜单直接运行生成器:

1
$ yo angular

一些生成器也会提供一些有共同开发库(common developer libraries)的可选配置来定制你的应用,能够加速初始化你的开发环境。 generator-angular会询问你需不需要使用Sass和/或者Bootstrap,使用’n'和’y'进行选择。

然后你需要选择你需要使用的Angular模块。Angular模块是一些带有特定功能的独立的JS文件。举个例子,ngResource模块 (angular-resource.js)提供了RESTful服务。你可以使用空格键来取消项目。下面来看一看默认值。(当你在试用空格的效果时,确 保所有的模块都被标记为绿色)

好的,现在按下回车键。Yeoman将会自动构建你的应用、拉取需要的依赖并在你的工作流中创建一些有帮助的Grunt任务(Grunt Tasks)。几分钟后,我们就能正式开始啦!

由Yeoman构建的文件目录结构

打开’mytodo’目录,你会看到下面的文件结构:

  • app/:Web应用的父级目录。
    • index.html: Angular应用的基准HTML文件(base html file)
    • 404.html、favicon.ico和robots.txt:通用的Web文件,Yeoman已经将它创建出来了,你不需要再手动去创建
    • bower_components:存放项目相关的JavaScript或Web依赖,由bower安装的
    • scripts:我们的JS文件
      • app.js:主程序
      • controllers:Angular控制器
    • styles:我们的CSS文件
    • views:Angular模板
  • Gruntfile.js、package.json 以及node_modules:Grunt需要使用的依赖以及配置。
  • test和karma.conf.js/karma-e2e.conf.js:测试框架以及针对这个项目的单元测试,包括了为控制器写的样板测试(boilerplate tests)。

 

在浏览器中查看你的应用

要在浏览器中查看应用,就要将grunt server运行起来:

1
grunt serve

运行命令后本地会启动一个基于Node的http服务。通过浏览器访问http://localhost:9000就可以看到你的应用了。现 在可以打开编辑器开始更改应用。每次保存更改后,浏览器将会自动刷新,就是说你是不需要手动再刷新浏览器了。这个被称作‘live reloading’,这提供了一个很好的方式来实时查看应用的状态。它是通过一系列的Grunt任务来监视你的文件的更改情况,一旦发现文件被改动 了,’live reloading’就会自动刷新应用。在这个例子中,我们编辑了views/main.html,通过’live reload’我们从下面的状态:马上到了这个状态:

 

在让我们开始编写我们的AngularJS应用吧

在app/mytodo目录中的文件你都可以在浏览器中访问到,所有要更改的文件也都在app目录下。如果你不确定你的文件建构是否正确,请查看上文的截图。

  • 创建新模板展现Todo列表

先将views/main.html中除了class是jumbotron的div以外的内容都删除,然后把’jumbotron’替换成’container’:

1
<div class="container"></div>

更改Angular控制器模板(即scripts/controller/main.js),将’awesomeThings’改为’todos’:

1
2
3
4
5
'use strict';
angular.module('mytodoApp')
    .controller('MainCtrl', function ($scope) {
         $scope.todos = ['Item 1', 'Item 2', 'Item 3'];
    });

然后更改视图(views/main.html)来显示我们的Todo事项:

1
2
3
4
5
6
<div class"container">
    <h2>My todos</h2>
    <p ng-repeat="todo in todos">
        <input type="text" ng-model="todo">
    </p>
</div>

 

在p标签中的ng-repeat属性是一个Angular指令(directive),当获取到一个集合(collection)中的项时,它将项实例化。在我们的例子中,你可以想象一下,每个p标签和它的内容都带着这个’ng-repeat‘属性。对于每个在todos数组中的项,Angular都会生成一组新的

1
<p><input></p>

 

ng-model是 另一个Angular指令,它主要是和input、select、textarea标签和一些自定义控件一起使用,达到数据双向绑定的效果。在我们的例子 中,它用于显示一系列带有’todo‘的值的文本输入域。 在浏览器中查看ng-repeat和ng-model动态变化的效果。在保存之前,我们的应用 看起来应该是下图这个样子的:

  • 添加一个Todo事项

我们将要实现添加新的Todo事项的功能。现在需要修改views/main.html:在h2元素和p元素之间加上一个form元素。现在你的views/main.html应该是下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="container">
       <h2>My todos</h2>
 
       <!-- Todos input -->
       <form role="form" ng-submit="addTodo()">
         <div class="row">
           <div class="input-group">
             <input type="text" ng-model="todo" placeholder="What needs to be done?" class="form-control">
             <span class="input-group-btn">
              <input type="submit" value="Add" class="btn btn-primary">
             </span>
           </div>
         </div>
       </form>
       <p></p>
 
       <!-- Todos list -->
       <p ng-repeat="todo in todos" class="form-group">
         <input type="text" ng-model="todo" class="form-control">
       </p>
     </div>

 

这一步里我们在页面顶部增加了一个带有提交按钮的表单。这个表单使用了另一个Angular指令:ng-submit。返回查看你的浏览器,现在的UI应该是下面这个这样子的:

如果现在点击’Add’按钮,什么事情都不会发生—— 我们接下来要实现添加的效果: ng-submit是将一个Angular表达式绑定到表单的onsubmit事件上。如果form上没有绑定任何动作,它也会阻止浏览器的默认行为。在 我们的例子中,我们添加了一个’addTodo()’表达式。 下面的“`addTodo“`方法是实现将新增的Todo事项添加入已有的事项列表中,然 后清空顶部的文本输入域:

1
2
3
4
$scope.addTodo = function () {
       $scope.todos.push($scope.todo);
       $scope.todo = '';
     };

 

将addTodo()方法加到scripts/controllers/main.js的MainCtrl控制器的定义中,现在你的控制器代码应该如下所示:

1
2
3
4
5
6
7
8
9
10
 'use strict';
 
 angular.module('mytodoApp')
   .controller('MainCtrl', function ($scope) {
     $scope.todos = ['Item 1', 'Item 2', 'Item 3'];
     $scope.addTodo = function () {
       $scope.todos.push($scope.todo);
       $scope.todo = '';
     };
   });

 

注意:你可能会在在终端/命令行中看到很多linting errors,这可能是因为jshint检 查出代码缩进有问题,所以抛出了警告,事实上这个Todo应用是可以正常工作的。不过,建议还是要看看jshint给你的建议,然后调整你的代码让它更干 净更有可读性。 再次在浏览器中查看,然后在顶部的输入框中输入新的Todo事项按下’Add’按钮。新增的这个事项就会立刻出现在你的Todo列表中!

 

注意:如果你添加了一个空的Todo事项,或者一个重名的事项,你的Todo应用就会罢工哦。:( 不过作为一个小练习,你可以加强一下addTodo()方法来避免上述的错误

  • 移除一个Todo事项

现在我们来添加一个移除事项的功能,先在列表中每一个Todo事项的的边上加上一个“移除”按钮。回到我们的视图模板 (views/main.html),在现有的ng-repeat指令上添加一个按钮。然后确认我们的输入框和移除按钮是对齐的,将p标签的class 从’form-group’改成’input-group’。 再改动之前代码是这样的:

1
2
3
4
<!-- Todos list -->
<p ng-repeat="todo in todos" class="form-group">
  <input type="text" ng-model="todo" class="form-control">
</p>

改动以后的代码:

1
2
3
4
5
6
7
<!-- Todos list -->
<p class="input-group" ng-repeat="todo in todos">
    <input type="text" ng-model="todo" class="form-control">
    <span class="input-group-btn">
        <button class="btn btn-danger" ng-click="removeTodo($index)" aria-label="Remove">X</button>
    </span>
</p>

 

现在你的应用看起来很不错了!

在上面的代码中我们使用了一个新的Angular指令——ng-click。可以用ng-click来控制元素被点击时的行为。在这个例子中,我们调用了removeTodo()方法并将$index传入了这个方法。 $index的 值是当前todo项在整个todo数组中的位置的索引值。举个例子,数组中的第一项的索引值是0,那么0就会被传入removeTodo();类似的,在 一个五项的Todo列表中,最后一项的索引值是4,4就会被传入removeTodo()。 现在我们来实现这个removeTodo()方法,下面的代码是使用JS中的splice方法将要移除的项通过给定的$index值从数组中移除:

1
2
3
$scope.removeTodo = function (index) {
    $scope.todos.splice(index, 1);
 };

修改以后的控制器(scripts/controller/main.js)如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';
angular.module('mytodoApp')
    .controller('MainCtrl', function ($scope) {
        $scope.todos = ['Item 1', 'Item 2', 'Item 3'];
        $scope.addTodo = function () {
           $scope.todos.push($scope.todo);
           $scope.todo = '';
        };
        $scope.removeTodo = function (index) {
        $scope.todos.splice(index, 1);
    };
});

当你更新好代码再去查看浏览器的时候,你的应用是下面这个样子的,刚刚我们又新添了一项: 

现在你可以点击’×’按钮将一个Todo事项从列表中移除。超神奇! 

你可能会注意到一点:现在虽然我们可以添加和移除Todo事项,但是这些记录都不能永久地保存。一旦页面被刷新了,更改的记录都会不见了,又恢复到我们main.js中设置的todo数组的值。不过不要担心这个问题,等我们了解更多关于使用Bower安装package了以后,这个问题就会被解决的。

使用Bower安装包

现在给我们的列表添加一些排列方式来合理地显示它。所以我们要使用Bower安装了一个Angular组件,叫做AngularUI Sortable module。用下面的指令我们可以检查现在已经安装上的package:

1
$ bower list

为了确认有AngularUI的包可以使用,用Bower查找’angular-ui-sortable’:

1
$ bower search angular-ui-sortable

搜索结果中只有一个和*angular-ui-sortable*有关。而且我们已经安装了jQuery,那么现在我们就一起把jQuery UI也一起安装上。为了节省搜索的时间,jQuery UI的包的名字是’jquery-ui’。要一次性安装上它们使用下面的命令:

1
$ bower install --save angular-ui-sortable jquery-ui

使用–save更新bower.json文件中关于angular-ui-sortable和jquery-ui的依赖,这样你就不用手动去bower.json中更新依赖了。 看一下你的bower_components目录是不是所有包都已经拉下来了,你可以看到’jquery-ui’和’angular-ui-sortable’出现在之前已经安装的Angular包边上了: 

  1. 让你的Todo应用可排序(使用Angular Sortable模块)这些新安装的依赖要被添加进我们的index.html文件。你可以手动添加,不过其实Yeoman会自动添加上。先在命令行中使用Ctrl+c,退出当前的进程。再次运行:
    1
    $ grunt serve

    你可以看到在index.html文件底部链接脚本的位置,jquery-ui/ui/jquery-ui.jsangular-ui-sortable/sortable.js已经自动地被引入了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- build:js scripts/vendor.js -->
    <!-- bower:js -->
    <script src="bower_components/jquery/jquery.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
    <script src="bower_components/angular-resource/angular-resource.js"></script>
    <script src="bower_components/angular-cookies/angular-cookies.js"></script>
    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
    <script src="bower_components/angular-route/angular-route.js"></script>
    <script src="bower_components/jquery-ui/ui/jquery-ui.js"></script>
    <script src="bower_components/angular-ui-sortable/sortable.js"></script>
    <!-- endbower -->
    <!-- endbuild -->

    为了使用Sortable模块,我们需要在scripts/app.js中更新Angular模块,将Sortable可以加载到我们的应用中,更改前代码如下所示:

    1
    2
    3
    4
    5
    6
    angular.module('mytodoApp', [
        'ngCookies',
        'ngResource',
        'ngSanitize',
        'ngRoute'
    ])

    将’ui.sortable’添加进数组中。现在scripts/app.js应该是下面这个样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    'use strict';
    angular.module('mytodoApp', [
        'ngCookies',
        'ngResource',
        'ngSanitize',
        'ngRoute',
        'ui.sortable'
    ])
        .config(function ($routeProvider) {
            $routeProvider
              .when('/', {
                  templateUrl: 'views/main.html',
                  controller: 'MainCtrl'
              })
              .otherwise({
                  redirectTo: '/'
              });
        });

    最后,在main.html中,我们需要将’ui-sortable’指令作为一个层将ng-repeat层包起来。

    1
    2
    3
    <!-- Todos list -->
    <div ui-sortable ng-model="todos">
        <p class="input-group" ng-repeat="todo in todos">

    我们也需要添加一些内联的CSS,将鼠标显示为“可移动”样式来告诉用户这些Todo事项是可以移动的:

    1
    <p ng-repeat="todo in todos" style="padding:5px 10px; cursor: move;">

    现在Todo列表的HTML部分应该是下面这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- Todos list -->
    <div ui-sortable ng-model="todos">
        <p class="input-group" ng-repeat="todo in todos" style="padding:5px 10px; cursor: move;">
            <input type="text" ng-model="todo" class="form-control">
            <span class="input-group-btn">
                <button class="btn btn-danger" ng-click="removeTodo($index)" aria-label="Remove">X</button>
            </span>
        </p>
    </div>

    现在这个列表就是可以移动的了:

    可以将它移动成这个样子:

    动画的gif图片(注意光标):

  2. 使用本地存储实现保存

现在回到刚刚提到的无法储存的问题上。

Angular有一个模块叫做’angular-local-storage’可以很简便地帮我们实现本地存储,使用Bower安装:

1
$ bower install --save angular-local-storage

与我们添加jQueryUI和AngularUI Sortable的时候相似,我们需要将angular-local-storage.js引入index.html:

1
<script src="bower_components/angular-local-storage/angular-local-storage.js"></script>

因为我们是使用bower.json来索引我们的模块的,所以先用Ctrl+c退出当前的进程,然后再次输入grunt serve让Yeoman将新逻辑写入你的index.html,重新启动服务后现在你的index.html的脚本部分看起来应该是下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/jquery-ui/ui/jquery-ui.js"></script>
<script src="bower_components/angular-ui-sortable/sortable.js"></script>
<script src="bower_components/angular-local-storage/angular-local-storage.js"></script>
<!-- endbower -->
<!-- endbuild -->

将localStorage写入scripts/app.js中:

1
2
3
4
5
6
7
8
angular.module('mytodoApp', [
    'ngCookies',
    'ngResource',
    'ngSanitize',
    'ngRoute',
    'ui.sortable',
    'LocalStorageModule'
 ])

在app.js中也要配置’localStorageServiceProvider’,将’todo’作为本地存储的前缀,这样你的应用就不会把其他应用中重名的变量的内容获取过来:

1
2
3
.config(['localStorageServiceProvider', function(localStorageServiceProvider){
     localStorageServiceProvider.setPrefix('ls');
 }])

我们的scripts/app.js现在应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';
angular.module('mytodoApp', [
   'ngCookies',
   'ngResource',
   'ngSanitize',
   'ngRoute',
   'ui.sortable',
   'LocalStorageModule'
])
   .config(['localStorageServiceProvider', function(localStorageServiceProvider){
       localStorageServiceProvider.setPrefix('ls');
   }])
   .config(function ($routeProvider) {
       $routeProvider
   .when('/', {
      templateUrl: 'views/main.html',
      controller: 'MainCtrl'
   })
   .otherwise({
      redirectTo: '/'
   });
 });

你也需要在你的控制器(scripts/controllers/main.js)中声明对本地存储服务的依赖。将’localStorageService’作为第二个传入参数添加到你的回调函数中。

1
2
3
4
5
'use strict';
angular.module('mytodoApp')
   .controller('MainCtrl', function ($scope, localStorageService) {
   // (code hidden here to save space)
});

那么现在呢,我们的Todo事项就不是从静态的数组中读取的,我们将会从本地存储里读取然后再将它们存入$scope.todos中。 我们还需要使用Angular的$warch监听器来监听$scope.todos的值得变化。如果有人添加或者删减了Todo项目,本地存储中的数据也会被同步, 因此,我们需要将现在的$scope.todos声明删掉:

1
$scope.todos = ['Item 1', 'Item 2', 'Item 3'];

替换成下面的代码:

1
2
3
4
5
var todosInStore = localStorageService.get('todos');
$scope.todos = todosInStore && todosInStore.split('\n') || [];
$scope.$watch('todos', function () {
   localStorageService.add('todos', $scope.todos.join('\n'));
}, true);

现在我们的控制器是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 'use strict';
 
 angular.module('mytodoApp')
   .controller('MainCtrl', function ($scope, localStorageService) {
 
     var todosInStore = localStorageService.get('todos');
 
     $scope.todos = todosInStore && todosInStore.split('\n') || [];
 
     $scope.$watch('todos', function () {
       localStorageService.add('todos', $scope.todos.join('\n'));
     }, true);
 
     $scope.addTodo = function () {
       $scope.todos.push($scope.todo);
       $scope.todo = '';
     };
 
     $scope.removeTodo = function (index) {
       $scope.todos.splice(index, 1);
     };
 
   });

如果现在你在浏览器中查看应用,你会发现Todo列表中没有任何东西。因为这个应用从本地存储中读取了todo数组,而本地存储中还没有任何Todo事项。

再来添加一些项目到列表中吧:

在当我们再次刷新我们的浏览器的时候,这些项目都还在。万岁!

用Chrome开发工具(Chrome DevTools)中的Resources面板里确认我们的数据是不是真的被永久储存在本地存储中。在资源面板的左侧里选中’Local Storage’:

没花多少时间,你就用Yeoman实现了一个很酷的Todo应用,快表扬下自己吧!现在你还能想到别的方式进行前端开发吗?

来回顾一下,在这一节中我们做了:

  • 用yo搭建了一个应用的模板文件
  • 为了增添应用的功能,用bower安装应用需要的依赖
  • grunt serve搭建和预览我们的应用,所有的改动都能实时地反映在页面上,不需要手动刷新。

准备好上线产品吧

现在我们有了一个功能性的应用,现在来测试一下,实现一个可上线的版本。

  1. 使用Karma和Jasmine测试Karma是个一个JS测试框架。Angular生成器本身已经包括了两个测试框架:ngScenarioJasmine。当之前我们运行yo angular的 时候,在mytodo文件夹下会生成了一个test目录,还有一个karma.conf文件,它会被放入在Node模块中以使用Karma。我们将会编辑 一个Jasmine脚本来完成我们的测试。现在先来看看要怎么进行测试。现在回到命令行结束grunt server的进程(Ctrl+c)。在Gruntfile.js中已经有了用于运行测试的grunt任务,可以直接像下面这样运行:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     files: [
       'app/bower_components/jquery/jquery.js',
       'app/bower_components/jquery-ui/ui/jquery-ui.js',
       'app/bower_components/angular/angular.js',
       'app/bower_components/angular-ui-sortable/sortable.js',
       'app/bower_components/angular-mocks/angular-mocks.js',
       'app/bower_components/angular-local-storage/angular-local-storage.js',
       'app/scripts/*.js',
       'app/scripts/**/*.js',
       'test/mock/**/*.js',
       'test/spec/**/*.js',
       'app/bower_components/angular-resource/angular-resource.js',
       'app/bower_components/angular-cookies/angular-cookies.js',
       'app/bower_components/angular-sanitize/angular-sanitize.js',
       'app/bower_components/angular-route/angular-route.js'
     ],

    还记得之前我们已经更改过main.js将数据从本地存储中加载到页面上么?现在为了让问题简单一点,我们不再使用本地存储而是用已经写在数组中的数据来做单元测试。将main.js中的内容替换成下面代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     'use strict';
     
     angular.module('mytodoApp')
       .controller('MainCtrl', function ($scope) {
     
       $scope.todos = [];
     
       $scope.addTodo = function () {
         $scope.todos.push($scope.todo);
         $scope.todo = '';
       };
     
       $scope.removeTodo = function (index) {
         $scope.todos.splice(index, 1);
       };
     });

    下一步,修改在main.js中的单元测试。打开test/spec/controllers/main.js。将下面的代码删除:

    1
    2
    3
     it('should attach a list of awesomeThings to the scope', function () {
       expect(scope.awesomeThings.length).toBe(3);
     });

    替换成下面的代码:

    1
    2
    3
     it('should have no items to start', function () {
         expect(scope.todos.length).toBe(0);
     });

    再次运行grunt test,应该可以看到测试通过:

    让我们再添加两个用于测试添加和移除Todo事项的测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     it('should add items to the list', function () {
       scope.todo = 'Test 1';
       scope.addTodo();
       expect(scope.todos.length).toBe(1);
     });
     
     it('should add items to the list', function () {
       scope.todo = 'Test 1';
       scope.addTodo();
       scope.removeTodo(0);
       expect(scope.todos.length).toBe(0);
     });

    现在整个测试脚本(test/spec/controllers/main.js)应该是像下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
     'use strict';
     
     describe('Controller: MainCtrl', function () {
     
       // load the controller's module
       beforeEach(module('mytodoApp'));
     
       var MainCtrl,
         scope;
     
       // Initialize the controller and a mock scope
       beforeEach(inject(function ($controller, $rootScope) {
         scope = $rootScope.$new();
         MainCtrl = $controller('MainCtrl', {
           $scope: scope
         });
       }));
     
       it('should have no items to start', function () {
         expect(scope.todos.length).toBe(0);
       });
     
       it('should add items to the list', function () {
         scope.todo = 'Test 1';
         scope.addTodo();
         expect(scope.todos.length).toBe(1);
       });
     
       it('should add items to the list', function () {
         scope.todo = 'Test 1';
         scope.addTodo();
         scope.removeTodo(0);
         expect(scope.todos.length).toBe(0);
       });
     
     });

    再次运行测试,我们应该看到这3个测试都能通过:

    棒极了!当你的应用越来越大、参与的开发者越来越多了以后,写单元测试可以方便开发者查bug。在Yeoman的帮助下,编写单元测试变得很轻松,你再也没有理由不写测试啦!;)

  2. 优化

准备发布你的Todo应用了吗?现在给我们的应用创建一个上线版本吧。我们需要规范代码、跑测试、压缩JS和CSS代码(减少网络请求)、优化图片还有编译使用了预处理的代码,让我们的应用尽可能的精简。

非常神奇的是,上面的工作我们都能通过下面这行命令实现:

1
$ grunt

这行命令会遍历Yeoman生成的Grunt任务和配置,然后创建出应用的过渡版本。过一会命令行中会显示出应用的结构和生成这个应用总共花费的时间,以及完成每一项任务所花费的时间。

现在你的应用应该很不错了。可以在’mytodo’中的’dist’文件夹下找到这个应用。如果想要在本地查看这个应用的话,运行下面这行命令:

1
$ grunt serve:dist

它会生成你的项目并且启动本地的服务器。

 

Yeoman可以做的更多

除了在这个教程中我们使用到的框架外,Yeoman还支持很多其他的框架。 举例来说,Anugular生成器也支持创建新的视图、指令和控制器。可以通过运行yo angular:controller controllerName搭建一个新的控制器,同时在app.js中的路由也会被更新。在可能使用单元测试的地方,我们也会试图搭建测试。

想要知道更多有关于Angular生成器的Yeoman命令,请查看generator readme

 

下一步是什么

 

以上由“戴帽子的人”发布。谢谢!