该页面翻译自 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 - 保存和获取数据 中,您将会修改您的“待办事项”列表应用,存储待办事项。