CycleTracker:JavaScript 功能
在上一个部分,我们为 CycleTracker 编写了 HTML 和 CSS,创建了一个静态版本的 Web 应用。在本部分,我们将编写将静态 HTML 转换为功能齐全的 Web 应用所需的 JavaScript。
如果您还没有这样做,请复制 HTML 和 CSS,并将它们保存到名为 index.html 和 style.css 的文件中。
HTML 文件中的最后一行调用了 app.js JavaScript 文件。这就是我们在此部分创建的脚本。在本课中,我们将编写客户端 JavaScript 代码来捕获表单提交、在本地存储提交的数据以及填充“过去的周期”部分。
在本课结束时,您将拥有一个功能齐全的应用。在未来的课程中,我们将逐步增强该应用,创建一个完全可安装的 PWA,即使在用户离线时也能运行。
JavaScript 任务
当用户访问页面时,我们会检查他们是否在本地存储中存储了现有数据。用户第一次访问页面时,不会有任何数据。当新用户选择两个日期并提交表单时,我们需要
对于每次后续的表单提交,我们需要
- 将新的月经周期添加到当前列表中
- 按日期顺序对列表进行排序
- 使用新的列表重新填充
<ul>,每个周期一个<li> - 将数据追加到我们保存的本地存储中
现有用户将在本地存储中有现有数据。当用户在同一设备上的同一浏览器中返回我们的网页时,我们需要
这是一个初学者级别的演示应用程序。目标是教授将 Web 应用转换为 PWA 的基础知识。此应用程序不包含表单验证、错误检查、编辑或删除功能等必要功能。欢迎您扩展涵盖的功能,并根据您的学习目标和应用需求定制课程和应用。
表单提交
该页面包含一个 <form>,其中包含用于选择每个月经周期开始和结束日期的日期选择器。日期选择器是 <input> 类型为 date,其 id 分别为 start-date 和 end-date。
表单没有 method 或 action。相反,我们使用 addEventListener() 为表单添加了一个事件监听器。当用户尝试提交表单时,我们会阻止表单提交,存储新的月经周期,将此周期与过去的周期一起渲染,然后重置表单。
// create constants for the form and the form controls
const newPeriodFormEl = document.getElementsByTagName("form")[0];
const startDateInputEl = document.getElementById("start-date");
const endDateInputEl = document.getElementById("end-date");
// Listen to form submissions.
newPeriodFormEl.addEventListener("submit", (event) => {
// Prevent the form from submitting to the server
// since everything is client-side.
event.preventDefault();
// Get the start and end dates from the form.
const startDate = startDateInputEl.value;
const endDate = endDateInputEl.value;
// Check if the dates are invalid
if (checkDatesInvalid(startDate, endDate)) {
// If the dates are invalid, exit.
return;
}
// Store the new period in our client-side storage.
storeNewPeriod(startDate, endDate);
// Refresh the UI.
renderPastPeriods();
// Reset the form.
newPeriodFormEl.reset();
});
在用 preventDefault() 阻止表单提交后,我们
- 验证用户输入;无效时退出,
- 通过检索、解析、追加、排序、字符串化和重新存储 localStorage 中的数据来存储新周期,
- 渲染表单数据以及过去月经周期的数据和一个部分标题,并且
- 使用 HTMLFormElement 的
reset()方法重置表单
验证用户输入
我们检查日期是否无效。我们进行最少的错误检查。我们确保日期都不是 null,这应该由 required 属性阻止。我们还检查开始日期是否不大于结束日期。如果出现错误,我们会清除表单。
function checkDatesInvalid(startDate, endDate) {
// Check that end date is after start date and neither is null.
if (!startDate || !endDate || startDate > endDate) {
// To make the validation robust we could:
// 1. add error messaging based on error type
// 2. Alert assistive technology users about the error
// 3. move focus to the error location
// instead, for now, we clear the dates if either
// or both are invalid
newPeriodFormEl.reset();
// as dates are invalid, we return true
return true;
}
// else
return false;
}
在更健壮的该应用版本中,我们至少会包含错误消息,告知用户存在错误。一个好的应用会告知用户错误是什么,将焦点放在有问题的表单控件上,并使用 ARIA live regions 来提醒辅助技术用户注意错误。
本地存储
我们正在使用 Web Storage API,特别是 window.localStorage,将开始和结束日期对存储在字符串化的 JSON 对象中。
LocalStorage 有一些限制,但足以满足我们应用的需求。我们使用 localStorage 使此应用简单且仅在客户端运行。这意味着数据将仅存储在一个设备上的一个浏览器中。清除浏览器数据也会丢失所有本地存储的周期。对于许多应用来说可能看起来是限制,但对于这个应用来说可能是一个优势,因为月经周期数据是私密的,此类应用的用户可能非常关心隐私。
对于更健壮的应用,其他 客户端存储选项,如 IndexedDB (IDB) 和稍后讨论的服务工作线程,具有更好的性能。
localStorage 的限制包括
- 有限的数据存储:
localStorage每 origin 的数据限制为 5MB。我们的存储需求远低于此。 - 仅存储字符串:
localStorage将数据存储为字符串键值对。我们的开始和结束日期将作为解析为字符串的 JSON 对象存储。对于更复杂的数据,需要更健壮的存储机制,如 IDB。 - 可能导致性能不佳:从本地存储获取和设置是同步在主线程上完成的。当主线程被占用时,应用没有响应,看起来卡死了。由于此应用的性质有限,这种糟糕用户体验的短暂中断是微不足道的。
- 仅主线程可用:除了占用主线程的性能问题外,服务工作线程无法访问主线程,这意味着服务工作线程无法直接设置或获取本地存储数据。
检索、追加、排序和重新存储数据
由于我们使用的是 localStorage,它包含一个单一的字符串,因此我们从本地存储中检索数据的 JSON 字符串,解析 JSON 数据(如果有),将新的日期对推送到现有数组,对日期进行排序,将 JSON 对象解析回字符串,然后将该字符串保存回 localStorage。
此过程需要创建几个函数
// Add the storage key as an app-wide constant
const STORAGE_KEY = "period-tracker";
function storeNewPeriod(startDate, endDate) {
// Get data from storage.
const periods = getAllStoredPeriods();
// Add the new period object to the end of the array of period objects.
periods.push({ startDate, endDate });
// Sort the array so that periods are ordered by start date, from newest
// to oldest.
periods.sort((a, b) => new Date(b.startDate) - new Date(a.startDate));
// Store the updated array back in the storage.
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(periods));
}
function getAllStoredPeriods() {
// Get the string of period data from localStorage
const data = window.localStorage.getItem(STORAGE_KEY);
// If no periods were stored, default to an empty array
// otherwise, return the stored data as parsed JSON
const periods = data ? JSON.parse(data) : [];
return periods;
}
将数据渲染到屏幕
我们的应用程序的最后一步是将过去的周期列表以及一个标题渲染到屏幕上。
在我们的 HTML 中,我们在 <section id="past-periods"> 添加了一个占位符,用于包含标题和过去的周期列表。
将容器元素添加到脚本顶部的内容列表中。
const pastPeriodContainer = document.getElementById("past-periods");
我们检索解析后的过去周期字符串,或者一个空数组。如果为空,则退出。如果存在过去的周期,我们会清除过去周期容器中当前的内容。我们创建一个标题和一个无序列表。我们遍历过去的周期,添加包含格式化后的开始和结束日期的列表项。
function renderPastPeriods() {
// get the parsed string of periods, or an empty array.
const periods = getAllStoredPeriods();
// exit if there are no periods
if (periods.length === 0) {
return;
}
// Clear the list of past periods, since we're going to re-render it.
pastPeriodContainer.textContent = "";
const pastPeriodHeader = document.createElement("h2");
pastPeriodHeader.textContent = "Past periods";
const pastPeriodList = document.createElement("ul");
// Loop over all periods and render them.
periods.forEach((period) => {
const periodEl = document.createElement("li");
periodEl.textContent = `From ${formatDate(
period.startDate,
)} to ${formatDate(period.endDate)}`;
pastPeriodList.appendChild(periodEl);
});
pastPeriodContainer.appendChild(pastPeriodHeader);
pastPeriodContainer.appendChild(pastPeriodList);
}
function formatDate(dateString) {
// Convert the date string to a Date object.
const date = new Date(dateString);
// Format the date into a locale-specific string.
// include your locale for better user experience
return date.toLocaleDateString("en-US", { timeZone: "UTC" });
}
加载时渲染过去的周期
当延迟的 JavaScript 在页面加载时运行时,我们会渲染过去的周期(如果有)。
// Start the app by rendering the past periods.
renderPastPeriods();
完整的 JavaScript
您的 app.js 文件应类似于此 JavaScript
const newPeriodFormEl = document.getElementsByTagName("form")[0];
const startDateInputEl = document.getElementById("start-date");
const endDateInputEl = document.getElementById("end-date");
const pastPeriodContainer = document.getElementById("past-periods");
// Add the storage key as an app-wide constant
const STORAGE_KEY = "period-tracker";
// Listen to form submissions.
newPeriodFormEl.addEventListener("submit", (event) => {
event.preventDefault();
const startDate = startDateInputEl.value;
const endDate = endDateInputEl.value;
if (checkDatesInvalid(startDate, endDate)) {
return;
}
storeNewPeriod(startDate, endDate);
renderPastPeriods();
newPeriodFormEl.reset();
});
function checkDatesInvalid(startDate, endDate) {
if (!startDate || !endDate || startDate > endDate) {
newPeriodFormEl.reset();
return true;
}
return false;
}
function storeNewPeriod(startDate, endDate) {
const periods = getAllStoredPeriods();
periods.push({ startDate, endDate });
periods.sort((a, b) => new Date(b.startDate) - new Date(a.startDate));
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(periods));
}
function getAllStoredPeriods() {
const data = window.localStorage.getItem(STORAGE_KEY);
const periods = data ? JSON.parse(data) : [];
console.dir(periods);
console.log(periods);
return periods;
}
function renderPastPeriods() {
const pastPeriodHeader = document.createElement("h2");
const pastPeriodList = document.createElement("ul");
const periods = getAllStoredPeriods();
if (periods.length === 0) {
return;
}
pastPeriodContainer.textContent = "";
pastPeriodHeader.textContent = "Past periods";
periods.forEach((period) => {
const periodEl = document.createElement("li");
periodEl.textContent = `From ${formatDate(
period.startDate,
)} to ${formatDate(period.endDate)}`;
pastPeriodList.appendChild(periodEl);
});
pastPeriodContainer.appendChild(pastPeriodHeader);
pastPeriodContainer.appendChild(pastPeriodList);
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", { timeZone: "UTC" });
}
renderPastPeriods();
您可以尝试功能齐全的 CycleTracker 周期跟踪 Web 应用,并在 GitHub 上查看 Web 应用源代码。是的,它奏效了,但它还不是 PWA。
接下来
PWA 的核心是一个可以安装的 Web 应用,经过渐进式增强后可以离线工作。现在我们有了一个功能齐全的 Web 应用,我们添加了将它转换为 PWA 所需的功能,包括 manifest 文件、安全连接和 service worker。
首先,我们创建 CycleTracker 的 manifest 文件,包括我们的 CycleTracker PWA 的身份、外观和图标。