创建 MVC

当您的应用的规模超出一个几十行的脚本时,如果应用的组成部分之间没有良好的角色分离,管理起来就越发的困难。无论使用哪种语言,组织复杂应用的一种最常见的模型是“模型-视图-控制器”(MVC)及其变体,如“模型-视图-展示(MVP)”。

有好几种框架能够帮助您将 MVC 概念应用到 JavaScript 应用程序上,大部分也都能在 Chrome 应用中使用。在这一代码实验室中我们将分别使用纯 JavaScript 以及 AngularJS 框架添加 MVC 模型。这一部分的大部分 AngularJS 代码都是从 AngularJS“待办事项”教程中复制过来的,只是做了一点改动。

注意:Chrome 应用并不强制要求使用任何特定的框架或编程风格。

创建简单视图

添加 MVC 的基本内容

如果您使用 AngularJS,请下载 Angular 脚本并将它存储为 angular.min.js

如果使用 JavaScript,您需要添加一个非常简单并具有基本 MVC 功能的 JavaScript controller.js

更新视图

修改 AngularJS index.html JavaScript index.html 使用简单的示例:

Angular
JavaScript
<!doctype html>
<html ng-app ng-csp>
  <head>
    <script src="angular.min.js"></script>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <ul>
        <li>
          {{todoText}}
        </li>
      </ul>
      <input type="text" ng-model="todoText"  size="30"
             placeholder="type your todo here">
    </div>
  </body>
</html>
    
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <ul>
        <li id="todoText">
        </li>
      </ul>
      <input type="text" id="newTodo" size="30"
             placeholder="type your todo here">
    </div>
    <script src="controller.js"></script>
  </body>
</html>
    

注意:ng-csp 指示符告诉 Angular 以“内容安全模式”运行,如果您使用 Angular v1.1.0+,您不需要该指示符。我们在这里包含了它,以便让示例在任何使用的 Angular 版本下都能正常工作。

添加样式表

AngularJS todo.css JavaScript todo.css 是相同的:

body {
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}

ul {
  list-style: none;
}

