الخطوة الثانية: استيراد تطبيق ويب حالي

في هذه الخطوة، ستتعرف على:

  • كيفية تكييف تطبيق ويب حالي مع النظام الأساسي لتطبيقات Chrome.
  • كيفية التأكّد من التزام النصوص البرمجية لتطبيقك بسياسة أمان المحتوى (CSP)
  • كيفية تنفيذ التخزين المحلي باستخدام chrome.storage.local.

الوقت المقدّر لإكمال هذه الخطوة: 20 دقيقة.
لمعاينة ما ستُكمله في هذه الخطوة، انتقِل إلى أسفل هذه الصفحة ↓.

استيراد تطبيق Todo حالي

كنقطة انطلاق، عليك استيراد إصدار JavaScript من TodoMVC، وهو مقياس أداء مشترك. تطبيقك في مشروعك.

لقد أدرجنا إصدارًا من تطبيق TodoMVC في رمز الرمز المرجعي في ملف todomvc. المجلد. انسخ جميع الملفات (بما في ذلك المجلدات) من todomvc إلى مجلد مشروعك.

نسخ مجلد todomvc إلى مجلد درس تطبيقي حول الترميز

سيُطلب منك استبدال ملف index.html. لا تتردّد في قبول العرض.

استبدل index.html

من المفترض أن تكون لديك الآن بنية الملف التالية في مجلد التطبيق:

مجلد مشروع جديد

الملفات المميزة باللون الأزرق هي من مجلد todomvc.

أعِد تحميل تطبيقك الآن (انقر بزر الماوس الأيمن > إعادة تحميل التطبيق). ينبغي أن تظهر لك واجهة المستخدم الأساسية قادرًا على إضافة مهامك.

جعل النصوص البرمجية تمتثل لسياسة أمان المحتوى (CSP)

افتح "وحدة التحكّم في أدوات مطوّري البرامج" (انقر بزر الماوس الأيمن > فحص العنصر، ثم اختَر علامة التبويب وحدة التحكّم). إِنْتَ سيظهر لك خطأ حول رفض تنفيذ نص برمجي مضمّن:

تطبيق Todo الذي يتضمّن خطأ في سجلّ وحدة تحكّم سياسة أمان المحتوى (CSP)

يمكننا إصلاح هذا الخطأ من خلال جعل التطبيق متوافقًا مع سياسة أمان المحتوى. ومن بين أكثر يرجع سبب عدم امتثال سياسة أمان المحتوى (CSP) الشائع إلى JavaScript المضمَّنة. تتضمن أمثلة JavaScript المضمنة معالِجات الأحداث كسمات DOM (مثل <button onclick=''>) وعلامات <script> مع محتوى داخل HTML.

الحل بسيط: نقل المحتوى المضمّن إلى ملف جديد.

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

إذا فتحت وحدة التحكّم في أدوات مطوّري البرامج الآن، من المفترض أن يكون الخطأ السابق قد اختفى. حدث خطأ جديد، ومع ذلك، حول عدم توفر window.localStorage:

تطبيق &quot;مهام&quot; يعرض خطأً في سجلّ وحدة التحكّم في التخزين المحلي

لا تدعم تطبيقات Chrome استخدام localStorage لأن localStorage متزامن. الوصول المتزامن قد يؤدي حظر الموارد (I/O) في بيئة تشغيل تتضمن سلسلة محادثات واحدة إلى عدم استجابة التطبيق.

تحتوي تطبيقات Chrome على واجهة برمجة تطبيقات مكافئة يمكنها تخزين العناصر بشكل غير متزامن. سيساعد هذا في تجنب عملية تسلسل الكائنات المكلفة أحيانًا object->string->.

لمعالجة رسالة الخطأ في التطبيق، عليك تحويل localStorage إلى chrome.storage.local.

تحديث أذونات التطبيقات

لاستخدام chrome.storage.local، عليك طلب إذن storage. ضِمن manifest.json، أضِف "storage" إلى مصفوفة permissions:

"permissions": ["storage"],

التعرُّف على معلومات عن local.storage.set() وlocal.storage.get()

لحفظ عناصر المهام واستردادها، يجب الاطّلاع على طريقتَي set() وget() واجهة برمجة تطبيقات chrome.storage.

تقبل الطريقة 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);
});

إذا أردت get() كل المحتوى المتوفّر حاليًا في chrome.storage.local، احذف الأول. :

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

على عكس localStorage، لن تتمكن من فحص العناصر المخزّنة محليًا باستخدام "أدوات مطوري البرامج". لوحة الموارد. ومع ذلك، يمكنك التفاعل مع chrome.storage من وحدة تحكم JavaScript مثل لذلك:

استخدام وحدة التحكّم لتصحيح أخطاء 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 يشير إلى this النموذج الأوّلي "Store" (يمكن العثور على مزيد من المعلومات حول الدوال المرتبطة في مستندات MDN: 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 الأحادية بأكملها في كل مرة أي تحديثات مجمَّعة على أكثر من عنصر بنود قائمة المهام، مثل "وضع علامة على جميع المهام كمكتملة"، إلى خطر البيانات القراءة بعد الكتابة: لم تحدث هذه المشكلة إذا كنا نستخدم مساحة تخزين أكثر ملاءمة للبيانات، مثل 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().

فتح تطبيق Todo المكتمل

لقد انتهيت من الخطوة 2. أعِد تحميل تطبيقك من المفترض أن يكون لديك الآن إصدار Chrome مجمّع يعمل بكامل طاقته. TodoMVC.

لمزيد من المعلومات

لمزيد من المعلومات التفصيلية حول بعض واجهات برمجة التطبيقات التي تم تقديمها في هذه الخطوة، راجع:

هل أنت مستعد للانتقال إلى الخطوة التالية؟ يُرجى الانتقال إلى الخطوة 3 - إضافة المنبّهات والإشعارات »