Langkah 2: Impor Aplikasi Web yang Ada

Pada langkah ini, Anda akan mempelajari:

  • Cara menyesuaikan aplikasi web yang ada untuk platform Aplikasi Chrome.
  • Cara membuat skrip aplikasi yang mematuhi Kebijakan Keamanan Konten (CSP).
  • Cara menerapkan penyimpanan lokal menggunakan chrome.storage.local.

Perkiraan waktu untuk menyelesaikan langkah ini: 20 menit.
Untuk melihat pratinjau apa yang akan Anda selesaikan pada langkah ini, turun ke bagian bawah halaman ini ↓.

Mengimpor aplikasi Daftar Tugas yang sudah ada

Sebagai titik awal, impor versi JavaScript vanilla TodoMVC, yang merupakan tolok ukur umum aplikasi Anda, ke dalam project Anda.

Kami telah menyertakan versi aplikasi TodoMVC di kode pos kode referensi di todomvc folder tersebut. Salin semua file (termasuk folder) dari todomvc ke folder project Anda.

Salin folder todomvc ke folder codelab

Anda akan diminta untuk mengganti index.html. Lanjutkan dan terima.

Ganti index.html

Sekarang Anda akan memiliki struktur file berikut di folder aplikasi:

Folder project baru

File yang ditandai dengan warna biru berasal dari folder todomvc.

Muat ulang aplikasi Anda sekarang (klik kanan > Reload App). Anda akan melihat UI dasar, tetapi Anda tidak akan dapat menambahkan daftar tugas.

Membuat skrip yang mematuhi Kebijakan Keamanan Konten (CSP)

Buka DevTools Console (klik kanan > Inspect Element, lalu pilih tab Console). Anda akan melihat kesalahan tentang menolak mengeksekusi skrip inline:

Aplikasi daftar tugas dengan error log konsol CSP

Mari kita perbaiki error ini dengan membuat aplikasi mematuhi Kebijakan Keamanan Konten. Salah satu cara yang paling ketidakpatuhan CSP yang umum disebabkan oleh JavaScript inline. Contoh JavaScript sebaris meliputi sebagai atribut DOM (misalnya, <button onclick=''>) dan tag <script> dengan konten di dalam HTML.

Solusinya sederhana: pindahkan konten inline ke file baru.

1. Di dekat bagian bawah index.html, hapus JavaScript inline, lalu sertakan 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. Buat file di folder js dengan nama bootstrap.js. Memindahkan kode inline sebelumnya yang ada dalam file ini:

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

Anda akan memiliki aplikasi Daftar Tugas yang tidak berfungsi jika memuat ulang aplikasi sekarang, tetapi prosesnya semakin dekat.

Konversi localStorage ke chrome.storage.local

Jika Anda membuka DevTools Console sekarang, error sebelumnya akan hilang. Ada sebuah {i>error<i} baru, namun, sekitar window.localStorage tidak tersedia:

Aplikasi daftar tugas dengan error log konsol localStorage

Aplikasi Chrome tidak mendukung localStorage karena localStorage bersifat sinkron. Akses sinkron ke resource pemblokiran (I/O) dalam runtime thread tunggal dapat membuat aplikasi Anda tidak responsif.

Aplikasi Chrome memiliki API setara yang dapat menyimpan objek secara asinkron. Hal ini akan membantu menghindari terkadang memerlukan proses serialisasi objek->string->objek yang mahal.

Untuk mengatasi pesan error di aplikasi kita, Anda harus mengonversi localStorage menjadi chrome.storage.local.

Memperbarui izin aplikasi

Untuk menggunakan chrome.storage.local, Anda perlu meminta izin storage. Di beberapa manifest.json, tambahkan "storage" ke array permissions:

"permissions": ["storage"],

Mempelajari tentang local.storage.set() dan local.storage.get()

Untuk menyimpan dan mengambil item daftar tugas, Anda perlu mengetahui metode set() dan get() dari API chrome.storage.

Metode set() menerima objek pasangan nilai kunci sebagai parameter pertamanya. Atribut adalah parameter kedua. Contoh:

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

Metode get() menerima parameter pertama opsional untuk kunci datastore yang ingin Anda retreive. Kunci tunggal bisa diteruskan sebagai string; beberapa kunci dapat disusun menjadi sebuah {i>array <i}yang {i>string<i} atau objek kamus.

Parameter kedua, yang wajib ada, adalah fungsi callback. Dalam objek yang ditampilkan, gunakan metode kunci yang diminta dalam parameter pertama untuk mengakses nilai yang disimpan. Contoh:

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

Jika Anda ingin melakukan get() semua yang saat ini ada di chrome.storage.local, hapus baris pertama :

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

Tidak seperti localStorage, Anda tidak akan dapat memeriksa item yang disimpan secara lokal menggunakan DevTools Panel Resource. Namun, Anda dapat berinteraksi dengan chrome.storage dari Konsol JavaScript seperti jadi:

