前言:最近承接了一个需求,其中有一步需要提供让用户上传文件。由于公司之前有类似的项目,所以一开始用“拿来主义”处理。后来随着业务逻辑的增加以及错误处理场景的增加,越来越多的代码被耦合到一起,于是萌生了重构上传功能的想法。

抛出一个问题

在常见的上传场景中,一般的业务流程是这样的:未命名文件.png
这是一个常见的流程,但是结合到实际的开发中时,由于存在UI交互和展示上传进度的需求,这个功能实际开发完是这样的:未命名文件 (10).png
这幅图让人看得脑袋生疼,实际上的代码逻辑远比流程图展示的更令人闻风丧胆。具体表现为:

  1. 很多代码包括功能是重复的,如果你要修改其中一处,最好有足够好的运气能找到另一处,否则很难保证业务流程的正确性。
  2. 系统运行时入口很多,维护困难。
  3. 基本没有可拓展性。
  4. 系统运行结果和过程不可预期。

在测试提了多次例如:重试时进度条闪烁、上传失败后没有UI提示等问题后,我在一个周五的深夜决定:重构该系统。

重构过程

要重构这部分代码,毫无疑问,第一步是对整个系统的功能进行划分。经过思考,这个系统可以划分为几个部分:
未命名文件 (4).png
用户的操作绑定在onClick事件中;异常情况通过对XHR的监听以及window.onoffline的监听;UI展示使用useState更新到DOM中。那么最复杂的部分存在于上传功能中,那么对于这个上传功能,我们希望将复杂的内部处理过程隐藏起来,只向外提供以下接口:

  1. onSuccess:上传成功的回调处理
  2. onError:上传失败的错误处理
  3. onProcess:上传过程中处理
  4. onRestart:当开始重新上传时,对DOM进行更新
  5. start:开始上传
  6. restart:重试接口
  7. abort:主动注销上传

经过对上传功能的抽象,业务流程会变成这样:
未命名文件 (9).png
经过抽象后,上传功能的主要过程都由内部处理,仅对外暴露控制和监听上传过程的接口。这样带来了几个好处:

  1. 系统的入口是不变的,但是每一个入口都可以直接指到对应的接口,减少了流程的复杂性。
  2. 上传进度等复杂的计算过程都由类内部处理,仅提供计算的结果,外部只需获取结果直接渲染。
  3. 类内部可以对方法互相调用,使用者只需要关心其接口最终功能。

经过以上的分析,使用OOP进行封装是最适合的选项。当然,实际实现时,上传功能内部需要对过程进行进一步解构:

未命名文件 (8).png
上传功能内部维护了this.XHR存储一个XMLHttpRequest对象,以及一个this.uploadState的上传状态对象,当开始上传的时候不断更新该对象,同时在遇到错误或者abort、restart的时候主动清空该对象。这样可以确保请求被抛弃后重新开始时状态为初始状态,避免了进度条闪烁等问题。
同时,当外界调用restart方法时,会自动触发onRestart事件。在业务中,触发restart的场景较多,这时外界通过该回调绑定了更新DOM的操作即可,可以继续根据业务需要在其他入口操作restat。

One More Thing…

在React中,因为组件会不断更新自己,而我们是在组件内初始化上传对象的。为了避免由于组件更新导致上传对象不断被重新实例化,我们需要使用单例模式来创建对象。

  1. class Uploader{
  2. private xhr:XMLHttpRequest;
  3. private uploadState:IUploadState;
  4. public onSuccess:()=>void;
  5. public onError:(error:Error)=>void;
  6. ...
  7. //单例模式实现
  8. static instance:Uploader;
  9. static getInstance(){
  10. if(!Uploader.instace){
  11. Uploader.instance = new Uploader();
  12. }
  13. return Uploader.instance;
  14. }
  15. }
  16. //在React中
  17. const UploadPage:React.FC = ()=>{
  18. const file = useSelector()... // 获取文件
  19. const uploader = Uploader.getInstance(); // 获取Uploader实例
  20. useEffect(()=>{
  21. // 在Effect中注册监听函数,否则会不断注册
  22. uploader.onProcess(({restTime})=>{
  23. setRestTime(restTime);
  24. })
  25. },[file])
  26. }

总结

  1. 在需求确定后可以使用优秀的设计模式构建代码,前提是:需求确定了。而大部分敏捷开发在开发过程中需求是变动的。
  2. 虽然JS大部分开发过程是面向过程的,但是也不乏可以应用OOP的地方,需要用心去发掘。