使用原生 JavaScript 构建离线表单
创建表单
创建空白草稿
页面退出时填写草稿
提交表格
最近,我和一些同事聊起,作为一名 Web 开发者,我是否经常需要同时使用离线和在线数据。我最初的回答是否定的,除了我开发的 Progressive Web App 的离线页面之外,我想不出还有什么情况需要离线数据。进一步的追问下,我意识到自己在很多情况下都实现了离线数据模式,比我想象的要多——比如在创建带有离线回退功能的自动保存表单时。
在需要大量编写的表单中,例如 GitHub 问题和博客编辑器,自动保存回退功能越来越常见。我一生中多次因关闭标签页或意外刷新表单而丢失十五分钟的工作成果,这至少可以说很烦人。对于网络或手机信号不稳定的地区用户来说,这种情况尤其严重,他们的系统可能会时断时续,即使断网也需要保留数据。在某些情况下,例如医疗系统、金融和采矿业,数据丢失可能会造成严重后果。
在本教程中,我们将构建一个在线离线笔记编辑器。当用户退出页面时,他们未完成的表单数据将被保存,并在用户返回页面时自动加载到表单中。我们将通过将正在进行的笔记标记为草稿来区分已加载到编辑器中的帖子和已完成的笔记。以下是本教程的完整代码。
通常,这是通过在页面退出时将数据存储在 localStorage 中来实现的。LocalStorage 的 API 对开发者友好,很大程度上是因为它是同步的,并且可以跨浏览器会话保存数据。因此,用户在每个设备上都会存储一份草稿,这对于简单的用例来说非常好,但如果用户通过其他设备更新数据,情况很快就会变得非常复杂——他们会加载哪个版本的数据?离线/在线数据问题比人们最初想象的要复杂得多:你实际上是在创建一个分布式系统。你使用 localStorage 存储部分数据,并使用数据库存储其余数据。此外,localStorage 可以存储的数据量是有限的,而且它的同步性会阻塞主线程。
当数据分布式时,CAP 定理就会发挥作用,该定理指出,一个系统只能兼具分区容错性、一致性和可用性这三者中的两个。分区容错性意味着系统在发生中断时仍能继续运行;可用性意味着每个请求在成功或失败时都能得到响应;一致性意味着所有副本同时拥有相同的数据。对于带有前端的应用,分区容错性是必需的:至少要有一台服务器和一个客户端,或者至少有两个分区。我们也已经声明过,我们希望数据在线和离线都可用。因此,完全一致性是被牺牲的,取而代之的是“最终一致性”。
最终一致性可能会使开发人员的编程逻辑更加困难。当您创建数据并成功执行后,您希望在查询中返回该数据。如果您需要考虑返回过时数据的情况,这很容易引入错误,从而导致应用程序用户体验不佳。在本教程中,我们将使用 AWS Amplify DataStore 来处理这些合并问题。
请注意,我是 AWS Amplify 团队的开发倡导者,如果您对此有任何反馈或疑问,请联系我或在我们的 discord 上提问 - discord.gg/amplify!
使用我们的离线/在线编辑器,当用户离线时,在用户重新上线之前,本地数据和全局数据都会有所不同。它采用本地优先原则,这意味着当您对数据运行查询或修改时,您将首先更新 IndexedDB(DataStore 默认的设备存储引擎)中的数据。它与 localStorage 类似,但允许存储更多数据并进行异步更新,但 API 也更为复杂。由于我们使用 DataStore 对其进行了抽象,因此我们无需担心这一点。之后,如果您启用在线存储,您的数据将同步到您选择的 AWS 数据库(默认为 DynamoDB)。
创建数据模型
首先,我们将使用 Amplify Admin UI 创建一个数据模型。
- 前往https://sandbox.amplifyapp.com,然后单击“创建应用后端”下的“开始”
- 选择数据作为要设置的功能,然后选择从空白模式开始。
- 在左上角,单击模型。
- 将模型命名为“Note”。
- 添加字段
title, draft
和body
。 - 选择
title
,draft
然后单击右侧菜单上的所需内容。 - 将的类型设置为。
draft
boolean
然后,点击“下一步:在您的应用中进行本地测试”按钮。请注意,您无需拥有 AWS 账户即可进行测试,只有在将来选择部署数据库时才需要一个。
创建项目
现在,我们将为我们的项目创建一个前端应用。该页面上有针对各种类型应用的说明,但由于我们不会为此应用使用框架,因此我们将忽略这些说明并创建自己的应用。点击两次“下一步”。
如果您想继续学习,我通常使用这个入门模板。您需要一个开发服务器才能使用 Amplify,因为它使用 ES 模块,而 DataStore 需要 TypeScript 转译器,所以它不像创建一个 HTML 文件那么简单。
然后,使用生成的命令安装 Amplify CLI 并将数据模型拉取到您的应用程序中。请注意,您需要使用您的个人沙盒 ID,该 ID 位于“在您的应用程序中进行本地测试”页面第 3 步中生成的命令中。
$ curl -sL https://aws-amplify.github.io/amplify-cli/install | bash && $SHELL
$ amplify pull --sandboxId your-sandbox-id
然后安装aws-amplify
JavaScript 库和 TypeScript。
$ npm i aws-amplify typescript
现在,在您的 JavaScript 文件中配置 Amplify:
import { Amplify, DataStore } from 'aws-amplify'
import awsconfig from './aws-exports'
import { Note } from './models'
Amplify.configure(awsconfig)
我们还将导入该Note
模型以供将来使用。
创建表单
首先,在 HTML 文件中创建一个表单,允许用户创建新笔记。我们只包含标题和正文字段。草稿字段将由我们的代码管理,无需最终用户管理。
<form class="create-form">
<label for="title">Title</label>
<input type="text" name="title" id="title">
<label for="body">Body</label>
<textarea type="text" name="body" id="body"></textarea>
<input type="submit" value="Create">
</form>
我们还需要Note
在表单提交时创建一个新对象。我们将为其添加一个事件监听器,然后在 DataStore 中创建一个新的笔记,用于捕获用户输入的标题和正文。由于它已提交,因此它不会是草稿。
document.querySelector('.create-form').addEventListener('submit', async e => {
try {
e.preventDefault()
const title = document.querySelector('#title').value
const body = document.querySelector('#body').value
const newNote = await DataStore.save(
new Note({
title,
body,
draft: false
})
)
console.log(newNote)
} catch (err) {
console.error(err)
}
})
创建空白草稿
到目前为止,我们已经创建了一个标准表单,用于在提交表单时保存新注释。现在,我们需要添加自动保存功能。
其工作原理是,我们始终会保留一条草稿笔记。页面加载时,我们会查询 DataStore 以查看是否存在草稿。如果存在,我们会将其标题和正文加载到表单中作为起始点。如果不存在,我们会创建一个新的空草稿笔记,并在用户退出页面时保存。
页面加载时,我们将使用DataStore 的查询语言查询 DataStore,查找处于草稿状态的笔记。我们还将创建一个变量来存储用户正在处理的当前草稿。
let draft = {}
window.addEventListener('load', async () => {
const drafts = await DataStore.query(Note, note => note.draft('eq', true))
})
我们还将创建一个用于创建新空白草稿的函数。这会将全局 draft 变量设置为新的空白草稿注释。
async function createNewDraft () {
try {
draft = await DataStore.save(
new Note({
title: '',
body: '',
draft: true
})
)
} catch (err) {
console.error(err)
}
}
现在,我们将添加一个条件语句来检查有多少个草稿。如果超过一个,我们就抛出一个错误——这种情况绝对不应该发生。
如果 DataStore 中目前没有草稿,我们需要创建一个新的。如果有草稿,我们将使用当前草稿的信息更新表单中的 Tile 和 Body。
let draft = {}
window.addEventListener('load', async () => {
const drafts = await DataStore.query(Note, note => note.draft('eq', true))
if (drafts.length === 0) {
createNewDraft()
} else if (drafts.length === 1) {
draft = drafts[0]
document.querySelector('#title').value = draft.title
document.querySelector('#body').value = draft.body
} else {
alert('weird! you have multiple drafts!')
}
})
页面退出时填写草稿
现在我们有了草稿,我们希望每当用户离开页面或刷新标签页时,该草稿都会自动保存。我们将在页面中添加一个事件监听器来监听该beforeunload
事件。
DataStore.save()
既可用于创建(我们之前用过),也可用于更新。为了更新当前存储的Note
,我们将创建它的副本,并更新我们想要更改的属性。
window.addEventListener('beforeunload', async () => {
try {
const title = document.querySelector('#title').value
const body = document.querySelector('#body').value
await DataStore.save(Note.copyOf(draft, updatedNote => {
updatedNote.title = title
updatedNote.body = body
}))
} catch (err) {
console.error(err)
}
})
提交表格
快完成了!最后一步是修改表单的提交功能。我们不会创建新的笔记,而是使用表单标题和正文修改草稿笔记,然后将 draft 设置为false
。
document.querySelector('.create-form').addEventListener('submit', async e => {
try {
e.preventDefault()
const title = document.querySelector('#title').value
const body = document.querySelector('#body').value
const newNote = await DataStore.save(Note.copyOf(draft, updatedNote => {
updatedNote.title = title
updatedNote.body = body
updatedNote.draft = false
}))
console.log(newNote)
} catch (err) {
console.error(err)
}
})
我们还需要创建一个新的空白草稿,以便用户可以开始输入新的笔记。我们还需要重置表单。
document.querySelector('.create-form').addEventListener('submit', async e => {
try {
e.preventDefault()
const title = document.querySelector('#title').value
const body = document.querySelector('#body').value
const newNote = await DataStore.save(Note.copyOf(draft, updatedNote => {
updatedNote.title = title
updatedNote.body = body
updatedNote.draft = false
}))
console.log(newNote)
+ createNewDraft()
+ document.querySelector('#title').value = draft.title
+ document.querySelector('#body').value = draft.body
} catch (err) {
console.error(err)
}
})
部署
目前,在应用的测试版本中,我们只是将数据本地存储在设备上,而不是将其同步到云数据库。为了启用在线/离线同步,您可以返回浏览器中的沙盒并部署后端。除了重新运行 Amplify pull 命令以获取数据库链接外,您无需在代码中执行任何其他操作。
这款编辑器还有很多功能可以实现。在实际生产用例中,您希望每个用户都拥有一份草稿,而不是只加载一份全局草稿到编辑器中。您还可能需要调整冲突规则,例如,如果用户在重新上线之前在其他设备上编辑了数据,则会发生冲突。
另一个潜在的功能是保存每个草稿版本。一种可能的实现是存储一个Note
包含多个子Version
模型的父模型。每个子模型Version
都会order
附加一个编号,以便按顺序访问。最终版本还会带有一个已发布标志来区分。您可以通过多种方式更改此模式,以适应更复杂的用例。
结论
自动保存表单和应用即使在离线状态下也能提供数据,有助于缓解用户烦恼,并为网络和移动连接不稳定地区的用户带来更佳体验。拥有高性能的离线应用对于实现全球可访问性至关重要。Amplify DataStore 无需大量开发人员投入即可在应用程序中实现这一点。