如何制作离线友好的表单?

2,203 阅读4分钟

本文翻译自 Offline-Friendly Forms

在网络不佳的情况下表单的表现并不理想。如果提交表单的时候恰好断网了,辛苦填好的数据有可能就找不回来了。下面就分享一下我们是如何修复这个问题的。

先睹为快,CodePen 上的 Demo

自从有了 Service Worker,开发者可以为用户提供离线版的 Web 应用。静态资源的缓存相对比较容易,但对于需要和服务端交互的表单来说就有点困难了。不过还好,我们还是有办法为离线提供一种 fallback 的方案。

首先,创建一个与离线友好表单相对应的 Class。将表单的 idaction 保存下来,并绑定表单的 submit 事件。

class OfflineForm {
  // setup the instance.
  constructor(form) {
    this.id = form.id;
    this.action = form.action;
    this.data = {};

    form.addEventListener('submit', e => this.handleSubmit(e));
  }
}

在 submit 的处理函数中,可以使用 navigator.onLine 来检查网络链接情况,这个 API 浏览器都支持得差不多了,就算要实现也比较简单。

但是这个 API 存在误报的可能。因为这个属性只能表示存在网络链接,并不保证网络是通的。反过来,如果结果是 false 就可以明确表示是断网的。因此据此判断是否离线是一种相对靠谱的方式。

如果用户处于离线状态,我们将表单提 hold 住,把表单数据保存在本地。

handleSubmit(e) {
  e.preventDefault();
  // parse form inputs into data object.
  this.getFormData();

  if (!navigator.onLine) {
    // user is offline, store data on device.
    this.storeData();
  } else {
    // user is online, send data via ajax.
    this.sendData();
  }
}

保存表单

我们有数种在用户设备商保存数据的方式。根据数据的特点,你可以使用 sessionStorage,如果不想把数据存储在内存种,也可保存在localStorage 中。

给表单数据付上一个时间戳,挂在一个新对象上。然后使用 localStorage.setItem 保存。这个方法接受两个参数,key(表单 id) 和 value(通常是一个 JSON 字符串)。

storeData() {
  // check if localStorage is available.
  if (typeof Storage !== 'undefined') {
    const entry = {
      time: new Date().getTime(),
      data: this.data,
    };
    // save data as JSON string.
    localStorage.setItem(this.id, JSON.stringify(entry));
    return true;
  }
  return false;
}

注意:可以在 Chrome 的开发者工具的 Application 标签中查看 storage 数据。如果不出差错,里面的内容如下:

devtools.png
devtools.png

还有最好将离线的情况告知用户,告诉他们填写的数据并不会丢失。补充 handleSubmit 方法,将这些信息反馈给用户。

message.png
message.png

考虑得真是周到呀!

检查保存的数据

一旦用户联网,就需要检查一下本地是否存在未提交的表单。我们需要监听 online 事件跟踪网络链接的变化,或者页面刷新触发的 load 事件。

constructor(form){
  ...
  window.addEventListener('online', () => this.checkStorage());
  window.addEventListener('load', () => this.checkStorage());
}

当这些事件触发时,我们只需检查在 storage 中是否存在与表单 id 相匹配的数据。根据表单数据类型的不同,可能需要添加一个过期时间的判断,只允许在特定的时间内的表单。这一点对于偶尔网络链接异常的情况很有效果,避免错误地把两个月以前保存在本地的表单提交。

checkStorage() {
  if (typeof Storage !== 'undefined') {
    // check if we have saved data in localStorage.
    const item = localStorage.getItem(this.id);
    const entry = item && JSON.parse(item);

    if (entry) {
      // discard submissions older than one day. (optional)
      const now = new Date().getTime();
      const day = 24 * 60 * 60 * 1000;
      if (now - day > entry.time) {
        localStorage.removeItem(this.id);
        return;
      }

      // we have valid form data, try to submit it.
      this.data = entry.data;
      this.sendData();
    }
  }
}

如果表单成功提交,则还需要最后一步,将表单数据从 localStorage 清除。比如说一个 Ajax 表单,我们可以在服务端成功返回后马上实施清除动作。这里简单使用 storage
removeItem() 方法即可。

sendData() {
  // send ajax request to server
  axios.post(this.action, this.data)
    .then((response) => {
      if (response.status === 200) {
        // remove stored data on success
        localStorage.removeItem(this.id);
      }
    })
    .catch((error) => {
      console.warn(error);
    });
}

如果实在不想使用 Ajax 提交,另外一种处理方案就是将数据回填表单,直接调用 form.submit() 提交,或者让用户自己点击提交按钮提交。

注意:简单起见,我略去了一些环节,比如表单验证以及安全 token 验证等等。这些步骤在真正的产品开发中是必不可少的。还有一个问题是关于敏感数据的处理,避免在本地明文存储用户密码信用卡信息等等。

如果有兴趣,可以在 CodePen 里查看完整的例子