Bước 2: Nhập một ứng dụng web hiện có

Trong bước này, bạn sẽ tìm hiểu:

  • Cách điều chỉnh một ứng dụng web hiện có cho nền tảng Ứng dụng Chrome.
  • Cách làm cho tập lệnh ứng dụng tuân thủ Chính sách bảo mật nội dung (CSP).
  • Cách triển khai bộ nhớ cục bộ bằng chrome.storage.local.

Thời gian hoàn thành bước này ước tính là 20 phút.
Để xem trước nội dung bạn sẽ hoàn tất trong bước này, hãy chuyển xuống cuối trang này ↓.

Nhập ứng dụng Todo hiện có

Để bắt đầu, hãy nhập phiên bản JavaScript thuần tuý của TodoMVC, một ứng dụng đo điểm chuẩn phổ biến, vào dự án của bạn.

Chúng tôi đã đưa một phiên bản của ứng dụng TodoMVC vào tệp zip mã tham chiếu trong thư mục todomvc. Sao chép tất cả các tệp (bao gồm cả thư mục) từ todomvc vào thư mục dự án.

Sao chép thư mục todomvc vào thư mục lớp học lập trình

Bạn sẽ được yêu cầu thay thế index.html. Hãy tiếp tục và chấp nhận.

Thay thế index.html

Bây giờ, bạn sẽ có cấu trúc tệp sau trong thư mục ứng dụng:

Thư mục dự án mới

Các tệp được đánh dấu màu xanh dương là từ thư mục todomvc.

Tải lại ứng dụng ngay (nhấp chuột phải > Tải lại ứng dụng). Bạn sẽ thấy giao diện người dùng cơ bản nhưng không thể thêm việc cần làm.

Tuân thủ Chính sách bảo mật nội dung (CSP) cho tập lệnh

Mở Bảng điều khiển Công cụ cho nhà phát triển (nhấp chuột phải > Kiểm tra phần tử, sau đó chọn thẻ Bảng điều khiển). Bạn sẽ thấy lỗi khi từ chối thực thi một tập lệnh cùng dòng:

Ứng dụng Todo có lỗi nhật ký bảng điều khiển CSP

Hãy khắc phục lỗi này bằng cách đảm bảo ứng dụng tuân thủ Chính sách bảo mật nội dung. Một trong những lỗi vi phạm phổ biến nhất về CSP là do JavaScript nội tuyến gây ra. Ví dụ về JavaScript nội tuyến bao gồm trình xử lý sự kiện dưới dạng thuộc tính DOM (ví dụ: <button onclick=''>) và thẻ <script> có nội dung bên trong HTML.

Giải pháp rất đơn giản: di chuyển nội dung cùng dòng sang một tệp mới.

1. Ở gần cuối index.html, hãy xoá JavaScript nội tuyến và thay vào đó, hãy thêm 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. Tạo tệp trong thư mục js có tên bootstrap.js. Di chuyển mã cùng dòng trước đó vào tệp này:

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

Ứng dụng Todo vẫn sẽ không hoạt động nếu bạn tải lại ứng dụng ngay bây giờ, nhưng bạn đã gần hoàn thành.

Chuyển đổi localStorage thành chrome.storage.local

Nếu bạn mở Bảng điều khiển DevTools ngay bây giờ, lỗi trước đó sẽ biến mất. Tuy nhiên, có một lỗi mới về việc window.localStorage không có sẵn:

Ứng dụng Todo có lỗi nhật ký bảng điều khiển localStorage

Ứng dụng Chrome không hỗ trợ localStoragelocalStorage là đồng bộ. Việc truy cập đồng bộ vào các tài nguyên chặn (I/O) trong thời gian chạy đơn luồng có thể khiến ứng dụng của bạn không phản hồi.

Ứng dụng Chrome có API tương đương có thể lưu trữ các đối tượng không đồng bộ. Điều này sẽ giúp tránh được quá trình chuyển đổi tuần tự đối tượng->chuỗi->đối tượng đôi khi tốn kém.

Để giải quyết thông báo lỗi trong ứng dụng, bạn cần chuyển đổi localStorage thành chrome.storage.local.

Cập nhật quyền cho ứng dụng

Để sử dụng chrome.storage.local, bạn cần yêu cầu quyền storage. Trong manifest.json, hãy thêm "storage" vào mảng permissions:

"permissions": ["storage"],

Tìm hiểu về local.storage.set() và local.storage.get()

Để lưu và truy xuất các mục việc cần làm, bạn cần biết về các phương thức set()get() của API chrome.storage.

Phương thức set() chấp nhận một đối tượng của các cặp khoá-giá trị làm tham số đầu tiên. Tham số thứ hai là hàm callback (không bắt buộc). Ví dụ:

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

Phương thức get() chấp nhận tham số đầu tiên không bắt buộc cho các khoá kho dữ liệu mà bạn muốn truy xuất. Một khoá có thể được truyền dưới dạng chuỗi; nhiều khoá có thể được sắp xếp vào một mảng các chuỗi hoặc một đối tượng từ điển.

Tham số thứ hai (bắt buộc) là một hàm callback. Trong đối tượng được trả về, hãy sử dụng các khoá được yêu cầu trong tham số đầu tiên để truy cập vào các giá trị được lưu trữ. Ví dụ:

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

Nếu bạn muốn get() mọi thứ hiện có trong chrome.storage.local, hãy bỏ qua tham số đầu tiên:

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

Không giống như localStorage, bạn sẽ không thể kiểm tra các mục được lưu trữ cục bộ bằng bảng điều khiển Tài nguyên DevTools. Tuy nhiên, bạn có thể tương tác với chrome.storage từ Bảng điều khiển JavaScript như sau:

