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 ứng dụng web hiện có cho nền tảng Ứng dụng Chrome.
  • Cách đảm bảo 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 ước tính để hoàn tất bước này: 20 phút.
Để xem trước những nội dung bạn sẽ hoàn tất ở bước này, hãy chuyển xuống cuối trang này ↓.

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

Trước tiên, hãy nhập phiên bản JavaScript Vanilla 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 mã zip của mã tham chiếu trong thư mục todomvc. Sao chép tất 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 đây trong thư mục ứng dụng của mình:

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

Các tệp được đánh dấu bằng 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 sẽ không thể thêm việc cần làm.

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

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 về việc từ chối thực thi tập lệnh cùng dòng:

Ứng dụng việc cần làm có lỗi nhật ký trong bảng điều khiển CSP

Hãy khắc phục lỗi này bằng cách làm cho ứng dụng tuân thủ Chính sách bảo mật nội dung. Một trong những lỗi không tuân thủ CSP phổ biến nhất là do JavaScript cùng dòng. Ví dụ về JavaScript cùng dòng 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 trên cùng dòng sang một tệp mới.

1. Ở gần cuối index.html, hãy xoá JavaScript cùng dòng và thay vào đó, hãy bao gồ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 một 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 = {};

Nếu tải lại ứng dụng ngay lúc này, nhưng ứng dụng Todo vẫn đang hoạt động.

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

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

Ứng dụng việc cần làm gặp lỗi nhật ký bảng điều khiển localStorage

Ứng dụng Chrome không hỗ trợ localStoragelocalStorage có tính chất đồng bộ. Quyền truy cập đồng bộ vào 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ữ đối tượng một cách không đồng bộ. Việc này sẽ giúp tránh được quá trình chuyển đổi tuần tự đối tượng->string->đố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 tệp 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ề 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 thông số đầu tiên. Hàm callback không bắt buộc là tham số thứ hai. 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 thông 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. Có thể truyền một khoá dưới dạng chuỗi; nhiều khoá có thể được sắp xếp thành một mảng 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 nội dung 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 của Công cụ cho nhà phát triển. 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 Bảng điều khiển để 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 Todo đều là những thay đổi nhỏ đối với lệnh gọi API. Việc thay đổi tất cả các vị trí mà localStorage hiện đang được sử dụng mặc dù tốn thời gian và dễ xảy ra lỗi nhưng bạn vẫn bắt buộc phải thay đổi.

Những điểm 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 gán đơ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);
    

    so với

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Thay vì truy cập trực tiếp vào localStorage[myStorageName], bạn cần sử dụng chrome.storage.local.get(myStorageName,function(storage){...}) rồi 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;
    

    so 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 về MDN: Function.prototype.bind().)

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

    so 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 lưu ý những điểm khác biệt chính này khi chúng tôi đề cập đến 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 Việc cần làm để 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 sẽ đảm nhận việc khởi chạy ứng dụng Việc cần làm bằng tất cả các mục việc cần làm hiện có từ 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, tuỳ chọn này sẽ tạo một mảng todos trống và lưu 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 sử dụng khi đọc việc cần làm từ Mô hình. Kết quả được trả về sẽ thay đổi tuỳ theo việc bạn đang 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 có 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úc. Mọi 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ột mối nguy hiểm về dữ liệu dưới tên Đọc-Sau-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 thích hợp hơn, chẳng hạn như IndexedDB, nhưng chúng tôi đ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ì vậy, chúng ta sẽ tận dụng cơ hội này để tái cấu trúc save() một chút bằng cách cập nhật một loạt mã việc cần làm cùng lúc:

1. Để bắt đầu, hãy gói mọi thứ đã có sẵn 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ả bản sao 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:

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 các mục việc cần làm là hoàn thành

Hiện tại, ứ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 loạt việc cần làm thay vì đánh dấu việc cần làm là đã hoàn thành từng việc một. Ngoài ra, 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 đơn lẻ hoặc một loạt 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ả mục việc cần làm

Có 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. 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 đóng gói đầy đủ hoạt động của Chrome.

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 để tiếp tục bước tiếp theo? Chuyển tới Bước 3 – Thêm chuông báo và thông báo »