दूसरा चरण: किसी मौजूदा वेब ऐप्लिकेशन को इंपोर्ट करना

इस चरण में आपको पता चलेगा कि:

  • किसी मौजूदा वेब ऐप्लिकेशन को Chrome Apps प्लैटफ़ॉर्म के मुताबिक बनाने का तरीका.
  • अपने ऐप्लिकेशन की स्क्रिप्ट को कॉन्टेंट की सुरक्षा के बारे में नीति (सीएसपी) के मुताबिक बनाने का तरीका.
  • chrome.storage.local का इस्तेमाल करके, लोकल स्टोरेज का इस्तेमाल करने का तरीका जानें.

इस चरण को पूरा करने में लगने वाला अनुमानित समय: 20 मिनट.
इस चरण में आपको क्या पूरा करना है, यह देखने के लिए इस पेज पर सबसे नीचे देखें ↓.

पहले से मौजूद 'Todo ऐप्लिकेशन' को इंपोर्ट करें

शुरुआत के तौर पर, अपने प्रोजेक्ट में TodoMVC का वैनिला JavaScript वर्शन इंपोर्ट करें. यह एक आम बेंचमार्क ऐप्लिकेशन है.

हमने todomvc फ़ोल्डर में, पहचान कोड पिन में TodoMVC ऐप्लिकेशन का एक वर्शन शामिल किया है. todomvc से सभी फ़ाइलों (फ़ोल्डर सहित) को अपने प्रोजेक्ट फ़ोल्डर में कॉपी करें.

todomvc फ़ोल्डर को कोडलैब फ़ोल्डर में कॉपी करें

आपसे index.html को बदलने के लिए कहा जाएगा. न्योता स्वीकार करें.

index.html को बदलें

अब आपके ऐप्लिकेशन फ़ोल्डर में फ़ाइल का यह स्ट्रक्चर होना चाहिए:

नया प्रोजेक्ट फ़ोल्डर

नीले रंग से हाइलाइट की गई फ़ाइलें todomvc फ़ोल्डर की हैं.

अपना ऐप्लिकेशन अभी फिर से लोड करें (राइट-क्लिक > ऐप्लिकेशन फिर से लोड करें). आपको बेसिक यूज़र इंटरफ़ेस (यूआई) दिखेगा, लेकिन सूची में बदलाव नहीं किए जा सकेंगे.

स्क्रिप्ट को कॉन्टेंट की सुरक्षा के बारे में नीति (सीएसपी) के मुताबिक बनाएं

DevTools कंसोल खोलें (राइट क्लिक करें > एलिमेंट की जांच करें, फिर कंसोल टैब चुनें). इनलाइन स्क्रिप्ट को लागू करने से मना करने के बारे में आपको गड़बड़ी का मैसेज दिखेगा:

सीएसपी कंसोल लॉग में गड़बड़ी वाला टूडू ऐप्लिकेशन

