2. Adım: Mevcut Bir Web Uygulamasını İçe Aktarın

Bu adımda şunları öğreneceksiniz:

  • Mevcut bir web uygulaması Chrome Uygulamaları platformu için nasıl uyarlanır?
  • Uygulama komut dosyalarınızı İçerik Güvenliği Politikası (İGP) ile uyumlu hale getirme.
  • chrome.storage.local kullanılarak yerel depolama nasıl uygulanır?

Bu adımın tamamlanması için tahmini süre: 20 dakika.
Bu adımda ne tamamlayacağınızı önizlemek için bu sayfanın en altına atlayın ↓.

Mevcut bir Todo uygulamasını içe aktarma

Başlangıç olarak, yaygın bir karşılaştırma uygulaması olan TodoMVC'nin vanilla JavaScript sürümünü projenize aktarın.

TodoMVC uygulamasının bir sürümünü todomvc klasöründeki referans kodu posta koduna ekledik. todomvc'deki tüm dosyaları (klasörler dahil) proje klasörünüze kopyalayın.

todomvc klasörünü codelab klasörüne kopyalayın

index.html dosyasını değiştirmeniz istenir. Devam et ve kabul et.

index.html'yi değiştirin

Artık uygulama klasörünüzde aşağıdaki dosya yapısına sahip olmanız gerekir:

Yeni proje klasörü

Maviyle vurgulanan dosyalar todomvc klasöründen alınır.

Uygulamanızı hemen yeniden yükleyin (sağ tıklayın > Uygulamayı Yeniden Yükle). Temel kullanıcı arayüzünü görürsünüz ancak Yapılacaklar ekleyemezsiniz.

Komut dosyalarını İçerik Güvenliği Politikası (İGP) ile uyumlu hale getirme

Geliştirici Araçları Konsolu'nu açın (sağ tıklayın > Öğeyi İncele'yi, ardından Konsol sekmesini seçin). Satır içi bir komut dosyasını yürütmenin reddedilmesiyle ilgili bir hata görürsünüz:

Todo uygulaması CSP konsolu günlük hatası

Uygulamayı İçerik Güvenliği Politikası uyumlu hale getirerek bu hatayı düzeltelim. En sık karşılaşılan CSP uyumsuzluklarından biri satır içi JavaScript'ten kaynaklanır. Satır içi JavaScript'e örnek olarak DOM özellikleri olarak etkinlik işleyiciler (ör. <button onclick=''>) ve HTML içinde içeriği olan <script> etiketleri verilebilir.

Çözüm oldukça basit: Satır içi içeriği yeni bir dosyaya taşıyın.

1. index.html dosyasının alt kısmına yakın bir yerde, satır içi JavaScript'i kaldırın ve bunun yerine js/bootstrap.js öğesini ekleyin:

<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 klasöründe bootstrap.js adlı bir dosya oluşturun. Önceki satır içi kodu bu dosyanın içine taşıyın:

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

Uygulamayı şimdi yeniden yüklerseniz ancak yaklaşırsanız çalışmayan Todo uygulamanız çalışmaya devam eder.

localStorage'ı chrome.storage.local biçimine dönüştürme

Geliştirici Araçları Konsolu'nu şimdi açarsanız önceki hata giderilmiş olur. Ancak window.localStorage ürününün mevcut olmamasıyla ilgili yeni bir hata var:

Todo uygulaması localStorage konsolu günlük hatası

localStorage eşzamanlı olduğundan Chrome Uygulamaları localStorage'i desteklemez. Tek iş parçacıklı bir çalışma zamanındaki engelleme kaynaklarına (G/Ç) eşzamanlı erişim, uygulamanızı yanıt vermemesine neden olabilir.

Chrome Uygulamaları, nesneleri eşzamansız olarak depolayabilen eşdeğer bir API'ye sahiptir. Bu, bazen maliyetli nesne->dize->nesne serileştirme sürecini önlemeye yardımcı olur.

Uygulamamızdaki hata mesajını düzeltmek için localStorage dosyasını chrome.storage.local biçimine dönüştürmeniz gerekir.

Uygulama izinlerini güncelleme

chrome.storage.local uygulamasını kullanmak için storage iznini istemeniz gerekir. manifest.json dosyasında, permissions dizisine "storage" ekleyin:

"permissions": ["storage"],

local.storage.set() ve local.storage.get() hakkında bilgi edinin

Yapılacaklar listesini kaydetmek ve almak için chrome.storage API'nin set() ve get() yöntemlerini bilmeniz gerekir.

set() yöntemi, anahtar/değer çiftlerinden oluşan bir nesneyi ilk parametre olarak kabul eder. İsteğe bağlı bir geri çağırma işlevi de ikinci parametredir. Örneğin:

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

get() yöntemi, almak istediğiniz veri deposu anahtarları için isteğe bağlı bir ilk parametreyi kabul eder. Tek bir anahtar dize olarak iletilebilir. Birden fazla anahtar bir dize dizisi veya sözlük nesnesi olarak düzenlenebilir.

Zorunlu olan ikinci parametre, bir geri çağırma işlevidir. Döndürülen nesnede, depolanan değerlere erişmek için ilk parametrede istenen anahtarları kullanın. Örneğin:

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

Şu anda chrome.storage.local içinde bulunan her şeyi get() yapmak istiyorsanız ilk parametreyi çıkarın:

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

localStorage'ın aksine, Geliştirici Araçları Kaynaklar panelini kullanarak yerel olarak depolanan öğeleri inceleyemezsiniz. Ancak JavaScript Konsolu'ndan chrome.storage ile şu şekilde etkileşim kurabilirsiniz:

