陈斌彬的技术博客

Stay foolish,stay hungry

用 Yeoman 和 AngularJS 构建 Todo 应用(原创)

img

AngularJS 是一个用于开发动态 Web 应用的 JavaScript 框架。如果要开发一个 Web 应用,AngularJS 能够操作 HTML 使之动态地发生改变,而不是一个单纯的静态文档,它提供了像数据绑定和依赖注入 (DI) 这样的高级特定来简化应用的开发。

安装 bower

img

官网:http://bower.io/

执行命令:

npm install -g bower

查看是否安装:

bower -v

安装 Grunt

img

官网:http://gruntjs.com/

sudo npm install -g grunt-cli

安装 Yeoman

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

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

执行命令:

$ npm install --global yo

安装后,用下面这行命令进入 Yeoman 的菜单:

$ yo

结果显示如下:

img

安装 angular

执行命令:

$ npm install -g generator-angular

使用生成器搭建应用:

$ mkdir mytodo
$ cd mytodo

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

$ yo angular

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

img

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

img

按下回车键。Yeoman 将会自动构建你的应用、拉取需要的依赖并在你的工作流中创建一些有帮助的Grunt任务(Grunt Tasks)。

由Yeoman构建的文件目录结构

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

img

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

grunt serve

运行命令后本地会启动一个基于 Nodehttp 服务。通过浏览器访问 http://localhost:9000 就可以看到自己的应用了。

img

现在可以打开编辑器开始更改应用。每次保存更改后,浏览器将会自动刷新,就是说你是不需要手动再刷新浏览器了。这个被称作live reloading,这提供了一个很好的方式来实时查看应用的状态。它是通过一系列的Grunt任务来监视你的文件的更改情况,一旦发现文件被改动了,live reloading就会自动刷新应用。在这个例子中,我们编辑了views/main.html,通过陈斌彬的技术博客我们从下面的状态:

img

编写AngularJS

  • 创建新模板展现 Todo 列表

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

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

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

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

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

<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都会生成一组新的

<p><input></p>

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

img

  • 添加一个 Todo 事项

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

<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 应该是下面这个这样子的: img

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

$scope.addTodo = function () {
       $scope.todos.push($scope.todo);
       $scope.todo = '';
     };

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

 '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 = '';
     };
   });

再次在浏览器中查看,然后在顶部的输入框中输入新的 Todo 事项按下 Add 按钮。新增的这个事项就会立刻出现在你的 Todo 列表中!

img

  • 移除一个 Todo 事项

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

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

改动以后的代码:

<!-- 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>

img

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

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

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

'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 事项从列表中移除。

img

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

使用 Bower 安装了一个 Angular 组件,叫做 AngularUI Sortable module。用下面的指令我们可以检查现在已经安装上的 package:

$ bower list

img

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

$ bower search angular-ui-sortable

img

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

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

使用 –save 更新 bower.json 文件中关于 angular-ui-sortablejquery-ui 的依赖,这样你就不用手动去 bower.json 中更新依赖了。

img

看一下 bower_components 目录是不是所有包都已经拉下来了,你可以看到 jquery-uiangular-ui-sortable 出现在之前已经安装的 Angular 包边上了:

img

1.让你的 Todo 应用可排序(使用 Angular Sortable 模块)这些新安装的依赖要被添加进我们的index.html文件。你可以手动添加,不过其实Y eoman 会自动添加上。退出当前进程,再次运行

$ grunt serve

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

img

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

angular
  .module('mytodoApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch'
  ])

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

angular
  .module('mytodoApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch',
    'ui.sortable'
  ])

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

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

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

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

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

<!-- 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>

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

img

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

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

$ bower install --save angular-local-storage

img

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

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

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

img

localStorage 写入 scripts/app.js 中:

angular.module('mytodoApp', [
    'ngCookies',
    'ngResource',
    'ngSanitize',
    'ngRoute',
    'ui.sortable',
    'LocalStorageModule'
 ])

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

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

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

'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 作为第二个传入参数添加到你的回调函数中。

'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 声明删掉:

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

替换成下面的代码:

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

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

'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 事项。

img 再来添加一些项目到列表中,当我们再次刷新我们的浏览器的时候,这些项目都还在。

小结

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

参考