深入浅出TypeScript在Model中的高级应用

开发 前端
在MVC、MVVC等前端经典常用开发模式中,V、C往往是重头戏,可能是前端业务主要集中这两块。结合实际业务,笔者更喜欢路由模式、插件式设计,这种在迭代和维护上更能让开发者受益。但我们今天来看看Model,看看M有什么扩展的可能。

[[330492]]

前言

在MVC、MVVC等前端经典常用开发模式中,V、C往往是重头戏,可能是前端业务主要集中这两块。结合实际业务,笔者更喜欢路由模式、插件式设计,这种在迭代和维护上更能让开发者受益(不过你需要找PM协调这事,毕竟他们理解的简化用户体验,多半是怎么让用户操作简单)。但我们今天来看看Model,看看M有什么扩展的可能。

背景

在读到本文之前,你实际项目(如React+Redux)中请求服务器数据,可能是如下策略:

  1. componentDidMount 中发送redux action请求数据;
  2. 在action中发起异步网络请求,当然你已经对网络请求有一定封装;
  3. 在网络请求内部处理一定异常和边际逻辑,然后返回请求到的数据;
  4. 拿到数据this.setState刷新页面,同时可能存一份到全局redux中;

正常情况下,一个接口对应至少一个接口相应Model,万一你还定义了接口请求的Model、一个页面有5个接口呢?

如果项目已经引入TypeScript,结合编写Model,你的编写体验肯定会如行云流水般一气呵成!但实际开发中,你还需要对服务器返回的数据、页面间传递的参数等涉及到数据传递的地方,做一些数据额外工作:

  • 对null、undefined等空值的异常处理(在ES最新方案和TS支持里,新增:链式调用?和运算符??,请读者自行查询使用手册);
  • 对sex=0、1、2,time=1591509066等文案转义;
  • (还有其他吗?欢迎留言补充)

作为一个优秀且成熟的开发者,你肯定也已经做了上述额外的工作,在utils文件下编写了几十甚至上百的tool类函数,甚至还根据函数用途做了分类:时间类、年龄性别类、数字类、......,接着你在需要的地方import,然后你开始进行传参调用。是的,一切看上去都很完美!

上面这个流程说的就是笔者本人,:)。

现况

随着项目和业务的迭代,加上老板还是压时间,最坏的情况是你遇到了并没有遵守上述"开发规范"的同事,那结果只能是呵呵呵呵呵了。下面直接切入正题吧!

上述流程虽说有一定设计,但没有做到高内聚、低耦合的原则,个人觉得不利于项目后期迭代和局部重构。

推荐另一个设计原则:面向对象五大原则SOLID[3]

下面举个例子:

  • 接口里字段发生变更时,如性别从Sex改为Gender;
  • 前端内部重构,发现数据模型不匹配时,页面C支持从页面A附加参数a、或页面B附加参数b跳入,重构后页面B1附加参数b1也要跳转C。从设计来说肯定是让B1尽量按照以前B去适配时是最好的,否则C会越来越重。

上面提过不管是页面交互,还是业务交互,最根本根本是数据的交换传递,从而去影响页面和业务。数据就是串联页面和业务的核心,Model就是数据的表现形式。

再比如现在前后端分离的开发模式下,在需求确认后,开发需要做的第一件事是数据库设计和接口设计,简单的说就是字段的约定,然后在进行页面开发,最终进行接口调试和系统调试,一直到交付测试。这期间,后端需要执行接口单元测试、前端需要Mock数据开发页面。

如何解决

接口管理

目前笔记是通过JSON形式来进行接口管理,在项目初始化时,将配置的接口列表借助于 dva[4] 注册到Redux Action中,然后接口调用就直接发送Action即可。最终到拿到服务器响应的Data。

