في هذه الخطوة، ستتعلم ما يلي:
- طريقة تكييف تطبيق ويب حالي ليناسب النظام الأساسي لتطبيقات Chrome
- كيفية جعل النصوص البرمجية للتطبيقات متوافقة مع سياسة أمان المحتوى (CSP) في تطبيقك
- كيفية تنفيذ ميزة التخزين المحلي باستخدام chrome.storage.local.
الوقت المقدَّر لإكمال هذه الخطوة: 20 دقيقة.
لمعاينة ما ستكمله في هذه الخطوة، انتقِل إلى أسفل هذه الصفحة ↓.
استيراد تطبيق Todo حالي
كنقطة بداية، عليك استيراد إصدار JavaScript من Vanilla من TodoMVC، وهو تطبيق قياسي شائع، إلى مشروعك.
تم تضمين إصدار من تطبيق TodoMVC في الرمز البريدي المرجعي في المجلد todomvc. انسخ جميع الملفات (بما في ذلك المجلدات) من todomvc إلى مجلد مشروعك.
سيُطلب منك استبدال index.html. استمر واقبل.
يجب أن يكون لديك الآن بنية الملفات التالية في مجلد التطبيقات:
تكون الملفات المميّزة باللون الأزرق من المجلد todomvc.
أعِد تحميل تطبيقك الآن (انقر بزر الماوس الأيمن > إعادة تحميل التطبيق). من المفترض أن تظهر لك واجهة المستخدم الأساسية ولكن لن تتمكن من إضافة المهام.
جعل النصوص البرمجية متوافقة مع سياسة أمان المحتوى (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
:
لا تتوافق تطبيقات 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 كما يلي:
معاينة التغييرات المطلوبة في واجهة برمجة التطبيقات
تُعد معظم الخطوات المتبقية في تحويل تطبيق 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:
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.
لمزيد من المعلومات
للحصول على معلومات أكثر تفصيلاً حول بعض واجهات برمجة التطبيقات التي تم تقديمها في هذه الخطوة، يُرجى الرجوع إلى:
- سياسة أمان المحتوى ↑
- تضمين الأذونات ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
هل أنت جاهز للمتابعة إلى الخطوة التالية؟ الانتقال إلى الخطوة 3: إضافة المنبّهات والإشعارات »