检查截止日期

在本文中,我们将探讨一个复杂示例,涉及将当前时间和日期与通过 IndexedDB 存储的截止日期进行比较。这里的主要复杂之处在于将存储的截止日期信息(月、时、日等)与从Date 对象获取的当前时间和日期进行比较。

A screenshot of the sample app. A red main title saying To do app, a test to-do item, and a red form for users to enter new tasks

我们将在这篇文章中参考的主要示例应用程序是**待办事项列表通知**,这是一个简单的待办事项列表应用程序,它通过IndexedDB 存储任务标题和截止时间和日期,然后使用NotificationVibration API 在截止日期到达时向用户提供通知。您可以从 GitHub 下载待办事项列表通知应用程序 并试用源代码,或查看正在运行的应用程序

基本问题

在待办事项应用程序中,我们首先希望以既可由机器读取又可在显示时被人理解的格式记录时间和日期信息,然后检查每个时间和日期是否发生在当前时刻。基本上,我们希望检查现在的时间和日期,然后检查每个存储的事件,以查看是否有任何事件的截止日期与当前时间和日期匹配。如果匹配,我们希望通过某种通知让用户知道。

如果我们只是比较两个Date 对象,这将很容易,但当然,人们不想以 JavaScript 理解的相同格式输入截止日期信息。人可读的日期截然不同,有许多不同的表示形式。

记录日期信息

为了在移动设备上提供合理的用户体验并减少歧义,我决定创建一个带有以下内容的 HTML 表单:

