檢查截止日期何時到期

在本文中,我們將透過一個複雜的示例,演示如何將當前時間和日期與儲存在 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);