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

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

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

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

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

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

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

คัดลอกโฟลเดอร์ todomvc ไปยังโฟลเดอร์ Codelab

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

แทนที่index.html

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

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

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

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

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

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

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

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

ใช้คอนโซลเพื่อแก้ไขข้อบกพร่องของ chrome.storage

ดูตัวอย่างการเปลี่ยนแปลง 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 ในฟังก์ชัน 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 เพื่อเรียกรายการสิ่งที่ต้องทำกัน:

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:

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 เรียบร้อยแล้ว โหลดแอปของคุณซ้ำ ตอนนี้คุณน่าจะมีเวอร์ชันแพ็กเกจของ Chrome ที่ทำงานได้อย่างเต็มรูปแบบแล้ว ของ TodoMVC

สำหรับข้อมูลเพิ่มเติม

หากต้องการข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับ API บางรายการที่นำมาใช้ในขั้นตอนนี้ โปรดดูที่

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