HotApp云笔记

来源:互联网 发布:组态软件免费下载 编辑:程序博客网 时间:2024/06/04 19:47

一, 介绍

HotApp云笔记是基于HotApp小程序统计云后台提供的api接口开发的一个微信小程序
功能有: 离线保存笔记、云端数据同步、数据统计分析
项目地址: https://github.com/hotapp888/hotapp-notepad
截图如下:
1
2
这里写图片描述
这里写图片描述

二, 功能分析

  • 需要数据库
    对于一个功能非常简单的小程序来说, 自己去开发一套后台需要话费蛮多时间和精力的, 而且很多小程序开发者不会写后台.
    hotapp 提供了key-value形式的免费接口, 支持get, post, delete和searchkey, 使用起来非常简单, 对于一个简单的笔记本来说完全够用了!

  • 需要云同步
    在离线情况下可以写笔记, 这些笔记应该保存到本地缓存中. 在有网络的时候, 需要把本地的数据上传到hotapp 云后台, 在更换设备之后, 还可以从hotapp 云后台把数据同步到新设备. 在新设备上新增笔记、修改笔记和删除笔记之后, 也可以及时同步到旧设备上

  • 需要数据统计
    需要知道每日新增用户, 活跃用户, 启动次数, 用户留存率, 增删改笔记的次数等. hotapp 提供了这些免费的接口, 一行代码就接入进去后就可以知道这些数据

三, 技术实现细节

1, key-value的设计