Chrome.storage hata ayıklamak için Console&#39;u kullanma

Gerekli API değişikliklerini önizleyin

Todo uygulamasını dönüştürmenin geri kalan adımlarının çoğu, API çağrılarında yapılan küçük değişikliklerdir. Zaman alıcı ve hataya açık olsa da localStorage uygulamasının kullanıldığı tüm yerlerin değiştirilmesi gerekir.

localStorage ile chrome.storage arasındaki temel farklar, chrome.storage eşzamansız yapısından kaynaklanır:

  • Basit atama kullanarak localStorage işlevine yazmak yerine isteğe bağlı geri çağırmalarla birlikte chrome.storage.local.set() kullanmanız gerekir.

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

    ile

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • localStorage[myStorageName] öğesine doğrudan erişmek yerine chrome.storage.local.get(myStorageName,function(storage){...}) kullanmanız ve ardından döndürülen storage nesnesini geri çağırma işlevinde ayrıştırmanız gerekir.

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

    ile

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • .bind(this) işlevi, this öğesinin Store prototipinin this öğesini belirttiğinden emin olmak için tüm geri çağırmalarda kullanılır. (Bağlı işlevler hakkında daha fazla bilgiyi MDN belgelerinde bulabilirsiniz: Function.prototype.bind().)

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

    ile

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

Yapılacaklar listesindeki öğeleri alma, kaydetme ve kaldırma konularını ele alırken, bu temel farklılıkları aklınızda bulundurun.

Yapılacaklar listesi öğelerini al

Yapılacaklar listesini almak için Todo uygulamasını güncelleyelim:

1. Store oluşturucu yöntemi, Todo uygulamasını veri deposundaki tüm mevcut yapılacaklar öğeleriyle başlatmayı tamamlar. Yöntem önce veri deposunun mevcut olup olmadığını kontrol eder. Aksi takdirde boş bir todos dizisi oluşturur ve çalışma zamanı okuma hatası olmaması için bunu veri deposuna kaydeder.

js/store.js dosyasında, oluşturucu yöntemindeki localStorage kullanımını, bunun yerine chrome.storage.local kullanımına dönüştürün:

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. Modelden yapılacaklar listesi okunurken find() yöntemi kullanılır. Döndürülen sonuçlar, "Tümü", "Etkin" veya "Tamamlandı" ölçütlerine göre filtreleme yapmanıza bağlı olarak değişir.

chrome.storage.local kullanmak için find() kodunu dönüştürün:

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() ile benzer olan findAll(), yapılacak tüm işleri Model'den alır. chrome.storage.local kullanmak için findAll() kodunu dönüştürün:

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

Yapılacaklar listesindeki öğeleri kaydet

Mevcut save() yöntemi zorluk teşkil ediyor. Bu, her seferinde monolitik JSON depolama alanında çalışan iki eşzamansız işleme (alıp ayarla) bağlıdır. Birden fazla yapılacaklar öğesinde (yapılacak işlerin tümünü tamamlandı olarak işaretle) yapılan toplu güncellemeler, Yazma Sonrası Okuma olarak bilinen veri tehlikesine yol açar. IndexedDB gibi daha uygun bir veri depolama kullanıyor olsaydık bu sorun olmazdı ama bu codelab için dönüşüm sürecini en aza indirmeye çalışıyoruz.

Bu sorunu düzeltmenin birkaç yolu vardır. Bu fırsatı değerlendirerek, tümünün güncellenmesini istediğiniz yapılacaklar listesi kimliklerinden oluşan bir diziyi alarak save() özelliğini biraz yeniden düzenlemenizi öneririz:

1. Başlamak için halihazırda save() içinde yer alan her şeyi chrome.storage.local.get() geri çağırmasıyla sarmalayın:

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 ile tüm localStorage örneklerini dönüştürün:

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. Ardından, mantığı tek bir öğe yerine bir dizide çalışacak şekilde güncelleyin:

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

Yapılacaklar listesindeki öğeleri tamamlandı olarak işaretle

Uygulama dizilerde çalıştığına göre, uygulamanın Tamamlananları temizle (#) düğmesini tıklayan kullanıcıları ele alma şeklini değiştirmeniz gerekir:

1. controller.js'de, toggleAll() öğesini tek tek tamamlandı olarak işaretlemek yerine bir yapılacaklar dizisiyle yalnızca bir kez toggleComplete() çağıracak şekilde güncelleyin. Ayrıca toggleComplete _filter() ayarlaması yapacağınız için _filter() çağrısını da silin.

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. Şimdi toggleComplete() uygulamasını hem tek bir yapılacak işi hem de bir yapılacak işler dizisini kabul edecek şekilde güncelleyin. Buna, filter() öğesini dışa değil update() içine taşımak dahildir.

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

Tüm yapılacaklar listesi öğelerini bırak

store.js dosyasında localStorage kullanan bir başka yöntem daha vardır:

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

Bu yöntem mevcut uygulamada çağrılmadığından, ekstra bir zorluk isterseniz kendi başınıza uygulamayı deneyin. İpucu: chrome.storage.local.clear() adresine bir göz atın.

Tamamlanmış Todo uygulamanızı başlatma

2. adımı tamamladınız! Uygulamanızı yeniden yüklediğinizde TodoMVC'nin tam olarak çalışan Chrome paket sürümüne sahip olursunuz.

Daha fazla bilgi için

Bu adımda tanıtılan API'lerden bazıları hakkında daha ayrıntılı bilgi için aşağıdaki makalelere bakın:

Bir sonraki adıma geçmeye hazır mısınız? 3. Adım - Alarm ve bildirim ekleyin » bölümüne gidin