ในขั้นตอนนี้ คุณจะได้เรียนรู้เกี่ยวกับสิ่งต่อไปนี้
- วิธีปรับเว็บแอปพลิเคชันที่มีอยู่สำหรับแพลตฟอร์มแอป Chrome
- วิธีทำให้สคริปต์แอปเป็นไปตามนโยบายรักษาความปลอดภัยเนื้อหา (CSP)
- วิธีนำพื้นที่เก็บข้อมูลในเครื่องโดยใช้ chrome.storage.local
เวลาโดยประมาณในการทำขั้นตอนนี้คือ 20 นาที
หากต้องการดูตัวอย่างสิ่งที่คุณจะทำในขั้นตอนนี้ ให้เลื่อนลงไปที่ด้านล่างของหน้านี้ ↓
นำเข้าแอป Todo ที่มีอยู่
เริ่มต้นด้วยการนำเข้า เวอร์ชัน JavaScript วานิลลา ของ TodoMVC ซึ่งเป็นเกณฑ์มาตรฐานทั่วไป ลงในโปรเจ็กต์ของคุณ
เราได้รวมเวอร์ชันของแอป TodoMVC ไว้ในรหัสไปรษณีย์ของไฟล์อ้างอิงใน 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()
ของเมธอด
API chrome.storage
เมธอด set() จะยอมรับออบเจ็กต์ของคู่คีย์-ค่าเป็นพารามิเตอร์แรก ตัวเลือกเพิ่มเติม ฟังก์ชัน Callback คือพารามิเตอร์ที่ 2 เช่น
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
เมธอด get() จะยอมรับพารามิเตอร์แรกที่ไม่บังคับสำหรับคีย์พื้นที่เก็บข้อมูลที่คุณต้องการ Retreive คีย์เดียวส่งผ่านเป็นสตริงได้ คีย์หลายอันสามารถจัดเรียงเป็นอาร์เรย์ หรือออบเจ็กต์พจนานุกรม
พารามิเตอร์ที่ 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()
พร้อมด้วย Callback ที่ไม่บังคับ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
ในฟังก์ชัน Callbackvar 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. 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 รายการ (ตั้งค่าและตั้งค่า)
ที่ทำงานบนพื้นที่เก็บข้อมูล JSON แบบโมโนลิธทั้งหมดทุกครั้ง การอัปเดตเป็นกลุ่มสำหรับมากกว่า 1 รายการ
รายการสิ่งที่ต้องทำ เช่น "ทำเครื่องหมายสิ่งที่ต้องทำทั้งหมดว่าเสร็จแล้ว" จะทำให้เกิดอันตรายของข้อมูลที่เรียกว่า
Read-After-Write ปัญหานี้จะไม่เกิดขึ้น ถ้าเราใช้ที่เก็บข้อมูลที่เหมาะสมมากกว่า
เช่น 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()
เพียงครั้งเดียวพร้อมอาร์เรย์
แทนการทำเครื่องหมายสิ่งที่ต้องทำว่าเสร็จแล้วทีละรายการ ลบการโทรถึง _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 เรียบร้อยแล้ว โหลดแอปของคุณซ้ำ ตอนนี้คุณน่าจะมีเวอร์ชันแพ็กเกจของ Chrome ที่ทำงานได้อย่างเต็มรูปแบบแล้ว ของ TodoMVC
สำหรับข้อมูลเพิ่มเติม
หากต้องการข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับ API บางรายการที่นำมาใช้ในขั้นตอนนี้ โปรดดูที่
- นโยบายรักษาความปลอดภัยเนื้อหา ↑
- ประกาศสิทธิ์ ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
พร้อมก้าวต่อไปหรือยัง ไปที่ขั้นตอนที่ 3 - เพิ่มการปลุกและการแจ้งเตือน »