שלב 2: יבוא של יישום אינטרנט קיים

בשלב הזה תלמדו:

  • איך להתאים אפליקציית אינטרנט קיימת לפלטפורמת אפליקציות Chrome.
  • איך לוודא שהסקריפטים של האפליקציה תואמים ל-Content Security Policy (CSP)
  • איך מטמיעים אחסון מקומי באמצעות chrome.storage.local.

זמן משוער להשלמת השלב הזה: 20 דקות.
כדי לראות תצוגה מקדימה של מה שתשלימו בשלב הזה, דלגו לתחתית הדף הזה ↓.

ייבוא אפליקציית Todo קיימת

בתור נקודת התחלה, עליך לייבא את גרסת ה-JavaScript וניל של TodoMVC, שהיא נקודת השוואה נפוצה באפליקציה שלכם לפרויקט.

כללנו גרסה של האפליקציה TodoMVC במיקוד של קוד ההפניה שב-todomvc . מעתיקים את כל הקבצים (כולל תיקיות) מ-todomvc לתיקיית הפרויקט.

העתקה של התיקייה todomvc לתיקיית Codelab

תתבקשו להחליף את index.html. עכשיו צריך לאשר.

החלפת index.html

עכשיו אמור להיות בתיקיית האפליקציות מבנה הקבצים הבא:

תיקיית פרויקט חדשה

הקבצים שמודגשים בכחול הם מהתיקייה todomvc.

טוענים מחדש את האפליקציה עכשיו (לוחצים לחיצה ימנית > טוענים מחדש את האפליקציה). אתם אמורים לראות את ממשק המשתמש הבסיסי, אבל לא לראות אותו. להוסיף משימות לביצוע.

תאימות של סקריפטים למדיניות אבטחת התוכן (CSP)

פותחים את מסוף כלי הפיתוח (לוחצים לחיצה ימנית > בדיקת הרכיב ולאחר מכן בוחרים בכרטיסייה מסוף). שלך תוצג שגיאה לגבי סירוב להפעלה של סקריפט מוטבע:

אפליקציית משימות לביצוע עם שגיאה ביומן של מסוף CSP

כדי לתקן את השגיאה הזו, עליך לוודא שהאפליקציה תואמת ל-Content Security Policy. אחד אי-תאימות שכיחה של 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 בשם shoestrap.js. העברת הקוד הקודם שהוטמע להיות בקובץ הזה:

// Bootstrap app data
window.app = {};

אם תטענו מחדש את האפליקציה עכשיו אבל תתקרבו לאפליקציה, עדיין תהיה לכם אפליקציית Todo שלא פועלת.

המרה של LocalStorage ל-chrome.storage.local

אם פותחים את מסוף כלי הפיתוח עכשיו, השגיאה הקודמת אמורה להיעלם. יש שגיאה חדשה, עם זאת, בערך window.localStorage לא זמינים:

אפליקציית משימות לביצוע עם שגיאת יומן של מסוף 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() מקבלת אובייקט של צמדי מפתח-ערך כפרמטר הראשון. שדה אופציונלי פונקציית קריאה חוזרת היא הפרמטר השני. לדוגמה:

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

ה-method get() מקבלת פרמטר אופציונלי ראשון למפתחות של מאגר הנתונים שרוצים אחזור. ניתן להעביר מפתח יחיד כמחרוזת; אפשר לסדר כמה מפתחות במערך של מחרוזות או אובייקט מילון.

הפרמטר השני, הנדרש, הוא פונקציית קריאה חוזרת. באובייקט שמוחזר, משתמשים ב מפתחות שהתבקשו בפרמטר הראשון כדי לגשת לערכים המאוחסנים. לדוגמה:

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
    });
    
  • במקום לגשת ישירות אל localStorage[myStorageName], צריך להשתמש ב chrome.storage.local.get(myStorageName,function(storage){...}), ואז לנתח את הנתונים שהוחזרו אובייקט storage בפונקציית הקריאה החוזרת.

    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. שיטת ה-constructor של Store דואגת לאתחול אפליקציית Todo עם כל פריטים שצריך לבצע ממאגר הנתונים. השיטה קודם בודקת אם מאגר הנתונים קיים. אם לא תהיה התאמה, ליצור מערך ריק של todos ולשמור אותו במאגר הנתונים כדי שלא יהיו שגיאות קריאה בסביבת זמן הריצה.

ב-js/store.js, ממירים את השימוש ב-localStorage ב-method של constructor כדי להשתמש במקום זאת 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() מציגה אתגר. הפעולה תלויה בשתי פעולות אסינכרוניות (get and set) שפועלים על כל אחסון JSON המונוליתי בכל פעם. כל עדכון באצווה בכמה גרסאות פריט משימות לביצוע, כמו "סמן את כל המשימות כמשימות שהושלמו", יגרום לסכנת נתונים שנקראת קריאה אחרי כתיבה הבעיה הזו לא הייתה מתרחשת אם אנחנו משתמשים באחסון נתונים מתאים יותר, כמו IndexedDB, אבל אנחנו מנסים לצמצם את מאמץ ההמרה ב-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() כי תתבצע כוונון של 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:

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();
};

הסרת כל הפריטים לביצוע משימות

יש עוד שיטה אחת ב-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 - הוספת התראות והתראות »