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

בשלב זה נלמד:

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

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

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

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

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

העתקה של תיקיית todomvc לתיקיית ה-codelab

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

החלפת index.html

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

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

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

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

תאימות למדיניות Content Security Policy (CSP) של סקריפטים

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

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

כדי לתקן את השגיאה הזו, עליך להתאים את האפליקציה ל-Content Security Policy. אחת הסיבות הנפוצות ביותר לאי-תאימות של CSP נגרמה על ידי JavaScript מוטבע. דוגמאות ל-JavaScript מוטבע כוללות גורמים מטפלים באירועים כמאפייני DOM (למשל <button onclick=''>) ותגי <script> עם תוכן בתוך ה-HTML.

הפתרון פשוט: מעבירים את התוכן שמוטמע לקובץ חדש.

1. בחלק התחתון של index.html, מסירים את ה-JavaScript המוטבע, ובמקום זאת כוללים את js/shoestrap.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 לא זמין:

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

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

השיטה 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, לא תוכלו לבדוק פריטים שמאוחסנים באופן מקומי באמצעות החלונית Resources DevTools. עם זאת, אפשר לקיים אינטראקציה עם chrome.storage מתוך JavaScript Console באופן הבא:

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

ב-js/store.js, ממירים את השימוש ב-localStorage בשיטת ה-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 ו-set) שפועלות בכל אחסון ה-JSON המונוליתי בכל פעם. כל עדכון אצווה ביותר מפריט אחד לביצוע, כמו "סימון כל המשימות כמשימות שהושלמו", יוביל לסכנת נתונים שנקראת Read-After-Write. הבעיה הזו לא הייתה מתרחשת אם היינו משתמשים באחסון נתונים מתאים יותר, כמו IndexedDB, אבל אנחנו מנסים לצמצם את מאמצי ההמרה במסגרת ה-Codelab הזה.

יש כמה דרכים לתקן את הבעיה, לכן נשתמש בהזדמנות הזו כדי לבצע ארגון מחדש קל של save() על ידי הגדרת מערך של מזהי משימות לביצוע עדכון בבת אחת:

1. כדי להתחיל, צריך לעטוף את כל התוכן שכבר נמצא בתוך save() באמצעות קריאה חוזרת (callback) של 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));
};

סימון שמשימות לביצוע

עכשיו, שהאפליקציה פועלת במערכים, צריך לשנות את אופן הטיפול של האפליקציה במשתמש שילחץ על הלחצן Clearהשלמה (#):

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().

הפעלת האפליקציה הסתיימה של משימות לביצוע

סיימת את שלב 2! טוענים מחדש את האפליקציה כדי שגרסת TodoMVC תכיל חבילה מלאה של Chrome.

אפשר לקבל מידע נוסף

לקבלת מידע מפורט יותר על חלק מממשקי ה-API שהוצגו בשלב הזה, עיינו במקורות הבאים:

מוכנים להמשיך לשלב הבא? עבור אל שלב 3 – הוספת שעונים מעוררים והתראות »