背景

业务模块分开,整体业务流由多个单页面组成,如:还款途中,需要绑卡,设置交易密码,或者还需要修改手机号码,最后一系列操作后回到还款页面提交还款,整个流程在多个页面间跳转,单页面的数据无法得到保存,故需要一套多页面的数据缓存方案。

数据保存

考虑浏览器的一般通用性,将数据缓存在sessionStorage中。

旧方案

每次触发页面跳转时,手动调用window.sessionStorage.setItem保存需要保存的数据,在页面跳转回来的时候,读取sessionStorage中的数据,将其赋给需要覆盖的变量,然后触发对应的初始化操作。现有这种方案存在以下一些不足:

  • 每个页面都需要编写storeData和restoreData的逻辑,分散的逻辑不易管理;
  • 如果引用了组件,组件自身维护了state的话,子组件需要被缓存的数据需要暴露给父组件才能统一在父组件中触发storeData逻辑,而restoreData的得到的数据也需要传递到子组件才能进行相应的赋值操作,同时要考虑组件渲染的时机,以及数据请求回来的时机之间的先后顺序

优化点

  • 需要一个统一的数据保存对象
  • 需要一个统一的数据管理入口
  • 统一的数据保存对象是为了,跳转前的直接缓存这个对象,后续跳转回来,也直接读取这个对象,并且全局都能访问到这个对象,即能优化旧方案的第二个点,减少父子组件间的数据传输;
  • 统一的数据管理入口,是为了方便管理数据缓存的逻辑,同时避免重复写代码;

改进方案

全局对象GlobalStore

  • 针对上述的几点,首先是定义一个globalStore的对象,然后export出来,各个需要缓存的页面就直接import这个对象。
  • 同时为了避免不同页面间的命名污染,需要给每个页面赋予一个pageId,并设为globalstore的key值,故最终GlobalStore格式如:GlobalStorepageId = value。
  • 如何标记需要被缓存的数据?考虑一般涉及展示的都是state中的数据,故直接将state保存下来,另外组件的props一般会变化的也是由父组件的state传递下来的,所以props不需要额外保存,故最终选取保存的数据是state里的数据。

数据管理入口

  • 由于业务代码中会修改state的状态,在最后页面跳转时,如何同步当前的state是需要解决的问题,通过实例this可以获取当前的state,但如果当前页面用到子组件,子组件中的state也需要被缓存,那也需要获取子组件的实例,从而获取他的state,这就会使数据管理很困难,整体缓存脉络也很不清晰,所以思路是是否能在更改state的同时,同步更新全局的GlobalStore;
  • 一开始是想要采取类似vue的作法,利用Object.definedproperty,写一个set的拦截器,但后来发现setstate并不是简单的通过this.state[key] = newValue来修改数据,而是整体替换,所以这个方案行不通;
  • 第二个方案就是直接改写React.component原型上的setState,在其上添加同步更新GlobalStore的代码,但并不是每个页面都需要缓存,改写原型会使其余不需要缓存的页面页回去修改GlobalStore,当然这里也可以根据当前实例的pageId来判断是否要写缓存来规避这个问题,但本着尽量不改变react逻辑的原则,采用了另一种方案;
  • 使用装饰器,也就是高阶组件,可以实现反向代理,同时对业务代码的侵入性更小,只需要在每个用到的页面的类上加一个装饰器并传入pageid作为参数就可以开启缓存策略;
  • 另外利用反向代理,可以获取到父类state上的所有属性,通过super.render()可以进行渲染,并通过给子类添加setState属性,覆盖原型上的setState方法,同时在添加的setState方法上调用super.setState来保证数据能得到正确更新,以及触发视图渲染;
  • 然后在添加的setState上获取到更新的对象,写入GlobalStore上,但是考虑到setState除了可以传入一个对象进行更新,还可以传入一个函数进行更新,而函数会接受前一个状态的state和props为参数,而如果同时调用了两次setState,虽然时批量更新,state不会马上修改值,但后一次setState中的preState是前一个setState修改后的state结果,这种逻辑下,要获取正确的state对象会比较复杂且容易出错,所以需要对传入的数据进行一下包装,再setState中声明一个函数,以arguments作为入参,内部执行一遍传入的func,入参依旧是arguments,记录返回值,分析更新了哪个key,对应修改进GlobalStore,最后返回这个返回值,并把这个内部声明的函数传入super.setState中,这样内部执行修改方法时也会修改到GlobalStore对象;
  • 至此利用装饰器反向代理,我们实现了在constructor阶段,将数据回填覆盖state,同时用自己实现的setState拦截react.component原型上的setState实现数据同步,最后每次产生外部跳转的时候,调用一个通用的跳转方法,在跳转的同时,将GlobalStore写入sessionStorage,这样回来初始化页面调用构造函数时就可以回填数据,整个多页面数据缓存的方案就大体如此。

