该页面翻译自 Google Chrome Extensions 与 Google Chrome Apps。除非特别说明,该页面的内容遵循 Creative Commons Attribution 3.0 License,代码示例遵循 BSD License。
当您的应用的规模超出一个几十行的脚本时,如果应用的组成部分之间没有良好的角色分离,管理起来就越发的困难。无论使用哪种语言,组织复杂应用的一种最常见的模型是“模型-视图-控制器”(MVC)及其变体,如“模型-视图-展示(MVP)”。
有好几种框架能够帮助您将 MVC 概念应用到 JavaScript 应用程序上,大部分也都能在 Chrome 应用中使用。在这一代码实验室中我们将分别使用纯 JavaScript 以及 AngularJS 框架添加 MVC 模型。这一部分的大部分 AngularJS 代码都是从 AngularJS“待办事项”教程中复制过来的,只是做了一点改动。
注意:Chrome 应用并不强制要求使用任何特定的框架或编程风格。
如果您使用 AngularJS,请下载 Angular 脚本并将它存储为 angular.min.js。
如果使用 JavaScript,您需要添加一个非常简单并具有基本 MVC 功能的 JavaScript controller.js
修改 AngularJS index.html 或 JavaScript index.html 使用简单的示例:
<!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。
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:
<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 - 保存和获取数据 中,您将会修改您的“待办事项”列表应用,存储待办事项。