ऐप्लिकेशन को कॉन्टेंट की सुरक्षा के बारे में नीति के मुताबिक बनाकर, इस गड़बड़ी को ठीक करते हैं. इनलाइन JavaScript की वजह से सीएसपी का पालन नहीं होता है. इनलाइन JavaScript के उदाहरणों में डीओएम एट्रिब्यूट के तौर पर इवेंट हैंडलर (जैसे कि <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 = {};

ऐप्लिकेशन को फिर से लोड करने पर, काम न करने वाला Todo ऐप्लिकेशन अब भी आपके पास ही रहेगा.

localStorage को chrome.storage.local में बदलें

अगर आपने DevTools कंसोल अभी खोला, तो पिछली गड़बड़ी हट जाएगी. एक नई गड़बड़ी हुई है, हालांकि, window.localStorage के उपलब्ध न होने के बारे में:

localStorage कंसोल लॉग में गड़बड़ी वाला टोडो ऐप

Chrome ऐप्स में localStorage का इस्तेमाल नहीं किया जा सकता, क्योंकि localStorage सिंक्रोनस है. सिंगल-थ्रेड रनटाइम में ब्लॉक करने वाले रिसॉर्स (I/O) का सिंक्रोनस ऐक्सेस, आपके ऐप्लिकेशन को काम नहीं कर सकता.

Chrome ऐप्लिकेशन में एक जैसा एपीआई होता है जो ऑब्जेक्ट को एसिंक्रोनस तरीके से सेव कर सकता है. इससे कभी-कभी महंगे ऑब्जेक्ट->स्ट्रिंग->ऑब्जेक्ट को छोटे-छोटे क्रम में लगाने की प्रोसेस से बचा जा सकता है.

हमारे ऐप्लिकेशन में गड़बड़ी के मैसेज को ठीक करने के लिए, आपको localStorage को chrome.storage.local में बदलना होगा.

ऐप्लिकेशन अनुमतियां अपडेट करें

chrome.storage.local का इस्तेमाल करने के लिए, आपको storage की अनुमति का अनुरोध करना होगा. manifest.json में जाकर, permissions कलेक्शन में "storage" को जोड़ें:

"permissions": ["storage"],

local.storage.set() और local.storage.get() के बारे में जानें

काम की सूची वाले आइटम को सेव और वापस लाने के लिए, आपको chrome.storage API के set() और get() तरीकों के बारे में जानना होगा.

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

अगर आपको chrome.storage.local में मौजूद सभी चीज़ों को get() करना है, तो पहले पैरामीटर को छोड़ दें:

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

localStorage के उलट, आप DevTools के संसाधन पैनल का इस्तेमाल करके, स्थानीय तौर पर सेव किए गए आइटम की जांच नहीं कर पाएंगे. हालांकि, JavaScript कंसोल से chrome.storage के साथ इस तरह से इंटरैक्ट किया जा सकता है:

chrome.storage को डीबग करने के लिए कंसोल का इस्तेमाल करना

एपीआई में हुए ज़रूरी बदलावों की झलक देखें

Todo ऐप्लिकेशन को फ़ॉर्मैट करने के बाकी बचे ज़्यादातर चरण, एपीआई कॉल में छोटे-छोटे बदलाव होते हैं. उन सभी जगहों को बदलना ज़रूरी है जहां फ़िलहाल localStorage का इस्तेमाल किया जा रहा है. हालांकि, इसमें समय लगता है और इसमें गड़बड़ी भी हो सकती है.

localStorage और chrome.storage के बीच मुख्य अंतर, chrome.storage के एसिंक्रोनस तरीके से है:

  • आसान असाइनमेंट का इस्तेमाल करके localStorage को लिखने के बजाय, आपको chrome.storage.local.set() को वैकल्पिक कॉलबैक के साथ इस्तेमाल करना होगा.

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

    बनाम

    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;
    

    बनाम

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • सभी कॉलबैक पर .bind(this) फ़ंक्शन का इस्तेमाल किया जाता है, ताकि यह पक्का किया जा सके कि this, Store प्रोटोटाइप का this है. (बाउंड फ़ंक्शन के बारे में ज़्यादा जानकारी के लिए एमडीएन दस्तावेज़ देखें: Function.prototype.bind().)

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

    बनाम

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

इन मुख्य अंतरों को ध्यान में रखें क्योंकि हम नीचे दिए गए सेक्शन में काम की सूची वापस पाने, सेव करने, और हटाने के बारे में बात करते हैं.

काम की सूची वापस पाएं

ज़रूरी कामों की सूची वापस पाने के लिए, 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() का मौजूदा तरीका एक चुनौती है. यह दो एसिंक्रोनस कार्रवाइयों (पाएं और सेट करें) पर निर्भर करता है, जो हर बार पूरे मोनोलिथिक JSON स्टोरेज पर ऑपरेट होती हैं. एक से ज़्यादा टूडू आइटम पर बैच अपडेट होने पर, जैसे कि "सभी कामों की सूची को 'पूरा हुआ' के तौर पर मार्क करें". ऐसे में, डेटा का खतरा दिखेगा. इस तरह के डेटा को Read-After-Write कहते हैं. अगर हम IndexedDB जैसे ज़्यादा सही डेटा स्टोरेज का इस्तेमाल कर रहे हों, तो यह समस्या नहीं होती. हालांकि, हम इस कोडलैब के लिए कन्वर्ज़न की कोशिश को कम करने की कोशिश कर रहे हैं.

इसे ठीक करने के कई तरीके हैं. हम इस अवसर का इस्तेमाल करके, save() के लिए एक छोटा सा बदलाव करेंगे. इसके लिए, काम की कई आईडी इकट्ठा करेंगे, ताकि इन्हें एक ही बार में अपडेट किया जा सके:

1. शुरू करने के लिए, save() में पहले से मौजूद सभी चीज़ों को 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. सभी localStorage इंस्टेंस को 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. इसके बाद, किसी एक आइटम के बजाय, किसी ऐरे पर काम करने के लिए लॉजिक को अपडेट करें:

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() देखें.

वह ऐप्लिकेशन लॉन्च करें जिसे पूरा कर लिया गया है

आपने दूसरा चरण पूरा कर लिया है! अपने ऐप्लिकेशन को फिर से लोड करें. इसके बाद, आपके पास TodoMVC का पूरी तरह से काम करने वाला Chrome पैकेज किया गया वर्शन होगा.

ज़्यादा जानकारी के लिए

इस चरण में पेश किए गए कुछ एपीआई के बारे में ज़्यादा जानकारी के लिए, यहां देखें:

क्या आप अगले चरण पर जाने के लिए तैयार हैं? तीसरा चरण - अलार्म और सूचनाएं जोड़ें » पर जाएं