في هذه الخطوة، ستتعرف على:
- كيفية تكييف تطبيق ويب حالي مع النظام الأساسي لتطبيقات Chrome.
- كيفية جعل نصوص تطبيقك البرمجية تمتثل لسياسة أمان المحتوى (CSP)
- كيفية تنفيذ ميزة التخزين المحلي باستخدام chrome.storage.local
الوقت المقدّر لإنهاء هذه الخطوة: 20 دقيقة
لمعاينة ما ستكمله في هذه الخطوة، انتقِل إلى أسفل هذه الصفحة ↓.
استيراد تطبيق مهام حالٍ
كنقطة انطلاق، عليك استيراد إصدار vanilla JavaScript من TodoMVC، وهو تطبيق قياس أداء شائع، إلى مشروعك.
لقد أدرجنا إصدارًا من تطبيق TodoMVC في ملف zip للرمز المرجعي في مجلد 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();
ضع هذه الاختلافات الرئيسية في الاعتبار عندما نتناول استرداد عناصر المهام وحفظها وإزالتها في الأقسام التالية.
استرداد عناصر المهام
لنعدِّل تطبيق "مهام Google" لاسترداد عناصر المهام:
1. تهتم طريقة الإنشاء Store
بإعداد تطبيق Todo باستخدام كل ملفّات tasks الحالية
من قاعدة البيانات. تتحقّق الطريقة أولاً مما إذا كان ملف تخزين البيانات متوفّرًا. وإذا لم يكن الأمر كذلك، سيتم
إنشاء صفيف فارغ من 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()
callback:
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 المكتمل
لقد انتهيت من الخطوة 2. أعِد تحميل تطبيقك ومن المفترض أن يكون لديك الآن إصدار Chrome مجمّع من TodoMVC.
لمزيد من المعلومات
لمزيد من المعلومات التفصيلية حول بعض واجهات برمجة التطبيقات التي تم تقديمها في هذه الخطوة، راجع:
- سياسة أمان المحتوى ↑
- الإفصاح عن الأذونات ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
هل أنت مستعد للمتابعة إلى الخطوة التالية؟ انتقِل إلى الخطوة 3: إضافة تنبيهات وإشعارات ».