Menggunakan Konsol untuk men-debug chrome.storage

Pratinjau perubahan API yang diperlukan

Sebagian besar langkah yang tersisa dalam mengonversi aplikasi Todo adalah perubahan kecil pada panggilan API. Mengubah semua tempat penggunaan localStorage saat ini, meskipun memakan waktu dan rentan terhadap error, tidak diperlukan.

Perbedaan utama antara localStorage dan chrome.storage berasal dari sifat asinkron chrome.storage:

  • Alih-alih menulis ke localStorage menggunakan tugas sederhana, Anda perlu menggunakan chrome.storage.local.set() dengan callback opsional.

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

    versus

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Daripada mengakses localStorage[myStorageName] secara langsung, Anda harus menggunakan chrome.storage.local.get(myStorageName,function(storage){...}), lalu uraikan yang ditampilkan storage dalam fungsi callback.

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

    versus

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • Fungsi .bind(this) digunakan di semua callback untuk memastikan this merujuk ke this Prototipe Store. (Info selengkapnya tentang fungsi terikat dapat ditemukan di dokumen MDN: Function.prototype.bind().)

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

    versus

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

Ingatlah perbedaan-perbedaan utama ini saat kami membahas mengambil, menyimpan, dan menghapus item daftar tugas di bagian berikut ini.

Mengambil item daftar tugas

Mari kita update aplikasi Daftar Tugas untuk mengambil item daftar tugas:

1. Metode konstruktor Store menangani inisialisasi aplikasi Todo dengan semua item daftar tugas dari datastore. Metode ini terlebih dahulu memeriksa apakah datastore ada. Jika tidak, membuat array kosong todos dan menyimpannya ke datastore sehingga tidak ada error pembacaan runtime.

Di js/store.js, konversikan penggunaan localStorage dalam metode konstruktor untuk menggunakan 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. Metode find() digunakan saat membaca daftar tugas dari Model. Hasil yang dikembalikan berubah berdasarkan apakah Anda memfilter menurut "Semua", "Aktif", atau "Selesai".

Konversikan find() untuk menggunakan 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. Serupa dengan find(), findAll() menyelesaikan semua tugas dari Model. Konversikan findAll() untuk menggunakan 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));
};

Simpan item daftar tugas

Metode save() saat ini menghadirkan tantangan. Kode ini bergantung pada dua operasi asinkron (get dan set) yang beroperasi di seluruh penyimpanan JSON monolitik setiap saat. Kumpulan update apa pun di lebih dari satu item daftar tugas, seperti "tandai semua daftar tugas sebagai selesai", akan mengakibatkan bahaya data yang dikenal sebagai Baca-Setelah-Tulis. Masalah ini tidak akan terjadi jika kita menggunakan penyimpanan data yang lebih sesuai, seperti IndexedDB, tetapi kami mencoba meminimalkan upaya konversi untuk codelab ini.

Ada beberapa cara untuk memperbaikinya, sehingga kita akan menggunakan peluang ini untuk sedikit memfaktorkan ulang save() dengan mengambil serangkaian ID daftar tugas untuk diperbarui sekaligus:

1. Untuk memulai, gabungkan semua yang sudah ada di dalam save() dengan chrome.storage.local.get() telepon balik:

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. Konversikan semua instance localStorage dengan 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. Kemudian, perbarui logika agar dapat beroperasi pada array, bukan satu item:

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

Tandai item daftar tugas sebagai selesai

Setelah aplikasi tersebut beroperasi pada array, Anda perlu mengubah cara aplikasi menangani pengguna yang mengklik Tombol Hapus yang selesai (#):

1. Di controller.js, perbarui toggleAll() untuk memanggil toggleComplete() hanya sekali dengan array daftar tugas alih-alih menandai tugas sebagai selesai satu per satu. Hapus juga panggilan ke _filter() karena Anda akan menyesuaikan 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. Sekarang update toggleComplete() untuk menerima satu daftar tugas atau array daftar tugas. Hal ini mencakup memindahkan filter() agar berada di dalam update(), bukan di luar.

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

Lepaskan semua item daftar tugas

Ada satu metode lagi di store.js menggunakan localStorage:

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

Metode ini tidak dipanggil dalam aplikasi saat ini. Jadi, jika Anda menginginkan tantangan tambahan, coba menerapkannya sendiri. Petunjuk: Lihat chrome.storage.local.clear().

Meluncurkan aplikasi Daftar Tugas yang sudah selesai

Anda sudah menyelesaikan Langkah 2! Muat ulang aplikasi dan Anda kini seharusnya memiliki versi paket Chrome yang berfungsi sepenuhnya dari TodoMVC.

Untuk informasi selengkapnya

Untuk informasi yang lebih mendetail tentang beberapa API yang diperkenalkan di langkah ini, lihat:

Siap untuk melanjutkan ke langkah berikutnya? Lanjutkan ke Langkah 3 - Tambahkan alarm dan notifikasi »