ขั้นตอนที่ 2: นำเข้าเว็บแอปที่มีอยู่

ในขั้นตอนนี้ คุณจะได้เรียนรู้เกี่ยวกับสิ่งต่อไปนี้

  • วิธีปรับเว็บแอปพลิเคชันที่มีอยู่สำหรับแพลตฟอร์มแอป Chrome
  • วิธีทำให้สคริปต์แอปสอดคล้องกับนโยบายรักษาความปลอดภัยเนื้อหา (CSP)
  • วิธีใช้พื้นที่เก็บข้อมูลในเครื่องโดยใช้ chrome.storage.local

เวลาโดยประมาณในการทำขั้นตอนนี้ให้เสร็จสิ้นคือ 20 นาที
หากต้องการดูตัวอย่างขั้นตอนที่ต้องทำในขั้นตอนนี้ ให้เลื่อนลงไปที่ด้านล่างของหน้านี้ ↓

นำเข้าแอป Todo ที่มีอยู่

เริ่มด้วยการนำเข้า TodoMVC เวอร์ชันวานิลลา JavaScript ซึ่งเป็นแอปเปรียบเทียบทั่วไปไปยังโปรเจ็กต์ของคุณ

เราได้รวมแอป TodoMVC เวอร์ชันหนึ่งไว้ในรหัสไปรษณีย์ของรหัสไปรษณีย์ในโฟลเดอร์ todomvc คัดลอกไฟล์ทั้งหมด (รวมถึงโฟลเดอร์) จาก todomvc ลงในโฟลเดอร์โครงการ

คัดลอกโฟลเดอร์ todomvc ลงในโฟลเดอร์ Codelab

ระบบจะขอให้คุณแทนที่ index.html เริ่มยอมรับเลย

แทนที่ index.html

ตอนนี้คุณควรมีโครงสร้างไฟล์ต่อไปนี้ในโฟลเดอร์แอปพลิเคชันแล้ว

โฟลเดอร์โปรเจ็กต์ใหม่

ไฟล์ที่ไฮไลต์เป็นสีน้ำเงินมาจากโฟลเดอร์ todomvc

โหลดแอปซ้ำเลย (คลิกขวา > โหลดแอปซ้ำ) คุณควรจะเห็น UI พื้นฐาน แต่คุณจะไม่สามารถ เพิ่มสิ่งที่ต้องทำได้

ทำให้สคริปต์เป็นไปตามนโยบายความปลอดภัยของเนื้อหา (CSP)

เปิดคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บ (คลิกขวา > ตรวจสอบองค์ประกอบ แล้วเลือกแท็บคอนโซล) คุณจะเห็นข้อผิดพลาดเกี่ยวกับการปฏิเสธที่จะเรียกใช้สคริปต์ในหน้า

แอปสิ่งที่ต้องทำที่มีข้อผิดพลาดในบันทึกคอนโซล 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 ไม่พร้อมใช้งาน

แอป Todo ที่มีข้อผิดพลาดในบันทึกคอนโซล 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 เช่น

ใช้คอนโซลเพื่อแก้ไขข้อบกพร่อง 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
    });
    
  • คุณต้องใช้ 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:

Todo app with localStorage console log error

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 บางส่วนที่นำมาใช้ในขั้นตอนนี้ โปรดดูที่:

หากพร้อมที่จะไปยังขั้นตอนถัดไปแล้ว ไปที่ขั้นตอนที่ 3 - เพิ่มการปลุกและการแจ้งเตือน »