检查截止日期何时到期

在本文中,我们将通过一个复杂的示例,演示如何将当前时间和日期与存储在 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、四月、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
function addData(e) {
  // ...
  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
function addData(e) {
  // ...
  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;
  };
  // 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();
  const minuteCheck = now.getMinutes();
  const hourCheck = now.getHours();
  const dayCheck = now.getDate();
  const monthCheck = now.getMonth();
  const yearCheck = now.getFullYear();
  // ...
}

首先,我们通过创建一个空白的 Date 对象来获取当前的日期和时间。Date 对象有许多方法可以提取其中的日期和时间的不同部分。在这里,我们获取当前的分钟(提供一个简单的数值)、小时(提供一个简单的数值)、月份中的日期(需要 getDate(),因为 getDay() 返回星期几,1-7)、月份(返回 0-11 的数字,见下文)和年份(需要 getFullYear()getYear() 已弃用,并返回一个奇怪的、几乎无用的值!)。

js
function checkDeadlines() {
  // ...
  const objectStore = db
    .transaction(["toDoList"], "readwrite")
    .objectStore("toDoList");

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

    if (!cursor) return;
    // ...
    cursor.continue();
  };
}

接下来,我们创建另一个 IndexedDB objectStore,并使用 openCursor() 方法打开一个游标,这基本上是 IndexedDB 中迭代存储中所有项的一种方式。然后,只要游标中还有有效项,我们就遍历游标中的所有项。函数中的最后一行移动游标,这将导致上述截止日期检查机制对存储在 IndexedDB 中的下一项任务运行。

现在我们开始填写用于检查截止日期的 onsuccess 处理程序中的代码。

js
const { hours, minutes, day, month, year, notified, taskTitle } = cursor.value;
const monthNumber = MONTHS.indexOf(month);
if (monthNumber === -1) throw new Error("Incorrect month entered in database.");

我们首先要做的就是将存储在数据库中的月份名称转换为 JavaScript 可以理解的月份数字。正如我们之前看到的,JavaScript Date 对象将月份值创建为 0 到 11 之间的数字。

现在我们已经组装好了要与 IndexedDB 中存储的值进行比较的当前时间和日期片段,是时候执行检查了。我们希望所有值都匹配,然后才能向用户显示某种通知,告诉他们截止日期已到。如果所有检查都匹配,然后我们运行 createNotification() 函数来向用户提供通知。

js
let matched = parseInt(hours, 10) === hourCheck;
matched &&= parseInt(minutes, 10) === minuteCheck;
matched &&= parseInt(day, 10) === dayCheck;
matched &&= monthNumber === monthCheck;
matched &&= parseInt(year, 10) === yearCheck;
if (matched && notified === "no") {
  // If the numbers all do match, run the createNotification() function to create a system notification
  // but only if the permission is set
  if (Notification.permission === "granted") {
    createNotification(taskTitle);
  }
}

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 objectStoreTitleRequest = objectStore.get(title);

objectStoreTitleRequest.onsuccess = () => {
  // Grab the data object returned as the result
  const data = objectStoreTitleRequest.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 updateTitleRequest = objectStore.put(data);

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

继续检查!

当然,只运行一次上述截止日期检查函数是没用的!我们需要不断检查所有截止日期,看看是否有任何截止日期即将到来。为了做到这一点,我们使用 setInterval() 每秒运行一次 checkDeadlines()

js
setInterval(checkDeadlines, 1000);