ในขั้นตอนนี้ คุณจะได้เรียนรู้สิ่งต่อไปนี้
- วิธีปรับเว็บแอปพลิเคชันที่มีอยู่ให้เหมาะกับแพลตฟอร์มแอป Chrome
- วิธีทำให้สคริปต์แอปเป็นไปตามนโยบายรักษาความปลอดภัยเนื้อหา (CSP)
- วิธีนำพื้นที่เก็บข้อมูลในเครื่องโดยใช้ chrome.storage.local
เวลาที่ใช้โดยประมาณในการดําเนินการขั้นตอนนี้: 20 นาที
หากต้องการดูตัวอย่างสิ่งที่คุณจะทําในขั้นตอนนี้ ให้เลื่อนลงไปที่ด้านล่างของหน้านี้ ↓
นำเข้าแอป Todo ที่มีอยู่
เริ่มต้นด้วยการนำTodoMVC ซึ่งเป็นแอปการเปรียบเทียบทั่วไปในเวอร์ชัน JavaScript พื้นฐานมาไว้ในโปรเจ็กต์
เราได้รวมแอป TodoMVC เวอร์ชันหนึ่งไว้ในไฟล์ ZIP ของโค้ดอ้างอิงในโฟลเดอร์ todomvc คัดลอกไฟล์ทั้งหมด (รวมถึงโฟลเดอร์) จาก todomvc ไปยังโฟลเดอร์โปรเจ็กต์
ระบบจะขอให้คุณแทนที่ index.html โปรดยอมรับ
ตอนนี้คุณควรมีโครงสร้างไฟล์ต่อไปนี้ในโฟลเดอร์แอปพลิเคชันของคุณ
ไฟล์ที่ไฮไลต์ด้วยสีน้ำเงินมาจากโฟลเดอร์ todomvc
โหลดแอปซ้ำเลย (คลิกขวา > โหลดแอปซ้ำ) คุณควรเห็น UI พื้นฐาน แต่เพิ่มสิ่งที่ต้องทำไม่ได้
ทําให้สคริปต์เป็นไปตามนโยบายรักษาความปลอดภัยเนื้อหา (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 มี API ที่เทียบเท่าซึ่งจัดเก็บออบเจ็กต์แบบไม่พร้อมกันได้ เพื่อช่วยหลีกเลี่ยงบางครั้งที่มีต้นทุนสูงออบเจ็กต์ ->string-> กระบวนการเรียงลำดับออบเจ็กต์
หากต้องการแก้ไขข้อความแสดงข้อผิดพลาดในแอป คุณต้องแปลง localStorage
เป็น chrome.storage.local
อัปเดตสิทธิ์ของแอป
หากต้องการใช้ chrome.storage.local
คุณต้องขอสิทธิ์ storage
ใน manifest.json ให้เพิ่ม "storage"
ลงในอาร์เรย์ permissions
ดังนี้
"permissions": ["storage"],
ดูข้อมูลเกี่ยวกับ local.storage.set() และ local.storage.get()
หากต้องการบันทึกและเรียกข้อมูลรายการสิ่งที่ต้องทํา คุณต้องทราบเกี่ยวกับเมธอด set()
และ get()
ของ chrome.storage
API
เมธอด set() จะยอมรับออบเจ็กต์ของคู่คีย์-ค่าเป็นพารามิเตอร์แรก ฟังก์ชัน Callback ที่ไม่บังคับคือพารามิเตอร์ที่สอง เช่น
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
เมธอด get() ยอมรับพารามิเตอร์แรกที่ไม่บังคับสำหรับคีย์ของที่เก็บข้อมูลที่ต้องการดึงข้อมูล คุณสามารถส่งคีย์เดียวเป็นสตริง และจัดเรียงคีย์หลายรายการเป็นอาร์เรย์สตริงหรือออบเจ็กต์พจนานุกรมได้
พารามิเตอร์ที่ 2 ซึ่งต้องระบุคือฟังก์ชัน Callback ในออบเจ็กต์ที่แสดงผล ให้ใช้คีย์ที่ขอในพารามิเตอร์แรกเพื่อเข้าถึงค่าที่เก็บไว้ เช่น
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
ที่แสดงผลในฟังก์ชัน Callback แทนการเข้าถึงlocalStorage[myStorageName]
โดยตรงvar todos = JSON.parse(localStorage[dbName]).todos;
ปะทะกับ
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
ฟังก์ชัน
.bind(this)
จะใช้ใน Callback ทั้งหมดเพื่อให้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 ด้วยรายการ 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()
เมื่ออ่านข้อมูล Todo จากโมเดล ผลลัพธ์ที่แสดงจะเปลี่ยนไปตามการกรองตาม "ทั้งหมด" "ใช้งานอยู่" หรือ "เสร็จสมบูรณ์"
วิธีแปลง 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. findAll()
ได้รับสิ่งที่ต้องทำทั้งหมดจากโมเดลนี้ เช่นเดียวกับ find()
แปลง 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 รายการ (get และ set) ที่ดำเนินการกับพื้นที่เก็บข้อมูล JSON แบบโมโนลิธิคทั้งหมดทุกครั้ง การอัปเดตแบบกลุ่มในรายการสิ่งที่ต้องทำมากกว่า 1 รายการ เช่น "ทำเครื่องหมายสิ่งที่ต้องทำทั้งหมดว่าเสร็จแล้ว" จะทำให้เกิดอันตรายของข้อมูลที่เรียกว่า Read-After-Write ปัญหานี้จะไม่เกิดขึ้นหากเราใช้พื้นที่เก็บข้อมูลที่เหมาะสมกว่า เช่น 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()
เพียงครั้งเดียวด้วยอาร์เรย์ของ Todo แทนการทำเครื่องหมาย 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();
};
วางรายการสิ่งที่ต้องทำทั้งหมด
มีอีก 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 - เพิ่มการปลุกและการแจ้งเตือน »