button, input[type=submit] {
  background-color: #0074CC;
  background-image: linear-gradient(top, #08C, #05C);
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
  color: white;
}

.done-true {
  text-decoration: line-through;
  color: grey;
}

检查结果

重新加载应用,检查结果:打开应用,单击右键并选择“重新加载应用”。

创建真正的待办事项列表

前面的示例尽管很有意思,但是并不是很有用,让我们将它转变成一个真正的“待办事项”列表。

添加控制器

无论是用纯 JavaScript 还是 AngularJS,待办事项列表由控制器管理:AngularJS controller.js JavaScript controller.js

Angular
JavaScript
function TodoCtrl($scope) {
  $scope.todos = [
    {text:'learn angular', done:true},
    {text:'build an angular Chrome packaged app', done:false}];

$scope.addTodo = function() {
    $scope.todos.push({text:$scope.todoText, done:false});
    $scope.todoText = '';
  };

$scope.remaining = function() {
    var count = 0;
    angular.forEach($scope.todos, function(todo) {
      count += todo.done ? 0 : 1;
    });
    return count;
  };

$scope.archive = function() {
    var oldTodos = $scope.todos;
    $scope.todos = [];
    angular.forEach(oldTodos, function(todo) {
      if (!todo.done) $scope.todos.push(todo);
    });
  };
}
    
(function(exports) {

  var nextId = 1;

  var TodoModel = function() {
    this.todos = {};
    this.listeners = [];
  }

  TodoModel.prototype.clearTodos = function() {
    this.todos = {};
    this.notifyListeners('removed');
  }

  TodoModel.prototype.archiveDone = function() {
    var oldTodos = this.todos;
    this.todos={};
    for (var id in oldTodos) {
      if ( ! oldTodos[id].isDone ) {
        this.todos[id] = oldTodos[id];
      }
    }
    this.notifyListeners('archived');
  }

  TodoModel.prototype.setTodoState = function(id, isDone) {
    if ( this.todos[id].isDone != isDone ) {
      this.todos[id].isDone = isDone;
      this.notifyListeners('stateChanged', id);
    }
  }

  TodoModel.prototype.addTodo = function(text, isDone) {
    var id = nextId++;
    this.todos[id]={'id': id, 'text': text, 'isDone': isDone};
    this.notifyListeners('added', id);
  }

  TodoModel.prototype.addListener = function(listener) {
    this.listeners.push(listener);
  }

  TodoModel.prototype.notifyListeners = function(change, param) {
    var this_ = this;
    this.listeners.forEach(function(listener) {
      listener(this_, change, param);
    });
  }

  exports.TodoModel = TodoModel;

})(window);


window.addEventListener('DOMContentLoaded', function() {

  var model = new TodoModel();
  var form = document.querySelector('form');
  var archive = document.getElementById('archive');
  var list = document.getElementById('list');
  var todoTemplate = document.querySelector('#templates > [data-name="list"]');

  form.addEventListener('submit', function(e) {
    var textEl = e.target.querySelector('input[type="text"]');
    model.addTodo(textEl.value, false);
    textEl.value=null;
    e.preventDefault();
  });

  archive.addEventListener('click', function(e) {
    model.archiveDone();
    e.preventDefault();
  });

  model.addListener(function(model, changeType, param) {
    if ( changeType === 'removed' || changeType === 'archived') {
      redrawUI(model);
    } else if ( changeType === 'added' ) {
      drawTodo(model.todos[param], list);
    } else if ( changeType === 'stateChanged') {
      updateTodo(model.todos[param]);
    }
    updateCounters(model);
  });

  var redrawUI = function(model) {
    list.innerHTML='';
    for (var id in model.todos) {
      drawTodo(model.todos[id], list);
    }
  };
  
  var drawTodo = function(todoObj, container) {
    var el = todoTemplate.cloneNode(true);
    el.setAttribute('data-id', todoObj.id);
    container.appendChild(el);
    updateTodo(todoObj);
    var checkbox = el.querySelector('input[type="checkbox"]');
    checkbox.addEventListener('change', function(e) {
      model.setTodoState(todoObj.id, e.target.checked);
    });
  }

  var updateTodo = function(model) {
    var todoElement = list.querySelector('li[data-id="'+model.id+'"]');
    if (todoElement) {
      var checkbox = todoElement.querySelector('input[type="checkbox"]');
      var desc = todoElement.querySelector('span');
      checkbox.checked = model.isDone;
      desc.innerText = model.text;
      desc.className = "done-"+model.isDone;
    }
  }

  var updateCounters = function(model) {
    var count = 0;
    var notDone = 0;
    for (var id in model.todos) {
      count++;
      if ( ! model.todos[id].isDone ) {
        notDone ++;
      }
    }
    document.getElementById('remaining').innerText = notDone;
    document.getElementById('length').innerText = count;
  }

  updateCounters(model);

});
    

更新视图

修改 AngularJS index.html JavaScript index.html

Angular
JavaScript
<html ng-app ng-csp>
  <head>
    <script src="angular.min.js"></script>
    <script src="controller.js"></script>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div ng-controller="TodoCtrl">
      <span>{{remaining()}} of {{todos.length}} remaining</span>
      [ <a href="" ng-click="archive()">archive</a> ]
      <ul>
        <li ng-repeat="todo in todos">
          <input type="checkbox" ng-model="todo.done">
          <span class="done-{{todo.done}}">{{todo.text}}</span>
        </li>
      </ul>
      <form ng-submit="addTodo()">
        <input type="text" ng-model="todoText" size="30"
               placeholder="add new todo here">
        <input class="btn-primary" type="submit" value="add">
      </form>
    </div>
  </body>
</html>
    
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <span><span id="remaining"></span> of <span id="length"></span> remaining</span>
      [ <a href="" id="archive">archive</a> ]
      <ul class="unstyled" id="list">
      </ul>
      <form>
        <input type="text" size="30"
               placeholder="add new todo here">
        <input class="btn-primary" type="submit" value="add">
      </form>
    </div>

    <!-- poor man's template -->
    <div id="templates" style="display: none;">
      <li data-name="list">
        <input type="checkbox">
        <span></span>
      </li>
    </div>

    <script src="controller.js"></script>
  </body>
</html>
    

注意储存在控制器中一个数组内的数据是如何与视图绑定,并在控制器更改它时自动更新的。

检查结果

重新加载应用,检查结果:打开应用,单击右键并选择“重新加载应用”。

更多信息

  • Chrome 应用通常是离线的,所以包含第三方脚本的推荐方法是下载并将它打包在您的应用内。

  • 您可以使用您希望使用的任何框架,只要它遵循内容安全策略以及 Chrome 应用强制遵循的其他限制。

  • MVC 框架使您可以更方便地建立应用,如果您想建立一个非凡的应用,请使用它们。

您还应该阅读

接下来做什么?

4 - 保存和获取数据 中,您将会修改您的“待办事项”列表应用,存储待办事项。