مرحله 2: یک برنامه وب موجود را وارد کنید

در این مرحله یاد خواهید گرفت:

  • نحوه تطبیق یک برنامه وب موجود برای پلتفرم Chrome Apps.
  • چگونه اسکریپت های برنامه خود را با خط مشی امنیت محتوا (CSP) مطابقت دهیم.
  • نحوه پیاده سازی حافظه محلی با استفاده از chrome.storage.local .

زمان تخمینی برای تکمیل این مرحله: 20 دقیقه.
برای پیش نمایش آنچه در این مرحله تکمیل می کنید، به پایین این صفحه بروید ↓ .

یک برنامه Todo موجود را وارد کنید

به عنوان نقطه شروع، نسخه وانیلی جاوا اسکریپت TodoMVC ، یک برنامه معیار رایج، را به پروژه خود وارد کنید.

ما نسخه ای از برنامه TodoMVC را در کد مرجع در پوشه todomvc قرار داده ایم. تمام فایل ها (از جمله پوشه ها) را از todomvc در پوشه پروژه خود کپی کنید.

پوشه todomvc را در پوشه codelab کپی کنید

از شما خواسته می شود که index.html را جایگزین کنید. برو و قبول کن

index.html را جایگزین کنید

اکنون باید ساختار فایل زیر را در پوشه برنامه خود داشته باشید:

پوشه پروژه جدید

فایل هایی که با رنگ آبی مشخص شده اند از پوشه todomvc هستند.

اکنون برنامه خود را مجدداً بارگیری کنید ( کلیک راست کنید > Reload App ). شما باید رابط کاربری اصلی را ببینید اما نمی‌توانید کارها را اضافه کنید.

اسکریپت ها را با خط مشی امنیت محتوای (CSP) مطابقت دهید

DevTools Console را باز کنید ( راست کلیک کنید > Inspect Element ، سپس تب Console را انتخاب کنید). شما یک خطا در مورد امتناع از اجرای یک اسکریپت درون خطی خواهید دید:

برنامه Todo با خطای گزارش کنسول CSP

بیایید این خطا را با مطابقت با سیاست امنیتی محتوای برنامه برطرف کنیم. یکی از متداول‌ترین موارد عدم انطباق CSP توسط جاوا اسکریپت داخلی ایجاد می‌شود. نمونه هایی از جاوا اسکریپت درون خطی شامل کنترل کننده های رویداد به عنوان ویژگی های DOM (به عنوان مثال <button onclick=''> ) و تگ های <script> با محتوای داخل HTML است.

راه حل ساده است: محتوای درون خطی را به یک فایل جدید منتقل کنید.

1. نزدیک به انتهای index.html ، جاوا اسکریپت درون خطی را حذف کنید و به جای آن 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 تبدیل کنید

اگر اکنون DevTools Console را باز کنید، خطای قبلی باید برطرف شود. با این حال، یک خطای جدید در مورد در دسترس نبودن window.localStorage وجود دارد:

برنامه Todo با خطای گزارش کنسول localStorage

برنامه‌های Chrome localStorage پشتیبانی نمی‌کنند زیرا localStorage همزمان است. دسترسی همزمان به منابع مسدود کننده (I/O) در زمان اجرا تک رشته ای می تواند برنامه شما را پاسخگو نباشد.

Chrome Apps یک API معادل دارد که می تواند اشیاء را به صورت ناهمزمان ذخیره کند. این کمک می کند تا از فرآیند سریال سازی شی گاه پرهزینه جلوگیری شود.

برای رسیدگی به پیام خطا در برنامه ما، باید localStorage به chrome.storage.local تبدیل کنید.

مجوزهای برنامه را به روز کنید

برای استفاده از chrome.storage.local ، باید مجوز storage را درخواست کنید. در manifest.json ، "storage" به آرایه permissions اضافه کنید:

"permissions": ["storage"],

درباره local.storage.set() و local.storage.get() بیاموزید

برای ذخیره و بازیابی موارد todo، باید در مورد متدهای 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);
});

اگر می خواهید همه چیزهایی را که در حال حاضر در chrome.storage.local هستند get() ، اولین پارامتر را حذف کنید:

chrome.storage.local.get(function(data) {
  console.log(data);
});

برخلاف localStorage ، نمی‌توانید موارد ذخیره شده محلی را با استفاده از پنل DevTools Resources بررسی کنید. با این حال، می‌توانید با chrome.storage از کنسول جاوا اسکریپت تعامل داشته باشید:

از کنسول برای رفع اشکال 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 برگشتی را در تابع 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 را برای بازیابی موارد 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() همه کارها را از Model دریافت می کند. تبدیل 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() جاری چالشی را ارائه می دهد. این به دو عملیات همگام (به دست آوردن و تنظیم) بستگی دارد که هر بار در کل فضای ذخیره سازی JSON یکپارچه عمل می کنند. هر گونه به روز رسانی دسته ای در بیش از یک مورد انجام کار، مانند "علامت گذاری همه کارها به عنوان تکمیل شده"، منجر به خطر داده ای به نام Read-After-Write می شود. اگر از ذخیره‌سازی داده‌های مناسب‌تری مانند IndexedDB استفاده می‌کردیم، این مشکل رخ نمی‌داد، اما سعی می‌کنیم تلاش‌های تبدیل را برای این Codelab به حداقل برسانیم.

راه‌های مختلفی برای رفع آن وجود دارد، بنابراین از این فرصت استفاده می‌کنیم تا با استفاده از آرایه‌ای از شناسه‌های todo که به‌صورت هم‌زمان به‌روزرسانی می‌شوند، کمی 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));
};

موارد انجام کار را به عنوان کامل علامت گذاری کنید

اکنون که برنامه روی آرایه‌ها کار می‌کند، باید نحوه مدیریت برنامه با کاربر با کلیک روی دکمه پاک کردن تکمیل شده (#) را تغییر دهید:

1. در controller.js ، به‌روزرسانی toggleAll() برای فراخوانی toggleComplete() تنها یک بار با آرایه‌ای از todos به جای علامت‌گذاری یک todo به عنوان تکمیل‌شده یکی یکی. همچنین تماس با _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 را تمام کردید! برنامه خود را مجدداً بارگیری کنید و اکنون باید یک نسخه بسته بندی شده کروم کاملاً کارآمد از TodoMVC داشته باشید.

برای اطلاعات بیشتر

برای اطلاعات دقیق تر در مورد برخی از API های معرفی شده در این مرحله، به آدرس زیر مراجعه کنید:

برای ادامه به مرحله بعدی آماده هستید؟ به مرحله 3 بروید - هشدارها و اعلان‌ها را اضافه کنید »