در این مرحله یاد خواهید گرفت:
- نحوه تطبیق یک برنامه وب موجود برای پلتفرم Chrome Apps.
- چگونه اسکریپت های برنامه خود را با خط مشی امنیت محتوا (CSP) مطابقت دهیم.
- نحوه پیاده سازی حافظه محلی با استفاده از chrome.storage.local .
زمان تخمینی برای تکمیل این مرحله: 20 دقیقه.
برای پیش نمایش آنچه در این مرحله تکمیل می کنید، به پایین این صفحه بروید ↓ .
یک برنامه Todo موجود را وارد کنید
به عنوان نقطه شروع، نسخه وانیلی جاوا اسکریپت TodoMVC ، یک برنامه معیار رایج، را به پروژه خود وارد کنید.
ما نسخه ای از برنامه TodoMVC را در کد مرجع در پوشه todomvc قرار داده ایم. تمام فایل ها (از جمله پوشه ها) را از todomvc در پوشه پروژه خود کپی کنید.
از شما خواسته می شود که index.html را جایگزین کنید. برو و قبول کن
اکنون باید ساختار فایل زیر را در پوشه برنامه خود داشته باشید:
فایل هایی که با رنگ آبی مشخص شده اند از پوشه todomvc هستند.
اکنون برنامه خود را مجدداً بارگیری کنید ( کلیک راست کنید > Reload App ). شما باید رابط کاربری اصلی را ببینید اما نمیتوانید کارها را اضافه کنید.
اسکریپت ها را با خط مشی امنیت محتوای (CSP) مطابقت دهید
DevTools Console را باز کنید ( راست کلیک کنید > Inspect Element ، سپس تب Console را انتخاب کنید). شما یک خطا در مورد امتناع از اجرای یک اسکریپت درون خطی خواهید دید:
بیایید این خطا را با مطابقت با سیاست امنیتی محتوای برنامه برطرف کنیم. یکی از متداولترین موارد عدم انطباق CSP توسط جاوا اسکریپت داخلی ایجاد میشود. نمونه هایی از جاوا اسکریپت درون خطی شامل کنترل کننده های رویداد به عنوان ویژگی های DOM (به عنوان مثال <button onclick=''>
) و تگ های <script>
با محتوای داخل HTML است.
راه حل ساده است: محتوای درون خطی را به یک فایل جدید منتقل کنید.
1. نزدیک به انتهای index.html ، جاوا اسکریپت درون خطی را حذف کنید و به جای آن 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 Console را باز کنید، خطای قبلی باید برطرف شود. با این حال، یک خطای جدید در مورد در دسترس نبودن window.localStorage
وجود دارد:
برنامههای Chrome localStorage
پشتیبانی نمیکنند زیرا localStorage
همزمان است. دسترسی همزمان به منابع مسدود کننده (I/O) در زمان اجرا تک رشته ای می تواند برنامه شما را پاسخگو نباشد.
Chrome Apps یک API معادل دارد که می تواند اشیاء را به صورت ناهمزمان ذخیره کند. این کمک می کند تا از فرآیند سریال سازی شی گاه پرهزینه جلوگیری شود.
برای رسیدگی به پیام خطا در برنامه ما، باید localStorage
به chrome.storage.local تبدیل کنید.
مجوزهای برنامه را به روز کنید
برای استفاده از chrome.storage.local
، باید مجوز storage
را درخواست کنید. در manifest.json ، "storage"
به آرایه permissions
اضافه کنید:
"permissions": ["storage"],
درباره local.storage.set() و local.storage.get() بیاموزید
برای ذخیره و بازیابی موارد todo، باید در مورد متدهای set()
و get()
chrome.storage
API بدانید.
متد 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 Resources بررسی کنید. با این حال، میتوانید با chrome.storage
از کنسول جاوا اسکریپت تعامل داشته باشید:
پیش نمایش تغییرات API مورد نیاز
بسیاری از مراحل باقی مانده در تبدیل برنامه Todo، تغییرات کوچک در تماس های API است. تغییر همه مکانهایی که در حال حاضر از 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
برگشتی را در تابع callback تجزیه کنید.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 را برای بازیابی موارد 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()
همه کارها را از Model دریافت می کند. تبدیل 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 استفاده میکردیم، این مشکل رخ نمیداد، اما سعی میکنیم تلاشهای تبدیل را برای این Codelab به حداقل برسانیم.
راههای مختلفی برای رفع آن وجود دارد، بنابراین از این فرصت استفاده میکنیم تا با استفاده از آرایهای از شناسههای todo که بهصورت همزمان بهروزرسانی میشوند، کمی save()
تغییر دهیم:
1. برای شروع، همه چیز را که قبلاً در save()
قرار دارد با یک callback 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()
تنها یک بار با آرایهای از todos به جای علامتگذاری یک todo به عنوان تکمیلشده یکی یکی. همچنین تماس با _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 را تمام کردید! برنامه خود را مجدداً بارگیری کنید و اکنون باید یک نسخه بسته بندی شده کروم کاملاً کارآمد از TodoMVC داشته باشید.
برای اطلاعات بیشتر
برای اطلاعات دقیق تر در مورد برخی از API های معرفی شده در این مرحله، به آدرس زیر مراجعه کنید:
- خط مشی امنیت محتوا ↑
- اعلام مجوز ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
برای ادامه به مرحله بعدی آماده هستید؟ به مرحله 3 بروید - هشدارها و اعلانها را اضافه کنید »