第 2 步:导入现有 Web 应用

在此步骤中,您将学习以下内容:

  • 如何针对 Chrome 应用平台调整现有 Web 应用。
  • 如何使您的应用脚本符合内容安全政策 (CSP)。
  • 如何使用 chrome.storage.local 实现本地存储。

完成此步骤预计用时:20 分钟。
若要预览您将在此步骤中完成的内容,请跳转到本页底部 ↓

导入现有的 Todo 应用

首先,导入TodoMVC(一种常用的基准)的原始 JavaScript 版本 复制到您的项目中

我们在 todomvc参考代码 zip 中添加了 TodoMVC 应用的一个版本 文件夹中。将 todomvc 中的所有文件(包括文件夹)复制到项目文件夹中。

将 todomvc 文件夹复制到 Codelab 文件夹中

系统会要求您替换 index.html。请直接接受邀请。

替换 index.html

您的应用文件夹中现在应具有以下文件结构:

新建项目文件夹

以蓝色突出显示的文件来自 todomvc 文件夹。

立即重新加载您的应用(右键点击 > 重新加载应用)。您应该会看到基本界面,但看不到 能够添加待办事项

确保脚本符合内容安全政策 (CSP)

打开开发者工具控制台(右键点击 > 审查元素,然后选择控制台标签页)。您 会看到有关拒绝执行内嵌脚本的错误:

待办事项应用包含 CSP 控制台日志错误

要解决此问题,使应用符合内容安全政策要求。其中 常见的 CSP 违规行为是由内嵌 JavaScript 引起的。内联 JavaScript 的示例包括 事件处理脚本作为 DOM 属性(例如 <button onclick=''>),以及包含内容的 <script> 标记 。

解决方案很简单:将内嵌内容移到新文件中。

1. 在 index.html 底部附近,移除内嵌 JavaScript 并改为添加 js/bootstrap.js:

<script src="bower_components/director/build/director.js"></script>
<script>
  // Bootstrap app data
  window.app = {};
</script>
<script src="js/bootstrap.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>

2. 在 js 文件夹中创建一个名为 bootstrap.js 的文件。移动之前的内嵌代码 添加到此文件中:

// Bootstrap app data
window.app = {};

如果您现在重新加载“待办事项”应用,该应用仍会无法正常运行,但距离您更近一步了。

将 localStorage 转换为 chrome.storage.local

如果您现在打开开发者工具控制台,之前的错误应该会消失。有个新错误, 但是,大约 window.localStorage 不可用:

包含 localStorage 控制台日志错误的待办事项应用

Chrome 应用不支持 localStorage,因为 localStorage 是同步的。同步访问 在单线程运行时中阻塞资源 (I/O) 可能会导致您的应用无响应。

Chrome 应用拥有可以异步存储对象的等效 API。这有助于避免 有时成本高昂的对象->字符串->对象序列化过程。

要解决应用中的错误消息,您需要将 localStorage 转换为 chrome.storage.local

更新应用权限

如需使用 chrome.storage.local,您需要请求 storage 权限。在 manifest.json,请将 "storage" 添加到 permissions 数组:

"permissions": ["storage"],

了解 local.storage.set() 和 local.storage.get()

要保存和检索待办事项,您需要了解 set()get() 方法 chrome.storage API。

set() 方法接受键值对对象作为其第一个参数。可选的 第二个参数是回调函数例如:

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

get() 方法接受您希望从数据存储区键中提取的第一个参数(可选) 检索。单个键可作为字符串传递;可以将多个键排列成一个数组, 字符串或字典对象。

第二个参数是回调函数,它是必需的。在返回的对象中,使用 第一个参数中请求的键以访问存储的值。例如:

chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
  console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});

如果要 get() 当前在 chrome.storage.local 中的所有内容,请省略第一个 参数:

chrome.storage.local.get(function(data) {
  console.log(data);
});

localStorage 不同,您将无法使用开发者工具检查本地存储的项 资源面板。不过,您可以通过 JavaScript 控制台与 chrome.storage 交互,例如 所以:

使用控制台调试 chrome.storage

预览所需的 API 更改

转换待办事项应用的其余大部分步骤都是对 API 调用的细微更改。更改 当前使用 localStorage 的所有位置,尽管如此既耗时又容易出错, 为必填字段。