The form of the to-do app, containing fields to fill in a task title, and minute, hour, day, month and year values for the deadline.

  • 一个用于输入待办事项列表标题的文本输入框。这是用户最不可避免的输入内容。
  • 用于截止日期的小时和分钟部分的数字输入框。在支持type="number" 的浏览器中,您将获得一个不错的上下箭头数字选择器。在移动平台上,您往往会获得一个数字键盘来输入数据,这很有帮助。在其他平台上,您只会获得一个标准的文本输入框,这也可以接受。
  • <select> 元素用于输入截止日期的日、月和年。因为这些值对于用户输入来说是最含糊的(7、星期天、周日? 04、4、四月、四月? 2013、'13、13?),所以我认为最好的解决方案是让他们从选项中选择,这也节省了移动用户令人厌烦的输入。日期以月份中的数字记录,月份以完整月份名称记录,年份以完整的四位数字记录。

当表单的提交按钮被按下时,我们运行addData() 函数,该函数从以下内容开始:

js
function addData(e) {
  e.preventDefault();

  if (!title.value || !hours.value || !minutes.value || !day.value || !month.value || !year.value) {
    note.appendChild(document.createElement("li")).textContent = "Data not submitted — form incomplete.";
    return;
  }

在此部分,我们检查表单字段是否已全部填写。如果没有,我们将一条消息放入我们的开发者通知窗格中(请参阅应用程序 UI 的左下角),告知用户发生了什么情况,并退出函数。此步骤主要适用于不支持 HTML 表单验证的浏览器(我在 HTML 中使用了required 属性以强制执行验证,在支持验证的浏览器中。)

js
   else {
    const newItem = [
      {
        taskTitle: title.value,
        hours    : hours.value,
        minutes  : minutes.value,
        day      : day.value,
        month    : month.value,
        year     : year.value,
        notified : "no"
      }
    ];

    // open a read/write db transaction, ready for adding the data
    const transaction = db.transaction(["toDoList"], "readwrite");

    // report on the success of opening the transaction
    transaction.oncomplete = (event) => {
      note.appendChild(document.createElement("li")).textContent =
        "Transaction opened for task addition.";
    };

    transaction.onerror = (event) => {
      note.appendChild(document.createElement("li")).textContent =
        "Transaction not opened due to error. Duplicate items not allowed.";
    };

    // create an object store on the transaction
    const objectStore = transaction.objectStore("toDoList");

    // add our newItem object to the object store
    const request = objectStore.add(newItem[0]);

在此部分,我们创建了一个名为newItem 的对象,该对象以插入数据库所需格式存储数据。接下来的几行打开数据库事务并提供消息以通知用户操作是否成功或失败。然后,创建一个objectStore,将新项添加到其中。数据对象的notified 属性表明待办事项列表项的截止日期尚未到来并且未被通知 - 稍后将详细介绍!

**注意:** db 变量存储对 IndexedDB 数据库实例的引用;然后,我们可以使用该变量的各种属性来操作数据。

js
    request.onsuccess = (event) => {

      note.appendChild(document.createElement("li")).textContent = "New item added to database.";

      title.value = "";
      hours.value = null;
      minutes.value = null;
      day.value = "01";
      month.value = "January";
      year.value = 2020;
    };
  }

下一部分创建一条日志消息,以表明新项添加成功,并重置表单,以便准备好输入下一个任务。

js
  // update the display of data to show the newly added item, by running displayData() again.
  displayData();
};

最后,我们运行displayData() 函数,该函数更新应用程序中数据的显示,以显示刚刚输入的新任务。

检查截止日期是否已到

此时,我们的数据已在数据库中;现在我们要检查是否有任何截止日期已到。这是由我们的checkDeadlines() 函数完成的

js
function checkDeadlines() {
  const now = new Date();

首先,我们通过创建一个空白的Date 对象来获取当前日期和时间。很简单,对吧?接下来会稍微复杂一点。

js
const minuteCheck = now.getMinutes();
const hourCheck = now.getHours();
const dayCheck = now.getDate();
const monthCheck = now.getMonth();
const yearCheck = now.getFullYear();

Date 对象有一些方法可以提取其中的日期和时间各个部分。在这里,我们获取当前分钟数(给出易于理解的数值)、小时数(给出易于理解的数值)、月份中的日期(需要getDate(),因为getDay() 返回的是一周中的日期,1-7)、月份(返回 0-11 之间的数字,请参见下文)和年份(需要getFullYear()getYear() 已被弃用,并返回一个奇怪的值,对任何人都没有多大用处!)

js
  const objectStore = db.transaction(['toDoList'], "readwrite").objectStore('toDoList');

  objectStore.openCursor().onsuccess = (event) => {
    const cursor = event.target.result;
    let monthNumber;

    if (cursor) {

接下来,我们创建另一个 IndexedDB objectStore,并使用openCursor() 方法打开一个游标,这基本上是 IndexedDB 中遍历存储中所有项的一种方法。然后,只要游标中还有有效的项,我们就遍历游标中的所有项。

js
switch (cursor.value.month) {
  case "January":
    monthNumber = 0;
    break;
  case "February":
    monthNumber = 1;
    break;

  // other lines removed from listing for brevity

  case "December":
    monthNumber = 11;
    break;
  default:
    alert("Incorrect month entered in database.");
}

我们首先将存储在数据库中的月份名称转换为 JavaScript 能够理解的月份编号。正如我们之前所见,JavaScript Date 对象创建月份值,该值是 0 到 11 之间的数字。

js
if (
  Number(cursor.value.hours) === hourCheck &&
  Number(cursor.value.minutes) === minuteCheck &&
  Number(cursor.value.day) === dayCheck &&
  monthNumber === monthCheck &&
  cursor.value.year === yearCheck &&
  notified === "no"
) {
  // If the numbers all do match, run the createNotification()
  // function to create a system notification
  createNotification(cursor.value.taskTitle);
}

现在我们已经组装了想要与 IndexedDB 存储值进行比较的当前时间和日期部分,是时候进行检查了。在向用户显示某种通知以告知他们截止日期已到之前,我们希望所有值都匹配。

在这种情况下,+ 运算符将带前导零的数字转换为等效的无前导零数字,例如 09 -> 9。这是因为 JavaScript Date 数字值永远不会有前导零,但我们的数据可能会有。

notified === "no" 检查旨在确保您每个待办事项项目只收到一次通知。当每个项目对象的通知被触发时,其notification 属性被设置为"yes",因此此检查将不会在下次迭代时通过,方法是在createNotification() 函数中使用以下代码(阅读使用 IndexedDB 以获取解释)

js
    // now we need to update the value of notified to "yes" in this particular data object, so the
    // notification won't be set off on it again

    // first open up a transaction as usual
    const objectStore = db.transaction(['toDoList'], "readwrite").objectStore('toDoList');

    // get the to-do list object that has this title as its title
    const request = objectStore.get(title);

    request.onsuccess = () => {
      // grab the data object returned as the result
      const data = request.result;

      // update the notified value in the object to "yes"
      data.notified = "yes";

      // create another request that inserts the item back into the database
      const requestUpdate = objectStore.put(data);

      // when this new request succeeds, run the displayData() function again to update the display
      requestUpdate.onsuccess = () => {
        displayData();
      }

如果所有检查都匹配,我们将运行createNotification() 函数以向用户提供通知。

js
      cursor.continue();
    }
  }
}

函数的最后一行将游标向前移动,这将导致上述截止日期检查机制对存储在 IndexedDB 中的下一个任务运行。

继续检查!

当然,只运行一次上述截止日期检查函数是没有用的!我们希望不断检查所有截止日期,以查看是否有任何截止日期已到。为此,我们使用setInterval() 每秒运行一次checkDeadlines()

js
setInterval(checkDeadlines, 1000);