Sử dụng Console để gỡ lỗi chrome.storage

Xem trước các thay đổi bắt buộc về API

Hầu hết các bước còn lại trong quá trình chuyển đổi ứng dụng Việc cần làm đều là những thay đổi nhỏ đối với lệnh gọi API. Bạn cần thay đổi tất cả các vị trí hiện đang sử dụng localStorage, mặc dù việc này tốn thời gian và dễ xảy ra lỗi.

Sự khác biệt chính giữa localStoragechrome.storage đến từ bản chất không đồng bộ của chrome.storage:

  • Thay vì ghi vào localStorage bằng cách chỉ định đơn giản, bạn cần sử dụng chrome.storage.local.set() với các lệnh gọi lại không bắt buộc.

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

    đấu với

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Thay vì truy cập trực tiếp localStorage[myStorageName], bạn cần sử dụng chrome.storage.local.get(myStorageName,function(storage){...}), sau đó phân tích cú pháp đối tượng storage được trả về trong hàm callback.

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

    đấu với

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • Hàm .bind(this) được dùng trên tất cả các lệnh gọi lại để đảm bảo this tham chiếu đến this của nguyên mẫu Store. (Bạn có thể xem thêm thông tin về các hàm liên kết trong tài liệu MDN: Function.prototype.bind().)

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

    đấu với

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

Hãy ghi nhớ những điểm khác biệt chính này khi chúng ta tìm hiểu về việc truy xuất, lưu và xoá các mục việc cần làm trong các phần sau.

Truy xuất các mục việc cần làm

Hãy cập nhật ứng dụng Todo để truy xuất các mục việc cần làm:

1. Phương thức hàm khởi tạo Store đảm nhận việc khởi chạy ứng dụng Việc cần làm với tất cả các mục việc cần làm hiện có trong kho dữ liệu. Trước tiên, phương thức này sẽ kiểm tra xem kho dữ liệu có tồn tại hay không. Nếu không, lớp này sẽ tạo một mảng trống của todos và lưu mảng đó vào kho dữ liệu để không có lỗi đọc trong thời gian chạy.

Trong js/store.js, hãy chuyển đổi việc sử dụng localStorage trong phương thức hàm khởi tạo để sử dụng 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. Phương thức find() được dùng khi đọc việc cần làm qua Mô hình. Kết quả được trả về sẽ thay đổi tuỳ thuộc vào việc bạn lọc theo "Tất cả", "Đang hoạt động" hay "Đã hoàn tất".

Chuyển đổi find() để sử dụng 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. Tương tự như find(), findAll() nhận tất cả việc cần làm từ Mô hình. Chuyển đổi findAll() để sử dụng 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));
};

Lưu các mục việc cần làm

Phương thức save() hiện tại đang gặp một thách thức. Điều này phụ thuộc vào hai thao tác không đồng bộ (get và set) hoạt động trên toàn bộ bộ nhớ JSON nguyên khối mỗi lần. Việc cập nhật theo lô cho nhiều mục việc cần làm, chẳng hạn như "đánh dấu tất cả việc cần làm là đã hoàn thành", sẽ dẫn đến mối nguy hiểm dữ liệu được gọi là Đọc sau khi ghi. Vấn đề này sẽ không xảy ra nếu chúng ta sử dụng một bộ nhớ dữ liệu phù hợp hơn, như IndexedDB, nhưng chúng ta đang cố gắng giảm thiểu nỗ lực chuyển đổi cho lớp học lập trình này.

Có một số cách để khắc phục vấn đề này, vì vậy, chúng ta sẽ tận dụng cơ hội này để tái cấu trúc một chút save() bằng cách lấy một mảng mã nhận dạng việc cần làm để cập nhật cùng một lúc:

1. Để bắt đầu, hãy gói mọi thứ đã có bên trong save() bằng lệnh gọi lại chrome.storage.local.get():

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. Chuyển đổi tất cả các thực thể localStorage bằng chrome.storage.local:

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. Sau đó, hãy cập nhật logic để hoạt động trên một mảng thay vì một mục duy nhất:

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

Đánh dấu việc cần làm là đã hoàn thành

Bây giờ, ứng dụng đang hoạt động trên các mảng, bạn cần thay đổi cách ứng dụng xử lý việc người dùng nhấp vào nút Xoá đã hoàn thành (#):

1. Trong controller.js, hãy cập nhật toggleAll() để chỉ gọi toggleComplete() một lần với một mảng việc cần làm thay vì đánh dấu từng việc cần làm là đã hoàn thành. Ngoài ra, hãy xoá lệnh gọi đến _filter() vì bạn sẽ điều chỉnh 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. Bây giờ, hãy cập nhật toggleComplete() để chấp nhận cả một việc cần làm hoặc một mảng việc cần làm. Điều này bao gồm việc di chuyển filter() vào bên trong update() thay vì bên ngoài.

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();
};

Bỏ tất cả việc cần làm

Còn một phương thức khác trong store.js sử dụng localStorage:

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

Phương thức này không được gọi trong ứng dụng hiện tại, vì vậy, nếu bạn muốn thêm thử thách, hãy thử tự triển khai phương thức này. Gợi ý: Hãy xem chrome.storage.local.clear().

Chạy ứng dụng Todo đã hoàn thiện

Bạn đã hoàn tất Bước 2! Tải lại ứng dụng và giờ đây, bạn sẽ có phiên bản TodoMVC được đóng gói trong Chrome hoạt động đầy đủ.

Thông tin khác

Để biết thêm thông tin chi tiết về một số API được giới thiệu trong bước này, hãy tham khảo:

Bạn đã sẵn sàng chuyển sang bước tiếp theo? Chuyển đến Bước 3 – Thêm chuông báo và thông báo »