hotapp云笔记有两种数据类型: 笔记反馈
对于笔记, 我设置的key是item_openid_rand, 对于反馈, 我设置的key是feedback_openid_rand
这样设计的好处是: 看到item就知道是笔记, 看到feedback就知道是反馈, 如果需要知道是哪个用户的笔记, 就查找item_openid_开头的key就可以了
hotapp云笔记 提供了这些接口, 让我实现起来非常地方便.

  • 获取用户所有的笔记

    /*** 获取所有数据*/getItems: function(cb) {  var that = this;  // 首先从缓存中获取, 如果缓存中有就直接返回  var items = wx.getStorageSync('items');  if (items) {      return typeof cb == 'function' && cb(items);  } else {    // 如果缓存中没有, 就通过hotapp.searchkey去云后台获取      var filters = {          prefix: hotapp.getPrefix('item'),          pageSize: -1   // -1表示不需要分页, 一次性获取所有的数据      };      hotapp.searchkey(filters, function(res) {          if (res.ret == 0) {              res.data.items.forEach(function(item) {                  // 把获取的数据格式稍作处理, 以便在前端显示                  item = that.formatItem(item);                     // state: 1表示未同步, 2表示已同步, 3表示本地已删除.                  // 数据同步的内容在下面介绍                  item.state = 2;                    });            // 为了提高速度, 把数据都存入缓存, 下次取的时候就直接从缓存中获取而不需要从云后台获取              wx.setStorageSync('items', res.data.items);                return typeof cb == 'function' && cb(res.data.items);          } else {              return typeof cb == 'function' && cb([]);          }      });  }},
  • 修改/新增笔记

           /*** 新增数据*/store: function(item, cb) {  var that = this;   // 先调用获取所有的笔记的接口  this.getItems(function(items) {    // 每次新增笔记的时候, 首先更新version, 以便同步. 关于同步, 下面再介绍      that.updateVersion(function(success) {          if (success) {             // 版本号更新好了之后, 就调用hotapp.post接口把笔记上传到云后台              hotapp.post(item.key, item.value, function(res) {                  if (res.ret == 0) {                      item = res.data;                      item.state = 2;   // 如果上传成功了就把该笔记的状态设置为"已同步"                  } else {                      item.state = 1;   // 如果上传失败了, 就把该笔记的状态设置为"未同步"                  }                 // 把新增的笔记的数据格式稍作处理, 以便在前端显示                  item = that.formatItem(item);                // 遍历所有的笔记, 判断这条笔记是新增加的还是经过修改过的                  var isNew = true;                  items.forEach(function(oldItem, index, arr) {                      if (oldItem.key == item.key) {                          arr[index] = item;                          isNew = false;                      }                  });                  if (isNew) {                      // 向hotapp统计发送新增事件,可知道用户每天新增次数                      hotapp.onEvent('new');                       // 新增的笔记就push到原来的笔记中, 并且按照创建时间排序                      items.push(item);                      items.sort(function(a, b) {                          return a.create_time < b.create_time;                      });                  } else {                      // 向hotapp统计发送保存事件,可知道用户每天保存次数                      hotapp.onEvent('store');                   }                 // 最后, 不论是新增的还是修改的笔记, 全部存入缓存中                  wx.setStorageSync('items', items);                  return typeof cb == 'function' && cb(true);              });          }      });  });},
  • 删除笔记

           /*** 删除数据*/destroy: function(item, cb) {  // 向hotapp统计发送删除事件,后台可知晓用户删除了哪些标题  hotapp.onEvent('delete', item.value.title);  var that = this;  // 先获取所有笔记  this.getItems(function(items) {      // 每次删除, 都需要更新版本号, 以便数据同步      that.updateVersion(function(success) {          if (success) {              // 版本号更新好了之后, 就调用delete接口删除云后台的数据              hotapp.del(item.key, function(res) {                 // 暂时可以先不删除本地数据, 直接修改state的状态为"已删除",                 // 然后存入数据库, 等到数据同步的时候再统一删除本地数据                  items.forEach(function(oldItem, index, arr) {                      if (oldItem.key == item.key) {                          oldItem.state = 3;                          wx.setStorageSync('items', arr);                          return typeof cb == 'function' && cb(true);                      }                  });              });          }      });  });},

2, 数据同步的设计

这个项目里面最复杂的就是数据同步的问题了.

在开发的时候, 遇到了下面几个问题:

  1. 每次同步的时候都要先请求云后台所有的数据, 然后和本地缓存数据来对比, 这样不仅会给云后台服务器增加压力, 也会浪费用户的很多流量

  2. 在和服务器的数据进行对比的时候, 不知道哪些数据是同步过的, 哪些数据是未同步的, 哪些数据是删除过的

  3. 用户第一次打开hotapp云笔记时是离线状态, 这时调用hotapp.login接口之后是获取不到用户的openid的, 用户新建的笔记数据中的key是和fakeOpenID相关的, 当用户打开网络的时候, hotapp.login获取到openid之后, 需要把本地的fakeOpenID全部替换成openid, 再把数据上传到服务器

解决方案:

  1. 在hotapp云后台添加一个key, 叫做version, 本地的version应该和服务器的version同步.
    本地每次做增删改操作的时候, 都要先更新本地缓冲中的version, 再更新服务器上的version, 确保本地和服务器上的version一致
    每次同步之前, 先检查本地的version和云后台的version是否一致, 如果一致, 就不需要把服务器上所有的数据
    如果同时有两台设备, 这两台设备的缓存中的version是一致的, 那么设备A做增删改操作时, 设备A缓存中的version和服务器上的version都会更新, 这时服务器上的version和设备B缓存中的version是不一致的, 所以设备B同步的时候就需要把服务器上所有的数据拉下来同步.

  2. 给本地数据的每一条记录都添加一个字段: state, state为1表示未同步, 2表示已同步, 3表示已删除
    在和服务器的数据进行对比的时候, 分7种情况考虑:

    1. 对于一条数据, 本地缓存中有记录且state为1, 服务器上也有记录
    2. 对于一条数据, 本地缓存中有记录且state为2, 服务器上也有记录
    3. 对于一条数据, 本地缓存中有记录且state为3, 服务器上也有记录
    4. 对于一条数据, 本地缓存中有记录且state为1, 服务器上没有记录
    5. 对于一条数据, 本地缓存中有记录且state为2, 服务器上没有记录
    6. 对于一条数据, 本地缓存中有记录且state为3, 服务器上没有记录
    7. 对于一条数据, 本地缓存中没有记录, 服务器上有记录

    仔细分析后, 可以归纳成下面的逻辑:

    1. 用服务器上的数据把本地的数据覆盖. 遍历的时候以服务器的数据为准, 来对比本地数据, 如果发现某一条笔记在本地没有, 就直接新增, 如果有, state为1就就改为2, state为2的不变, state为3的就从缓存中删除
    2. 遍历本地缓存, 如果state为1, 就上传到服务器并设置state为2, 如果state为3, 就调用delete接口删除服务器的数据和本地的数据

    下面是具体的实现, 其中函数syncLocalDataToServer()和函数syncServerDatatoLocal()分别实现了上述的逻辑1和逻辑2:

    /**  * 下拉刷新事件, 数据同步  */ onPullDownRefresh: function() {   wx.showToast({     title: '正在同步数据',     icon: 'loading'   });   // 临时变量   var tempData = this.data.items;   var that = this;   // 先检查版本, 如果和服务器版本不同, 则需要从服务器拉取数据   app.checkVersion(function(shouldPullData) {     // 如果返回true, 则表示本地的version和服务器的version不一致     // 需要把服务器上所有数据都拉下来对比     if (shouldPullData) {       var filters = {         prefix: app.globalData.hotapp.getPrefix('item')       };       // 从服务器拉取所有数据       app.globalData.hotapp.searchkey(filters, function(res) {         if (res.ret == 0) {           // 拉取成功, 更新版本号           app.updateVersion(function(success) {             if (success) {               // 更新版本号之后把本地数据和服务器数据合并去重(逻辑1)               tempData = that.syncServerDatatoLocal(tempData, res.data.items);               tempData.forEach(function(item, index, arr) {                 arr[index] = app.formatItem(item);                 arr[index].state = 2;               });               // 更新视图数据               that.setData({                 items: tempData               });               // 把合并好的数据存缓存               wx.setStorageSync('items', tempData);               // 把本地数据上传到服务器/从服务器删除数据(逻辑2)               that.syncLocalDataToServer(tempData);             }           });         }       });      } else {       // 版本号和服务器相同, 则不需要从服务器上拉取数据, 直接同步数据到服务器       that.syncLocalDataToServer(tempData);     }   }); }, /**  * 将本地数据同步到服务器  */ syncLocalDataToServer: function(data) {   var that = this;   // 遍历所有的数据   data.forEach(function(item, index, items) {     app.globalData.hotapp.replaceOpenIdKey(item.key, function(newKey) {       if (newKey) {         item.key = newKey;         // 如果还有数据没有同步过, 则调用post接口同步到服务器         if (item.state == 1) {           app.globalData.hotapp.post(item.key, item.value, function(res) {             if (res.ret == 0) {               // 同步成功后更新状态, 并存缓存               item.state = 2;               item = app.formatItem(item);               that.setData({                 items: items               });               wx.setStorageSync('items', items);             }           });         }         // 如果数据被删除过, 则调用delete接口从服务器删除数据         if (item.state == 3) {           app.globalData.hotapp.del(item.key, function(res) {             if (res.ret == 0 || res.ret == 103) {               // 服务器的数据删除成功后, 删除本地数据并更新缓存               items.splice(index, 1);               that.setData({                 items: items               });               wx.setStorageSync('items', items);             }           });         }       } else {         return;       }     })   }); }, /**  * 将服务器的数据同步到本地  */ syncServerDatatoLocal: function(localData, serverData) {   var that = this;   // 通过hash的性质去重, 服务器数据覆盖本地数据   // 但是要保留本地中状态为已删除的数据   // 删除的逻辑不在这里处理   var localHash = new Array();   localData.forEach(function(item) {     localHash[item.key] = item;   });   var serverHash = new Array();   serverData.forEach(function(item) {     serverHash[item.key] = item;   });   // 先把服务器上有的数据但是本地没有的数据合并   serverData.forEach(function(item) {     var t = localHash[item.key];     // 有新增的数据     if (!t) {       localHash[item.key] = item;     }     // 有相同的key则以服务器端为准     if (t && t.state != 3) {       item.state = 2;       item = app.formatItem(item);       localHash[item.key] = item;     }   });   // 然后再删除本地同步过的但是服务器上没有的缓存数据(在其它设备上删除过了)   localData.forEach(function(item, index, arr) {     var t = serverHash[item.key];     if (!t && item.state == 2) {       console.log(item);       delete localHash[item.key];     }   });   // 将hash中的数据转换回数组   var result = new Array();   for (var prob in localHash) {     result.push(localHash[prob]);   }   // 按时间排序   result.sort(function(a, b) {     return a.create_time < b.create_time;   });   console.log(result);   return result; }

0 0
原创粉丝点击