用Vanilla JavaScript构建一个可离线使用的表单(详细教程)

339 阅读10分钟

最近我和一些同事进行了一次谈话,讨论我作为一个网络开发者是否经常需要有离线和在线的数据。我最初的回答是否定的,除了我所开发的渐进式网络应用程序的离线页面之外,我想不出有多少需要离线数据的情况。在进一步的提示下,我意识到我已经在更多的情况下实现了离线数据模式--即在创建带有离线回退功能的自动保存表单。

在需要大量写作的表单中,自动保存回退是一种越来越常见的模式,比如GitHub问题和博客编辑器。在我的生活中,我曾多次关闭标签或不小心刷新表单而失去了15分钟的工作,这至少可以说是很恼火的。对于那些没有稳定的互联网或手机服务的地区的人来说,这就更有意义了,他们可能会忽上忽下,即使失去互联网,也需要让他们的数据持续存在。在某些情况下,如医疗系统、金融和采矿业,数据丢失可能会产生可怕的后果。

在本教程中,我们将建立一个在线离线笔记编辑器。当用户退出页面时,他们部分完成的表单数据将被保存,并在他们回到页面时自动加载回表单中。我们将通过把正在进行的Note标记为草稿,来区分加载到编辑器中的帖子和已经完成的笔记。下面是本教程的完成代码

通常情况下,这是在页面退出时通过在localStorage中存储数据来实现的。LocalStorage的API对开发者很友好,这在很大程度上是因为它是同步的,可以跨浏览器会话保存数据。因此,用户在每个设备上有一个存储的草稿,这对简单的用例来说是很好的,但如果用户通过另一个设备更新数据,它就会很快变得非常复杂--哪个版本的数据会为他们加载?离线/在线数据问题比人们最初想象的要复杂得多:你本质上是在创建一个分布式系统。你对一些数据使用localStorage,对其余的数据使用你的数据库。另外,localStorage能存储多少数据以及它的同步性会阻碍主线程,这都是有限制的。

当你有分布式数据时,CAP定理就发挥作用了,它指出一个系统只能有三个中的两个:分区容忍度、一致性和可用性。分区容忍度意味着系统在出现故障时继续运行,可用性意味着每个请求在成功或失败时都能得到响应,一致性意味着所有复制在同一时间拥有相同的数据。对于一个有前端的应用程序来说,分区容忍是必要的:你有一个服务器和一个客户端,或者至少有两个分区。我们也已经说过,我们希望我们的数据可以在线和离线。所以,完全一致性是牺牲的分支,而颁布了 "最终一致性"。

最终一致性会使开发人员的编程逻辑更加困难。当你创建数据并且成功时,你希望在查询中获得这些数据。如果你需要考虑拿回陈旧数据的情况,这很容易引入错误,导致应用程序用户的体验不佳。在本教程中,我们将使用AWS Amplify DataStore,它为我们处理这些合并问题。

通过我们的离线/在线编辑器,当用户离线时,会有不同的本地和全局数据,直到用户重新上线。它是本地优先,这意味着当你对数据运行查询或突变时,你将首先更新IndexedDB中的数据,这是默认的DataStore设备上的存储引擎。它与localStorage类似,但允许更多的数据和异步更新,以换取更复杂的API,我们不需要担心这个问题,因为我们使用DataStore来抽象它。然后,如果你启用在线存储,你的数据将同步到你选择的AWS数据库,默认为DynamoDB。

创建一个数据模型

首先,我们将使用Amplify管理界面创建一个数据模型。

  1. 进入sandbox.amplifyapp.com,然后点击创建一个应用程序后台下的 "开始"
  2. 选择数据作为要设置的功能,并选择从一个空白模式开始
  3. 在左上角,点击模型
  4. 将模型命名为 "Note"
  5. 添加字段title, draftbody
  6. 选择titledraft ,然后在右边的菜单上点击需要
  7. draft类型设置为boolean

然后,点击 "下一步。在你的应用程序中进行本地测试 "按钮。注意,你不需要有AWS账户来进行测试,只有在你将来选择部署数据库时才需要一个。

创建一个项目

现在,我们将为我们的项目创建一个前端应用程序。该页面上有关于各种类型的应用程序的说明,但我们将忽略这些,并创建我们自己的,因为我们不会为这个应用程序使用一个框架。点击 "下一步 "两次。

如果你想跟着做,我通常使用这个启动模板。你确实需要一个开发服务器来使用Amplify,因为它使用ES模块,而DataStore需要一个TypeScript转译器,所以它并不像创建一个HTML文件那么简单。

然后,使用生成的命令来安装Amplify CLI并将数据模型拉到你的应用程序中。注意,你将需要使用你的个人沙盒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))
})

我们还将创建一个函数来制作一个新的空白草稿。这将把那个全局草稿变量设置为一个新的空白草稿纸。

async function createNewDraft () {
  try {
    draft = await DataStore.save(
      new Note({
        title: '',
        body: '',
        draft: true
      })
    )
  } catch (err) {
    console.error(err)
  }
}

现在,我们将添加一个条件,检查有多少个草稿。如果多于一个,我们要抛出一个错误--这不应该发生。

如果DataStore中目前没有草稿,我们就需要创建一个新的。如果有草稿,那么我们将用当前草稿的信息更新表单中的瓦片和正文。

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)
  }
})

提交表单

我们就快成功了!需要的最后一步是改变表单的提交功能。我们不是创建一个新的注释,而是用表单的标题和正文修改我们的草稿注释,然后将草稿设置为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以获得到数据库的链接外,你不需要在你的代码中做任何其他事情。

对于这个编辑器,还有很多事情可以做。在一个生产用例中,你会希望每个用户都有一个草稿,而不是一个加载到编辑器的全局草稿。你也可能想调整碰撞规则,例如,如果用户在重新上线之前在另一个设备上编辑数据,会发生什么情况。

另一个潜在的功能是保存每个草稿版本。一个可能的实现是存储一个有多个子Versions的Note 父模型。每个Version 将有一个order 号,以便它们可以被按顺序访问。最终的版本也会有一个发布的标志来区分它。你有很多方法可以改变这种模式,以适应更复杂的使用情况。

结论

自动保存表单和应用程序,即使在离线时也有数据可用,这有助于缓解用户的烦恼,并为互联网和移动连接不稳定的地区的用户带来更好的体验。拥有一个高性能的离线可用的应用程序对于全球可访问性是非常重要的。Amplify DataStore有助于在应用程序中实现这一点,而不需要大量的开发人员。

进一步阅读