接口配置(对应下面第二版):

  1. list: [ 
  2.   { 
  3.     alias: 'getCode'
  4.     apiPath: '/user/v1/getCode'
  5.     auth: false
  6.   }, 
  7.   { 
  8.     alias: 'userLogin'
  9.     apiPath: '/user/v1/userLogin'
  10.     auth: false
  11.     nextGeneral: 'saveUserInfo'
  12.   }, 
  13.   { 
  14.     alias: 'loginTokenByJVerify'
  15.     apiPath: '/user/v1/jgLoginApi'
  16.     auth: false
  17.     nextGeneral: 'saveUserInfo'
  18.   }, 

第一版:

  1. import { apiComm, apiMy } from 'services'
  2.  
  3. export default { 
  4.   namespace: 'bill'
  5.   state: {}, 
  6.   reducers: { 
  7.     updateState(state, { payload }) { 
  8.       return { ...state, ...payload }; 
  9.     }, 
  10.   }, 
  11.   effects: { 
  12.     *findBydoctorIdBill({ payload, callback }, { call }) { 
  13.       const res = yield call(apiMy.findBydoctorIdBill, payload); 
  14.       !apiComm.IsSuccess(res) && callback(res.data); 
  15.     }, 
  16.     *findByDoctorIdDetail({ payload, callback }, { call }) { 
  17.       const res = yield call(apiMy.findByDoctorIdDetail, payload); 
  18.       !apiComm.IsSuccess(res) && callback(res.data); 
  19.     }, 
  20.     *findStatementDetails({ payload, callback }, { call }) { 
  21.       const res = yield call(apiMy.findStatementDetails, payload); 
  22.       !apiComm.IsSuccess(res) && callback(res.data); 
  23.     }, 
  24.   }, 
  25. }; 

第二版使用高阶函数,同时支持服务器地址切换,减少冗余代码:

  1. export const connectModelService = (cfg: any = {}) => { 
  2.   const { apiBase = '', list = [] } = cfg; 
  3.   const listEffect = {}; 
  4.   list.forEach(kAlias => { 
  5.     const { alias, apiPath, nextGeneral, cbError = false, ...options } = kAlias; 
  6.     const effectAlias = function* da({ payload = {}, nextPage, callback }, { call, put }) { 
  7.       let apiBaseNew = apiBase; 
  8.       // apiBaseNew = urlApi; 
  9.       if (global.apiServer) { 
  10.         apiBaseNew = global.apiServer.indexOf('xxx.com') !== -1 ? global.apiServer : apiBase; 
  11.       } else if (!isDebug) { 
  12.         apiBaseNew = urlApi; 
  13.       } 
  14.       const urlpath = 
  15.         apiPath.indexOf('http://') === -1 && apiPath.indexOf('https://') === -1 ? `${apiBaseNew}${apiPath}` : apiPath; 
  16.       const res = yield call(hxRequest, urlpath, payload, options); 
  17.       const next = nextPage || nextGeneral; 
  18.       // console.log('=== hxRequest res'next, res); 
  19.       if (next) { 
  20.         yield put({ 
  21.           type: next
  22.           payload, 
  23.           res, 
  24.           callback, 
  25.         }); 
  26.       } else if (cbError) { 
  27.         callback && callback(res); 
  28.       } else { 
  29.         hasNoError(res) && callback && callback(res.data); 
  30.       } 
  31.     }; 
  32.     listEffect[alias] = effectAlias; 
  33.   }); 
  34.   return listEffect; 
  35. }; 

上面看上去还不错,解决了接口地址管理、封装了接口请求,但自己还得处理返回Data里的异常数据。

另外的问题是,接口和对应的请求与相应的数据Model并没有对应起来,后面再次看代码需要一段时间才能梳理业务逻辑。

请读者思考一下上面的问题,然后继续往下看。

Model管理

一个接口必然对应唯一一个请求Model和唯一一个响应Model。对,没错!下面利用此机制进一步讨论。

所以通过响应Model去发起接口请求,在函数调用时也能利用请求Model判定入参合不合理,这样就把主角从接口切换到Model了。这里个人觉得优先响应Model比较合适,更能直接明白这次请求后拿到的数据格式。

下面先看看通过Model发起请求的代码:

  1. SimpleModel.get( 
  2.   { id: '1' }, 
  3.   { auth: false, onlyData: false }, 
  4. ).then((data: ResponseData<SimpleModel>) => 
  5.   setTimeout( 
  6.     () => 
  7.       console.log( 
  8.         '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>'
  9.         typeof data, 
  10.         data, 
  11.       ), 
  12.     2000, 
  13.   ), 
  14. ); 

其中,SimpleModel是定义的响应Model,第一个参数是请求,第二个参数是请求配置项,接口地址被隐藏在SimpleModel内部了。

  1. import { Record } from 'immutable'
  2.  
  3. import { ApiOptons } from './Common'
  4. import { ServiceManager } from './Service'
  5.  
  6. /** 
  7.  * 简单类型 
  8.  */ 
  9. const SimpleModelDefault = { 
  10.   a: 'test string'
  11.   sex: 0, 
  12. }; 
  13.  
  14. interface SimpleModelParams { 
  15.   id: string; 
  16.  
  17. export class SimpleModel extends Record(SimpleModelDefault) { 
  18.   static async get(params: SimpleModelParams, options?: ApiOptons) { 
  19.     return await ServiceManager.get<SimpleModel>( 
  20.       SimpleModel, 
  21.       'http://localhost:3000/test',   // 被隐藏的接口地址 
  22.       params, 
  23.       options, 
  24.     ); 
  25.   } 
  26.  
  27.   static sexMap = { 
  28.     0: '保密'
  29.     1: '男'
  30.     2: '女'
  31.   }; 
  32.  
  33.   sexText() { 
  34.     return SimpleModel.sexMap[this.sex] ?? '保密'
  35.   } 

这里借助了immutable里的Record[5],目的是将JSON Object反序列化为Class Object,目的是提高Model在项目中相关函数的内聚。更多介绍请看我另外一篇文章:JavaScript的强语言之路—另类的JSON序列化与反序列化[6]

  1. // utils/tool.tsx 
  2. export const sexMap = { 
  3.   0: '保密'
  4.   1: '男'
  5.   2: '女'
  6. }; 
  7.  
  8. export const sexText = (sex: number) => { 
  9.   return sexMap[sex] ?? '保密'
  10. }; 

直接在SimpleModel内部用this访问具体数据,比调用utils/tool函数时传入外部参数,更为内聚和方便维护。通过这种思路,相信你可以创造更多"黑魔法"的语法糖!

接着我们来看看 Common 文件内容:

  1. /** 
  2.  * 接口响应,最外层统一格式 
  3.  */ 
  4. export class ResponseData<T = any> { 
  5.   code? = 0; 
  6.   message? = '操作成功'
  7.   toastId? = -1; 
  8.   data?: T; 
  9.  
  10. /** 
  11.  * api配置信息 
  12.  */ 
  13. export class ApiOptons { 
  14.   headers?: any = {}; // 额外请求头 
  15.   loading?: boolean = true; // 是否显示loading 
  16.   loadingTime?: number = 2; // 显示loading时间 
  17.   auth?: boolean = true; // 是否需要授权 
  18.   onlyData?: boolean = true; // 只返回data 
  19.  
  20. /** 
  21.  * 枚举接口能返回的类型 
  22.  * - T、T[] 在 ApiOptons.onlyData 为true时是生效 
  23.  * - ResponseData<T>、ResponseData<T[]> 在 ApiOptons.onlyData 为false时是生效 
  24.  * - ResponseData 一般在接口内部发生异常时生效 
  25.  */ 
  26. export type ResultDataType<T> = 
  27.   | T 
  28.   | T[] 
  29.   | ResponseData<T> 
  30.   | ResponseData<T[]> 
  31.   | ResponseData; 

Service文件内部是封装了axios:

  1. import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
  2. import { ApiOptons, ResponseData, ResultDataType } from './Common'
  3.  
  4. /** 
  5.  * 模拟UI loading 
  6.  */ 
  7. class Toast { 
  8.   static loading(txt: string, time: number = 3) { 
  9.     console.log(txt, time); 
  10.     return 1; 
  11.   } 
  12.   static info(txt: string, time: number = 3) { 
  13.     console.log(txt, time); 
  14.     return 1; 
  15.   } 
  16.   static remove(toastId: number) { 
  17.     console.log(toastId); 
  18.   } 
  19.  
  20. /** 
  21.  * 未知(默认)错误码 
  22.  */ 
  23. const codeUnknownTask = -999; 
  24.  
  25. /** 
  26.  * 接口请求封装基类 
  27.  */ 
  28. export class InterfaceService { 
  29.   /** 
  30.    * todo 
  31.    */ 
  32.   private static userProfile: { sysToken?: '' } = {}; 
  33.   public static setUser(_user: any) { 
  34.     InterfaceService.userProfile = _user; 
  35.   } 
  36.  
  37.   constructor(props: ApiOptons) { 
  38.     this.options = props; 
  39.   } 
  40.   /** 
  41.    * 默认配置 
  42.    */ 
  43.   public options = new ApiOptons(); 
  44.  
  45.   /** 
  46.    * todo 
  47.    */ 
  48.   public get sysToken(): string { 
  49.     return InterfaceService.userProfile?.sysToken ?? ''
  50.   } 
  51.  
  52.   /** 
  53.    * 构建header 
  54.    */ 
  55.   public get headers(): Object { 
  56.     return { 
  57.       Accept: 'application/json'
  58.       'Content-Type''application/json; charset=utf-8'
  59.       'app-info-key''xxx', // 自定义字段 
  60.     }; 
  61.   } 
  62.  
  63.   /** 
  64.    * 请求前置条件。可根据自己情况重构此函数 
  65.    */ 
  66.   preCheck() { 
  67.     if (this.options.loading && this.options.loadingTime > 0) { 
  68.       return Toast.loading('加载中...', this.options?.loadingTime ?? 3); 
  69.     } 
  70.     return -1; 
  71.   } 
  72.  
  73.   /** 
  74.    * 下载json,返回对象 
  75.    */ 
  76.   public static async getJSON(url: string) { 
  77.     try { 
  78.       const res = await fetch(url); 
  79.       return await res.json(); 
  80.     } catch (e) { 
  81.       console.log(e); 
  82.       return {}; 
  83.     } 
  84.   } 
  85.  
  86. /** 
  87.  * 接口请求封装(axios版,也可以封装其他版本的请求) 
  88.  */ 
  89. export class InterfaceAxios extends InterfaceService { 
  90.   constructor(props: ApiOptons) { 
  91.     super(props); 
  92.   } 
  93.  
  94.   /** 
  95.    * 封装axios 
  96.    */ 
  97.   private request = (requestCfg: AxiosRequestConfig): Promise<ResponseData> => { 
  98.     return axios(requestCfg) 
  99.       .then(this.checkStatus) 
  100.       .catch((err: any) => { 
  101.         // 后台接口异常,如接口不通、http状态码非200、data非json格式,判定为fatal错误 
  102.         console.log(requestCfg, err); 
  103.         return { 
  104.           code: 408, 
  105.           message: '网络异常'
  106.         }; 
  107.       }); 
  108.   }; 
  109.  
  110.   /** 
  111.    * 检查网络响应状态码 
  112.    */ 
  113.   private checkStatus(response: AxiosResponse<ResponseData>) { 
  114.     if (response.status >= 200 && response.status < 300) { 
  115.       return response.data; 
  116.     } 
  117.     return { 
  118.       code: 408, 
  119.       message: '网络数据异常'
  120.     }; 
  121.   } 
  122.  
  123.   /** 
  124.    * 发送POST请求 
  125.    */ 
  126.   public async post(url: string, data?: any) { 
  127.     const toastId = this.preCheck(); 
  128.     const ret = await this.request({ 
  129.       url, 
  130.       headers: this.headers, 
  131.       method: 'POST'
  132.       data: Object.assign({ sysToken: this.sysToken }, data), 
  133.     }); 
  134.     ret.toastId = toastId; 
  135.  
  136.     return ret; 
  137.   } 
  138.  
  139.   /** 
  140.    * 发送GET请求 
  141.    */ 
  142.   public async get(url: string, params?: any) { 
  143.     const toastId = this.preCheck(); 
  144.     const ret = await this.request({ 
  145.       url, 
  146.       headers: this.headers, 
  147.       method: 'GET'
  148.       params: Object.assign({ sysToken: this.sysToken }, params), 
  149.     }); 
  150.     ret.toastId = toastId; 
  151.     return ret; 
  152.   } 
  153.  
  154. export class ServiceManager { 
  155.   /** 
  156.    * 检查接口数据 
  157.    */ 
  158.   public hasNoError(res: ResponseData) { 
  159.     if (res.toastId > 0) { 
  160.       Toast.remove(res.toastId); 
  161.     } 
  162.     if (res?.code !== 0 && res.code !== codeUnknownTask) { 
  163.       Toast.info(res?.message ?? '服务器出错'); 
  164.       return false
  165.     } 
  166.     return true
  167.   } 
  168.  
  169.   /** 
  170.    * 解析响应 
  171.    */ 
  172.   public static parse<T>( 
  173.     modal: { new (x: any): T }, 
  174.     response: any
  175.     options: ApiOptons, 
  176.   ): ResultDataType<T> { 
  177.     if (!response || !response.data) { 
  178.       response.data = new modal({}); 
  179.     } else { 
  180.       if (response.data instanceof Array) { 
  181.         response.data = response.data.map((item: T) => new modal(item)); 
  182.       } else if (response.data instanceof Object) { 
  183.         response.data = new modal(response.data); 
  184.       } 
  185.       return options.onlyData ? response.data : response; 
  186.     } 
  187.   } 
  188.  
  189.   /** 
  190.    * post接口请求 
  191.    */ 
  192.   public static async post<T>( 
  193.     modal: { new (x: any): T }, 
  194.     url: string, 
  195.     body?: any
  196.     options: ApiOptons = new ApiOptons(), 
  197.   ): Promise<ResultDataType<T>> { 
  198.     // 使用合并,减少外部传入配置 
  199.     options = Object.assign(new ApiOptons(), options); 
  200.  
  201.     const request = new InterfaceAxios(options); 
  202.     if (options.auth && !request.sysToken) { 
  203.       return { 
  204.         code: 403, 
  205.         message: '未授权'
  206.       }; 
  207.     } 
  208.  
  209.     try { 
  210.       const response = await request.post(url, body); 
  211.       return ServiceManager.parse<T>(modal, response, options); 
  212.     } catch (err) { 
  213.       // 记录错误日志 
  214.       console.log(url, body, options, err); 
  215.       return { 
  216.         code: codeUnknownTask, 
  217.         message: '内部错误,请稍后再试'
  218.       }; 
  219.     } 
  220.   } 
  221.  
  222.   /** 
  223.    * get接口请求 
  224.    */ 
  225.   public static async get<T>( 
  226.     modal: { new (x: any): T }, 
  227.     url: string, 
  228.     params?: any
  229.     options: ApiOptons = new ApiOptons(), 
  230.   ): Promise<ResultDataType<T>> { 
  231.     // 使用合并,减少外部传入配置 
  232.     options = Object.assign(new ApiOptons(), options); 
  233.  
  234.     const a = new InterfaceAxios(options); 
  235.     const request = new InterfaceAxios(options); 
  236.     if (options.auth && !request.sysToken) { 
  237.       return { 
  238.         code: 403, 
  239.         message: '未授权'
  240.       }; 
  241.     } 
  242.  
  243.     try { 
  244.       const response = await a.get(url, params); 
  245.       return ServiceManager.parse<T>(modal, response, options); 
  246.     } catch (err) { 
  247.       // 记录错误日志 
  248.       console.log(url, params, options, err); 
  249.       return { 
  250.         code: codeUnknownTask, 
  251.         message: '内部错误,请稍后再试'
  252.       }; 
  253.     } 
  254.   } 

Service文件里内容有点长,主要有下面几个类:

  • Toast:模拟请求接口时的loading,可通过接口调用时来配置;
  • InterfaceService:接口请求的基类,内部记录当前用户的Token、多环境服务器地址切换(代码中未实现)、单次请求的接口配置、自定义Header、请求前的逻辑检查、直接请求远端JSON配置文件;
  • InterfaceAxios:继承于InterfaceService,即axios版的接口请求,内部发起实际请求。你可以封装fetch版本的。
  • ServiceManager:提供给Model使用的请求类,传入响应Model和对应服务器地址后,等异步请求拿到数据后再将相应数据Data解析成对应的Model。

下面再贴一下完整的Model发起请求示例:

  1. import { ResponseData, ApiOptons, SimpleModel } from './model'
  2.  
  3. // 接口配置不同的三种请求 
  4. SimpleModel.get({ id: '1' }).then((data: ResponseData) => 
  5.   setTimeout( 
  6.     () => 
  7.       console.log( 
  8.         '因需授权导致内部异常,返回 ResponseData:'
  9.         typeof data, 
  10.         data, 
  11.       ), 
  12.     1000, 
  13.   ), 
  14. ); 
  15.  
  16. SimpleModel.get( 
  17.   { id: '1' }, 
  18.   { auth: false, onlyData: false }, 
  19. ).then((data: ResponseData<SimpleModel>) => 
  20.   setTimeout( 
  21.     () => 
  22.       console.log( 
  23.         '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>'
  24.         typeof data, 
  25.         data, 
  26.       ), 
  27.     2000, 
  28.   ), 
  29. ); 
  30.  
  31. SimpleModel.get( 
  32.   { id: '1' }, 
  33.   { auth: false, onlyData: true }, 
  34. ).then((data: SimpleModel) => 
  35.   setTimeout( 
  36.     () => 
  37.       console.log( 
  38.         '仅返回关键数据data,返回 T 或 T[]:'
  39.         typeof data, 
  40.         data, 
  41.         data.sexText(), 
  42.       ), 
  43.     3000, 
  44.   ), 
  45. ); 

控制台打印结果。注意,返回的 data 可能是JSON Object,也可能是 Immutable-js Record Object。

  1. 加载中... 2 
  2. 加载中... 2 
  3. 因需授权导致内部异常,返回 ResponseData:object { code: 403, message: '未授权' } 
  4. 设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]> object { 
  5.   code: 0, 
  6.   message: '1'
  7.   data: SimpleModel { 
  8.     __ownerID: undefined, 
  9.     _values: List { 
  10.       size: 2, 
  11.       _origin: 0, 
  12.       _capacity: 2, 
  13.       _level: 5, 
  14.       _root: null
  15.       _tail: [VNode], 
  16.       __ownerID: undefined, 
  17.       __hash: undefined, 
  18.       __altered: false 
  19.     } 
  20.   }, 
  21.   toastId: 1 
  22. 仅返回关键数据data,返回 T 或 T[]:object SimpleModel { 
  23.   __ownerID: undefined, 
  24.   _values: List { 
  25.     size: 2, 
  26.     _origin: 0, 
  27.     _capacity: 2, 
  28.     _level: 5, 
  29.     _root: null
  30.     _tail: VNode { array: [Array], ownerID: OwnerID {} }, 
  31.     __ownerID: undefined, 
  32.     __hash: undefined, 
  33.     __altered: false 
  34.   } 
  35. } 男
最后再补充一个常见的复合类型Model示例:
  1. /** 
  2.  * 复杂类型 
  3.  */ 
  4.  
  5. const ComplexChildOneDefault = { 
  6.   name'lyc'
  7.   sex: 0, 
  8.   age: 18, 
  9. }; 
  10.  
  11. const ComplexChildTwoDefault = { 
  12.   count: 10, 
  13.   lastId: '20200607'
  14. }; 
  15.  
  16. const ComplexChildThirdDefault = { 
  17.   count: 10, 
  18.   lastId: '20200607'
  19. }; 
  20.  
  21. // const ComplexItemDefault = { 
  22. //   userNo: 'us1212'
  23. //   userProfile: ComplexChildOneDefault, 
  24. //   extraFirst: ComplexChildTwoDefault, 
  25. //   extraTwo: ComplexChildThirdDefault, 
  26. // }; 
  27.  
  28. // 复合类型建议使用class,而不是上面的object。因为object里不能添加可选属性? 
  29. class ComplexItemDefault { 
  30.   userNo = 'us1212'
  31.   userProfile = ComplexChildOneDefault; 
  32.   extraFirst? = ComplexChildTwoDefault; 
  33.   extraSecond? = ComplexChildThirdDefault; 
  34.  
  35. // const ComplexListDefault = { 
  36. //   list: [], 
  37. //   pageNo: 1, 
  38. //   pageSize: 10, 
  39. //   pageTotal: 0, 
  40. // }; 
  41.  
  42. // 有数组的复合类型,如果要指定数组元素的Model,就必须用class 
  43. class ComplexListDefault { 
  44.   list: ComplexItemDefault[] = []; 
  45.   pageNo = 1; 
  46.   pageSize = 10; 
  47.   pageTotal = 0; 
  48.  
  49. interface ComplexModelParams { 
  50.   id: string; 
  51.  
  52. // 因为使用的class,所以需要 new 一个去初始化Record 
  53. export class ComplexModel extends Record(new ComplexListDefault()) { 
  54.   static async get(params: ComplexModelParams, options?: ApiOptons) { 
  55.     return await ServiceManager.get<ComplexModel>( 
  56.       ComplexModel, 
  57.       'http://localhost:3000/test2'
  58.       params, 
  59.       options, 
  60.     ); 
  61.   } 

下面是调用代码:

  1. ComplexModel.get({ id: '2' }).then((data: ResponseData) => 
  2.   setTimeout( 
  3.     () => 
  4.       console.log( 
  5.         '因需授权导致内部异常,返回 ResponseData:'
  6.         typeof data, 
  7.         data, 
  8.       ), 
  9.     1000, 
  10.   ), 
  11. ); 
  12.  
  13. ComplexModel.get( 
  14.   { id: '2' }, 
  15.   { auth: false, onlyData: false }, 
  16. ).then((data: ResponseData<ComplexModel>) => 
  17.   setTimeout( 
  18.     () => 
  19.       console.log( 
  20.         '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>'
  21.         typeof data, 
  22.         data.data.toJSON(), 
  23.       ), 
  24.     2000, 
  25.   ), 
  26. ); 
  27.  
  28. ComplexModel.get( 
  29.   { id: '2' }, 
  30.   { auth: false, onlyData: true }, 
  31. ).then((data: ComplexModel) => 
  32.   setTimeout( 
  33.     () => 
  34.       console.log( 
  35.         '仅返回关键数据data,返回 T 或 T[]:'
  36.         typeof data, 
  37.         data.toJSON(), 
  38.       ), 
  39.     3000, 
  40.   ), 
  41. ); 

接着是打印结果。这次Immutable-js Record Object就调用了data.toJSON()转换成原始的JSON Object。

  1. 加载中... 2 
  2. 加载中... 2 
  3. 因需授权导致内部异常,返回 ResponseData:object { code: 403, message: '未授权' } 
  4. 设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]> object { 
  5.   list: [ { userNo: '1', userProfile: [Object] } ], 
  6.   pageNo: 1, 
  7.   pageSize: 10, 
  8.   pageTotal: 0 
  9. 仅返回关键数据data,返回 T 或 T[]:object { 
  10.   list: [ { userNo: '1', userProfile: [Object] } ], 
  11.   pageNo: 1, 
  12.   pageSize: 10, 
  13.   pageTotal: 0 

总结

本文的代码地址:
https://github.com/stelalae/node_demo,欢迎follow me~

现在接口调用是不是很优雅?!只关心请求和影响的数据格式,多使用高内聚低耦合,这对项目持续迭代非常有帮助的。使用TypeScript和Immutable-js来处理数据,在大型应用中越来越深入,从数据管理出发可以优化上层UI显示和业务逻辑。

责任编辑:庞桂玉 来源: 今日头条
相关推荐

2010-01-27 16:13:43

2021-03-16 08:54:35

AQSAbstractQueJava

2018-12-19 14:40:08

Redis高级特性

2011-07-04 10:39:57

Web

2022-08-02 07:56:53

反转依赖反转控制反转

2022-03-23 18:58:11

ZookeeperZAB 协议

2022-12-02 09:13:28

SeataAT模式

2009-11-30 16:46:29

学习Linux

2019-11-11 14:51:19

Java数据结构Properties

2012-05-21 10:06:26

FrameworkCocoa

2019-01-07 15:29:07

HadoopYarn架构调度器

2021-07-20 15:20:02

FlatBuffers阿里云Java

2017-07-02 18:04:53

块加密算法AES算法

2023-05-05 18:33:15

2022-09-26 09:01:15

语言数据JavaScript

2023-03-20 09:48:23

ReactJSX

2024-11-27 06:50:58

元组函数返回值

2018-11-09 16:24:25

物联网云计算云系统

2009-11-18 13:30:37

Oracle Sequ

2019-12-04 10:13:58

Kubernetes存储Docker
点赞
收藏

51CTO技术栈公众号