localStoragechrome.storage 之间的主要区别在于 chrome.storage

  • 您无需使用简单赋值来写入 localStorage,而是需要使用 带有可选回调的 chrome.storage.local.set()

    var data = { todos: [] };
    localStorage[dbName] = JSON.stringify(data);
    

    vs.

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • 您不必直接访问 localStorage[myStorageName],而是需要使用 chrome.storage.local.get(myStorageName,function(storage){...}),然后解析返回的 storage 对象。

    var todos = JSON.parse(localStorage[dbName]).todos;
    

    vs.

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • 函数 .bind(this) 用于所有回调,以确保 this 引用this Store 原型设计而成。(有关绑定函数的详细信息,请参阅 MDN 文档: Function.prototype.bind()。)

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'undefined'
      });
    }
    new Store();
    

    vs.

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'inside Store'
      }.bind(this));
    }
    new Store();
    

介绍如何在 Google Cloud 控制台中检索、保存和移除待办事项, 后续部分。

检索待办事项

我们来更新 Todo 应用,以检索待办事项:

1. Store 构造函数方法负责使用现有的所有现有映像来初始化 Todo 应用 从数据存储区中排除待办事项。该方法首先检查数据存储区是否存在。否则 创建一个空的 todos 数组,并将其保存到数据存储区中,以避免运行时读取错误。

js/store.js 中,将构造函数方法中的 localStorage 转换为使用 chrome.storage.local

function Store(name, callback) {
  var data;
  var dbName;

  callback = callback || function () {};

  dbName = this._dbName = name;

  if (!localStorage[dbName]) {
    data = {
      todos: []
    };
    localStorage[dbName] = JSON.stringify(data);
  }
  callback.call(this, JSON.parse(localStorage[dbName]));

  chrome.storage.local.get(dbName, function(storage) {
    if ( dbName in storage ) {
      callback.call(this, storage[dbName].todos);
    } else {
      storage = {};
      storage[dbName] = { todos: [] };
      chrome.storage.local.set( storage, function() {
        callback.call(this, storage[dbName].todos);
      }.bind(this));
    }
  }.bind(this));
}

2. 从模型中读取待办事项时使用 find() 方法。返回的结果会根据 以及按“全部”“有效”还是“已完成”过滤

find() 转换为使用 chrome.storage.local

Store.prototype.find = function (query, callback) {
  if (!callback) {
    return;
  }

  var todos = JSON.parse(localStorage[this._dbName]).todos;

  callback.call(this, todos.filter(function (todo) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName].todos.filter(function (todo) {
      for (var q in query) {
         return query[q] === todo[q];
      }
      });
    callback.call(this, todos);
  }.bind(this));
  }));
};

3. 与 find() 类似,findAll() 从模型获取所有待办事项。将findAll()转为使用 chrome.storage.local

Store.prototype.findAll = function (callback) {
  callback = callback || function () {};
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName] && storage[this._dbName].todos || [];
    callback.call(this, todos);
  }.bind(this));
};

保存待办事项

当前的 save() 方法提出了一项挑战。这取决于两项异步操作(get 和 set) 。在多个系统上执行任何批量更新 如“将所有待办事项标记为已完成”,则会导致被称为“完成”的数据危害。 Read-After-Write。如果我们使用更合适的数据存储,就不会发生此问题, 但我们会尽量减少此 Codelab 的转换工作量。

有多种方法可以解决此问题,因此,我们将借此机会对 save() 进行微调,具体方法如下: 获取要一次性全部更新的待办事项 ID 数组:

1. 首先,使用 chrome.storage.local.get() 封装 save() 中已有的所有内容 回调:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    // ...
    if (typeof id !== 'object') {
      // ...
    }else {
      // ...
    }
  }.bind(this));
};

2. 使用 chrome.storage.local 转换所有 localStorage 实例:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' ) {
      // ...

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, [updateData]);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

3. 然后更新逻辑以对数组(而不是单个项)执行操作:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' || Array.isArray(id) ) {
      var ids = [].concat( id );
      ids.forEach(function(id) {
        for (var i = 0; i < todos.length; i++) {
          if (todos[i].id == id) {
            for (var x in updateData) {
              todos[i][x] = updateData[x];
            }
          }
        }
      });

      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      todos.push(updateData);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

将待办事项标记为已完成

