ในขั้นตอนนี้ คุณจะได้เรียนรู้เกี่ยวกับสิ่งต่อไปนี้
- วิธีปรับเว็บแอปพลิเคชันที่มีอยู่สำหรับแพลตฟอร์มแอป Chrome
- วิธีทำให้สคริปต์แอปสอดคล้องกับนโยบายรักษาความปลอดภัยเนื้อหา (CSP)
- วิธีใช้พื้นที่เก็บข้อมูลในเครื่องโดยใช้ chrome.storage.local
เวลาโดยประมาณในการทำขั้นตอนนี้ให้เสร็จสิ้นคือ 20 นาที
หากต้องการดูตัวอย่างขั้นตอนที่ต้องทำในขั้นตอนนี้ ให้เลื่อนลงไปที่ด้านล่างของหน้านี้ ↓
นำเข้าแอป Todo ที่มีอยู่
เริ่มด้วยการนำเข้า TodoMVC เวอร์ชันวานิลลา JavaScript ซึ่งเป็นแอปเปรียบเทียบทั่วไปไปยังโปรเจ็กต์ของคุณ
เราได้รวมแอป TodoMVC เวอร์ชันหนึ่งไว้ในรหัสไปรษณีย์ของรหัสไปรษณีย์ในโฟลเดอร์ todomvc คัดลอกไฟล์ทั้งหมด (รวมถึงโฟลเดอร์) จาก todomvc ลงในโฟลเดอร์โครงการ
ระบบจะขอให้คุณแทนที่ index.html เริ่มยอมรับเลย
ตอนนี้คุณควรมีโครงสร้างไฟล์ต่อไปนี้ในโฟลเดอร์แอปพลิเคชันแล้ว
ไฟล์ที่ไฮไลต์เป็นสีน้ำเงินมาจากโฟลเดอร์ todomvc
โหลดแอปซ้ำเลย (คลิกขวา > โหลดแอปซ้ำ) คุณควรจะเห็น UI พื้นฐาน แต่คุณจะไม่สามารถ เพิ่มสิ่งที่ต้องทำได้
ทำให้สคริปต์เป็นไปตามนโยบายความปลอดภัยของเนื้อหา (CSP)
เปิดคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บ (คลิกขวา > ตรวจสอบองค์ประกอบ แล้วเลือกแท็บคอนโซล) คุณจะเห็นข้อผิดพลาดเกี่ยวกับการปฏิเสธที่จะเรียกใช้สคริปต์ในหน้า
เรามาแก้ไขข้อผิดพลาดนี้โดยทำให้แอปเป็นไปตามนโยบายรักษาความปลอดภัยเนื้อหากัน การไม่ปฏิบัติตามข้อกำหนดของ CSP ที่พบบ่อยที่สุดอย่างหนึ่งคือ JavaScript ในหน้า ตัวอย่างของ JavaScript ในหน้าประกอบด้วยเครื่องจัดการเหตุการณ์เป็นแอตทริบิวต์ DOM (เช่น <button onclick=''>
) และแท็ก <script>
ที่มีเนื้อหาภายใน HTML
การแก้ปัญหานี้ทำได้ง่ายๆ คือย้ายเนื้อหาในหน้าไปยังไฟล์ใหม่
1. บริเวณด้านล่างของ index.html ให้นำ JavaScript ในบรรทัดออกและใส่ js/Boottrap.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 โดยใช้ชื่อว่า Botstrap.js ย้ายโค้ดในหน้าก่อนหน้านี้ ไปไว้ในไฟล์นี้
// Bootstrap app data
window.app = {};
หากโหลดแอปซ้ำตอนนี้ คุณจะยังมีแอป Todo ที่ใช้งานไม่ได้
แปลง localStorage เป็น chrome.storage.local
หากเปิดคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บตอนนี้ ข้อผิดพลาดก่อนหน้านี้จะหายไป มีข้อผิดพลาดใหม่เกิดขึ้น แต่ประมาณ window.localStorage
ไม่พร้อมใช้งาน
แอป Chrome ไม่รองรับ localStorage
เนื่องจาก localStorage
เป็นแบบซิงโครนัส การเข้าถึงการบล็อกทรัพยากร (I/O) แบบพร้อมกันในรันไทม์แบบชุดข้อความเดียวอาจทำให้แอปไม่ตอบสนอง
แอป Chrome มี API ที่เทียบเท่าซึ่งสามารถจัดเก็บออบเจ็กต์แบบไม่พร้อมกัน วิธีนี้จะช่วยหลีกเลี่ยงกระบวนการทำให้ออบเจ็กต์เป็นออบเจ็กต์->สตริง->มีค่าใช้จ่ายสูงในบางครั้ง
หากต้องการแก้ไขข้อความแสดงข้อผิดพลาดในแอป คุณต้องแปลง localStorage
เป็น chrome.storage.local
อัปเดตสิทธิ์ของแอป
หากต้องการใช้ chrome.storage.local
คุณต้องขอสิทธิ์ storage
ใน manifest.json ให้เพิ่ม "storage"
ลงในอาร์เรย์ permissions
ดังนี้
"permissions": ["storage"],
เรียนรู้เกี่ยวกับ local.storage.set() และ local.storage.get()
หากต้องการบันทึกและเรียกข้อมูลรายการสิ่งที่ต้องทำ คุณต้องทราบวิธี set()
และ get()
ของ API ของ chrome.storage
เมธอด set() จะยอมรับออบเจ็กต์ของคู่คีย์-ค่าเป็นพารามิเตอร์แรก ฟังก์ชันเรียกกลับที่ไม่บังคับคือพารามิเตอร์ที่ 2 เช่น
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
เมธอด get() จะยอมรับพารามิเตอร์แรกที่ไม่บังคับสำหรับคีย์พื้นที่เก็บข้อมูลที่คุณต้องการดึงข้อมูล คีย์เดียวจะส่งเป็นสตริงได้ หรือจะจัดคีย์หลายคีย์เป็นอาร์เรย์ของสตริงหรือออบเจ็กต์พจนานุกรมก็ได้
พารามิเตอร์ที่ 2 ซึ่งจำเป็นต้องมีคือฟังก์ชันเรียกกลับ ในออบเจ็กต์ที่แสดงผล ให้ใช้คีย์ที่ขอในพารามิเตอร์แรกเพื่อเข้าถึงค่าที่จัดเก็บไว้ เช่น
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 เช่น
แสดงตัวอย่างการเปลี่ยนแปลง 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 });
คุณต้องใช้
chrome.storage.local.get(myStorageName,function(storage){...})
แล้วแยกวิเคราะห์ออบเจ็กต์storage
ที่แสดงผลในฟังก์ชันเรียกกลับแทนการเข้าถึงlocalStorage[myStorageName]
โดยตรง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()
ปัจจุบันมีความท้าทาย ขึ้นอยู่กับการทำงานแบบไม่พร้อมกัน 2 รายการ (รับและตั้งค่า) ซึ่งทำงานกับพื้นที่เก็บข้อมูล JSON แบบโมโนลิธทั้งหมดทุกครั้ง การอัปเดตแบบกลุ่มในรายการสิ่งที่ต้องทำมากกว่า 1 รายการ เช่น "ทำเครื่องหมายสิ่งที่ต้องทำทั้งหมดเป็นเสร็จสมบูรณ์แล้ว" จะส่งผลให้เกิดอันตรายของข้อมูลที่เรียกว่าอ่านหลังเขียน ปัญหานี้จะไม่เกิดขึ้นหากเราใช้พื้นที่เก็บข้อมูลที่เหมาะสมมากกว่า เช่น IndexedDB แต่เราพยายามลดความพยายาม Conversion สำหรับ Codelab นี้
ซึ่งมีวิธีแก้ไขหลายวิธี โดยเราจะใช้โอกาสนี้ในการพลิกโฉม 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()
เพียงครั้งเดียวด้วยอาร์เรย์ของรายการสิ่งที่ต้องทำแทนการทำเครื่องหมาย todo ว่าเสร็จแล้วทีละรายการ ลบการโทรไปยัง _filter()
ด้วยเนื่องจากคุณจะปรับเปลี่ยน _filter()
ของ toggleComplete
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();
};
วางรายการสิ่งที่ต้องทำทั้งหมด
ยังมีอีก 1 เมธอดใน 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 เวอร์ชันแพ็กเกจของ Chrome ที่ทำงานได้อย่างสมบูรณ์แล้ว
สำหรับข้อมูลเพิ่มเติม
สำหรับข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับ API บางส่วนที่นำมาใช้ในขั้นตอนนี้ โปรดดูที่:
- นโยบายรักษาความปลอดภัยเนื้อหา ↑
- ประกาศสิทธิ์ ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
หากพร้อมที่จะไปยังขั้นตอนถัดไปแล้ว ไปที่ขั้นตอนที่ 3 - เพิ่มการปลุกและการแจ้งเตือน »