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

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

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

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

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

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

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

نسخ مجلد todomvc إلى مجلد Codelab

سيُطلب منك استبدال 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:

تطبيق Todo يحتوي على خطأ في سجلّ وحدة تحكّم LocalStorage

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

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

لمعالجة رسالة الخطأ في تطبيقنا، يجب تحويل 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 المكتمل

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

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

للحصول على معلومات أكثر تفصيلاً حول بعض واجهات برمجة التطبيقات التي تم تقديمها في هذه الخطوة، يُرجى الرجوع إلى:

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