由于应用程序是在数组上运行,因此您需要更改应用程序处理用户点击 清除已完成的任务 (#) 按钮:

1. 在 controller.js 中,更新 toggleAll() 以使用数组仅调用 toggleComplete() 一次 而不是逐个将待办事项标记为已完成。同时删除对 _filter() 的调用 因为您即将调整 toggleComplete _filter()

Controller.prototype.toggleAll = function (e) {
  var completed = e.target.checked ? 1 : 0;
  var query = 0;
  if (completed === 0) {
    query = 1;
  }
  this.model.read({ completed: query }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.toggleComplete(item.id, e.target, true);
      ids.push(item.id);
    }.bind(this));
    this.toggleComplete(ids, e.target, false);
  }.bind(this));

  this._filter();
};

2. 现在,更新 toggleComplete() 以接受单个待办事项或一系列待办事项。这包括 将 filter() 移到 update() 内部,而不是外部。

Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
  var completed = checkbox.checked ? 1 : 0;
  this.model.update(ids, { completed: completed }, function () {
    if ( ids.constructor != Array ) {
      ids = [ ids ];
    }
    ids.forEach( function(id) {
      var listItem = $$('[data-id="' + id + '"]');
      
      if (!listItem) {
        return;
      }
      
      listItem.className = completed ? 'completed' : '';
      
      // In case it was toggled from an event and not by clicking the checkbox
      listItem.querySelector('input').checked = completed;
    });

    if (!silent) {
      this._filter();
    }

  }.bind(this));
};

Count todo items

After switching to async storage, there is a minor bug that shows up when getting the number of todos. You'll need to wrap the count operation in a callback function:

1. In model.js, update getCount() to accept a callback:

  Model.prototype.getCount = function (callback) {
  var todos = {
    active: 0,
    completed: 0,
    total: 0
  };
  this.storage.findAll(function (data) {
    data.each(function (todo) {
      if (todo.completed === 1) {
        todos.completed++;
      } else {
        todos.active++;
      }
      todos.total++;
    });
    if (callback) callback(todos);
  });
  return todos;
};

2. Back in controller.js, update _updateCount() to use the async getCount() you edited in the previous step:

Controller.prototype._updateCount = function () {
  var todos = this.model.getCount();
  this.model.getCount(function(todos) {
    this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);

    this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
    this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';

    this.$toggleAll.checked = todos.completed === todos.total;

    this._toggleFrame(todos);
  }.bind(this));

};

You are almost there! If you reload the app now, you will be able to insert new todos without any console errors.

Remove todos items

Now that the app can save todo items, you're close to being done! You still get errors when you attempt to remove todo items:

Todo app with localStorage console log error

1. In store.js, convert all the localStorage instances to use chrome.storage.local:

a) To start off, wrap everything already inside remove() with a get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  }.bind(this));
};

b) Then convert the contents within the get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

2. The same Read-After-Write data hazard issue previously present in the save() method is also present when removing items so you will need to update a few more places to allow for batch operations on a list of todo IDs.

a) Still in store.js, update remove():

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    var ids = [].concat(id);
    ids.forEach( function(id) {
      for (var i = 0; i < todos.length; i++) {
        if (todos[i].id == id) {
          todos.splice(i, 1);
          break;
        }
      }
    });

    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

b) In controller.js, change removeCompletedItems() to make it call removeItem() on all IDs at once:

Controller.prototype.removeCompletedItems = function () {
  this.model.read({ completed: 1 }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.removeItem(item.id);
      ids.push(item.id);
    }.bind(this));
    this.removeItem(ids);
  }.bind(this));

  this._filter();
};

c) Finally, still in controller.js, change the removeItem() to support removing multiple items from the DOM at once, and move the _filter() call to be inside the callback:

Controller.prototype.removeItem = function (id) {
  this.model.remove(id, function () {
    var ids = [].concat(id);
    ids.forEach( function(id) {
      this.$todoList.removeChild($$('[data-id="' + id + '"]'));
    }.bind(this));
    this._filter();
  }.bind(this));
  this._filter();
};

放置所有待办事项

store.js 中还有一个使用 localStorage 的方法:

Store.prototype.drop = function (callback) {
  localStorage[this._dbName] = JSON.stringify({todos: []});
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};

当前应用中未调用此方法,因此,如果您想进行额外的挑战,请尝试 可以自行实现提示:请查看 chrome.storage.local.clear()

启动完成的待办事项应用

您已完成第 2 步!重新加载应用,现在您就应该拥有功能完备的 Chrome 封装版本了 TodoMVC 的一个组件。

了解详情

如需详细了解此步骤中引入的一些 API,请参阅:

准备好继续下一步了吗?转到第 3 步 - 添加闹钟和通知 »