优化

考虑实际的应用场景,还有以下几个地方可以做优化:

公共字段

  • 内部页面之间有可能使用了相同的变量,此时没必要针对每个页面ID都保存一份副本,而应该把这部分数据提取到一个公共的字段下,例如 [common];
  • 那么就需要有手段去识别出,state上的哪些字段是取自公共字段的,这里考虑用一个自定义的class来实现这个功能,因为首先class可以利用instanceOf判断是否属于公共字段,其次一般没有人会在state上设置一个class的实例;
  • 现在暂且称这个class为CommonData,考虑公共字段要用到的属性,给他两个属性,分别是key和value,其中key为对应GlobalStore中的字段名(因为state中的字段名不一定要和公共字段名相同),value为其初始值;
  • 同时对外export一个方法,暂称genCommonData,传入两个参数,并在内部实例化CommonData,返回实例;
  • 这时,装饰器就可以对state中的属性进行判断,哪些是公共字段,哪些是页面独有的字段,在constructor里,就可以进行分别处理,页面独有的字段就如之前那样处理,公共字段要用实例中的value以及GlobalStore中的值进行重新赋值,同时用一个私有变量保存state中公共字段和GlobalStore中公共字段的对应关系;
  • 根据这个对应关系,在后续setState中,会判断其是否为公共字段,从而决定更新[pageId]里的属性,还是[common]中的属性

组件数据缓存

  • 组件的数据缓存也可以用上面的一套逻辑,但考虑到一个组件有可能被多个页面应用,同时还有jsx写法中难以直接使用装饰器,在定义组件的文件中,输出组件的时候,针对会使用缓存的组件,改造时应该额外输出多一种被装饰器装饰的组件作为缓存组件使用;
  • 另外,还是因为组件可能被多页面引用,这里存放在GlobalStore中的数据不能再直接添加一个组件Id做区分,考虑到组件是挂在页面下的,可以在GlobalStore[pageId]下保存一个[componentId]的字段去进行保存,而公共字段依然保存在[common]中;
  • 由于渲染的特性,父组件的constructor会在子组件的constructor之前执行,同时一个时间只可能有一个页面,那么可以在GlobalStore中添加一个currentPageId的属性,去记录当前的页面id,等对应子组件加载时,直接利用currentPageId去对应的字段下赋值,从而让对应页面的数据记录在对应页面下

至此,一个基本能运行的方案就实现了

有待解决的问题

  • 一个页面下引用了多个相同的组件,怎么保存数据? - 利用key
  • 组件嵌套的情况下怎么保存数据? - 只能利用Props指明关系?
  • 多个装饰器时可能有副作用
  • 并不是所有的state都需要被缓存的 - 参考CommonData的做法?
  • 有些需要缓存的数据不是放在state上的,而是直接挂在this上的 - 建议放到state里,或者同样时采用CommonData的做法
  • hook是否可以利用?

该方案对Vue的启发

  • vue多页面也会有同样的问题,vue中无法使用装饰器,但Object.definedPorperty可以利用,在其勾子函数created和beforeMount之间,可以对修改其data上的set方法,使得数据可以同步更新,入口放在mixin;
  • 至于公共对象,由于vue会针对data中的值进行监听,不能采用CommonData的做法,这种可以事先在页面定义一个公共字段的map,传入data时使用解构,修改监听的时候针对这个map做筛选哪些是更新到公共字段;
  • 但更直接的方案是使用Vuex,直接缓存store对象,进入页面进行init的时候就对其进行回填即可。

zengrc
28 声望0 粉丝

« 上一篇
Diff算法