zoukankan      html  css  js  c++  java
  • TypeScript在Model中是如何操作运用的?本文详解

    TypeScript在Model中的高级应用

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

    如果读者熟悉iOS开发,应该听过VIPER开发模式,下面推荐

    背景

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

    1. componentDidMount 中发送redux action请求数据;
    2. 在action中发起异步网络请求,当然你已经对网络请求有一定封装;
    3. 在网络请求内部处理一定异常和边际逻辑,然后返回请求到的数据;
    4. 拿到数据this.setState刷新页面,同时可能存一份到全局redux中;
      5.记住我,我是08年出道的高级前端架构师,有问题或者交流经验可以进我的扣扣裙 519293536 我都会尽力帮大家哦

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

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

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

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

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

    现况

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

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

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

    下面举个例子:

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

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

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

    如何解决

    接口管理

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

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

    list: [
      {
        alias: 'getCode',
        apiPath: '/user/v1/getCode',
        auth: false,
      },
      {
        alias: 'userLogin',
        apiPath: '/user/v1/userLogin',
        auth: false,
        nextGeneral: 'saveUserInfo',
      },
      {
        alias: 'loginTokenByJVerify',
        apiPath: '/user/v1/jgLoginApi',
        auth: false,
        nextGeneral: 'saveUserInfo',
      },
    ]
    复制代码

    第一版:

    import { apiComm, apiMy } from 'services';
    
    export default {
      namespace: 'bill',
      state: {},
      reducers: {
        updateState(state, { payload }) {
          return { ...state, ...payload };
        },
      },
      effects: {
        *findBydoctorIdBill({ payload, callback }, { call }) {
          const res = yield call(apiMy.findBydoctorIdBill, payload);
          !apiComm.IsSuccess(res) && callback(res.data);
        },
        *findByDoctorIdDetail({ payload, callback }, { call }) {
          const res = yield call(apiMy.findByDoctorIdDetail, payload);
          !apiComm.IsSuccess(res) && callback(res.data);
        },
        *findStatementDetails({ payload, callback }, { call }) {
          const res = yield call(apiMy.findStatementDetails, payload);
          !apiComm.IsSuccess(res) && callback(res.data);
        },
      },
    };
    复制代码

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

    export const connectModelService = (cfg: any = {}) => {
      const { apiBase = '', list = [] } = cfg;
      const listEffect = {};
      list.forEach(kAlias => {
        const { alias, apiPath, nextGeneral, cbError = false, ...options } = kAlias;
        const effectAlias = function* da({ payload = {}, nextPage, callback }, { call, put }) {
          let apiBaseNew = apiBase;
          // apiBaseNew = urlApi;
          if (global.apiServer) {
            apiBaseNew = global.apiServer.indexOf('xxx.com') !== -1 ? global.apiServer : apiBase;
          } else if (!isDebug) {
            apiBaseNew = urlApi;
          }
          const urlpath =
            apiPath.indexOf('http://') === -1 && apiPath.indexOf('https://') === -1 ? `${apiBaseNew}${apiPath}` : apiPath;
          const res = yield call(hxRequest, urlpath, payload, options);
          const next = nextPage || nextGeneral;
          // console.log('=== hxRequest res', next, res);
          if (next) {
            yield put({
              type: next,
              payload,
              res,
              callback,
            });
          } else if (cbError) {
            callback && callback(res);
          } else {
            hasNoError(res) && callback && callback(res.data);
          }
        };
        listEffect[alias] = effectAlias;
      });
      return listEffect;
    };
    复制代码

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

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

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

    Model管理

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

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

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

    SimpleModel.get(
      { id: '1' },
      { auth: false, onlyData: false },
    ).then((data: ResponseData<SimpleModel>) =>
      setTimeout(
        () =>
          console.log(
            '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>',
            typeof data,
            data,
          ),
        2000,
      ),
    );
    复制代码

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

    import { Record } from 'immutable';
    
    import { ApiOptons } from './Common';
    import { ServiceManager } from './Service';
    
    /**
     * 简单类型
     */
    const SimpleModelDefault = {
      a: 'test string',
      sex: 0,
    };
    
    interface SimpleModelParams {
      id: string;
    }
    
    export class SimpleModel extends Record(SimpleModelDefault) {
      static async get(params: SimpleModelParams, options?: ApiOptons) {
        return await ServiceManager.get<SimpleModel>(
          SimpleModel,
          'http://localhost:3000/test',   // 被隐藏的接口地址
          params,
          options,
        );
      }
    
      static sexMap = {
        0: '保密',
        1: '男',
        2: '女',
      };
    
      sexText() {
        return SimpleModel.sexMap[this.sex] ?? '保密';
      }
    }
    复制代码

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

    // utils/tool.tsx
    export const sexMap = {
      0: '保密',
      1: '男',
      2: '女',
    };
    
    export const sexText = (sex: number) => {
      return sexMap[sex] ?? '保密';
    };
    复制代码

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

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

    /**
     * 接口响应,最外层统一格式
     */
    export class ResponseData<T = any> {
      code? = 0;
      message? = '操作成功';
      toastId? = -1;
      data?: T;
    }
    
    /**
     * api配置信息
     */
    export class ApiOptons {
      headers?: any = {}; // 额外请求头
      loading?: boolean = true; // 是否显示loading
      loadingTime?: number = 2; // 显示loading时间
      auth?: boolean = true; // 是否需要授权
      onlyData?: boolean = true; // 只返回data
    }
    
    /**
     * 枚举接口能返回的类型
     * - T、T[] 在 ApiOptons.onlyData 为true时是生效
     * - ResponseData<T>、ResponseData<T[]> 在 ApiOptons.onlyData 为false时是生效
     * - ResponseData 一般在接口内部发生异常时生效
     */
    export type ResultDataType<T> =
      | T
      | T[]
      | ResponseData<T>
      | ResponseData<T[]>
      | ResponseData;
    
    复制代码

    Service文件内部是封装了axios:

    import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
    import { ApiOptons, ResponseData, ResultDataType } from './Common';
    
    /**
     * 模拟UI loading
     */
    class Toast {
      static loading(txt: string, time: number = 3) {
        console.log(txt, time);
        return 1;
      }
      static info(txt: string, time: number = 3) {
        console.log(txt, time);
        return 1;
      }
      static remove(toastId: number) {
        console.log(toastId);
      }
    }
    
    /**
     * 未知(默认)错误码
     */
    const codeUnknownTask = -999;
    
    /**
     * 接口请求封装基类
     */
    export class InterfaceService {
      /**
       * todo
       */
      private static userProfile: { sysToken?: '' } = {};
      public static setUser(_user: any) {
        InterfaceService.userProfile = _user;
      }
    
      constructor(props: ApiOptons) {
        this.options = props;
      }
      /**
       * 默认配置
       */
      public options = new ApiOptons();
    
      /**
       * todo
       */
      public get sysToken(): string {
        return InterfaceService.userProfile?.sysToken ?? '';
      }
    
      /**
       * 构建header
       */
      public get headers(): Object {
        return {
          Accept: 'application/json',
          'Content-Type': 'application/json; charset=utf-8',
          'app-info-key': 'xxx', // 自定义字段
        };
      }
    
      /**
       * 请求前置条件。可根据自己情况重构此函数
       */
      preCheck() {
        if (this.options.loading && this.options.loadingTime > 0) {
          return Toast.loading('加载中...', this.options?.loadingTime ?? 3);
        }
        return -1;
      }
    
      /**
       * 下载json,返回对象
       */
      public static async getJSON(url: string) {
        try {
          const res = await fetch(url);
          return await res.json();
        } catch (e) {
          console.log(e);
          return {};
        }
      }
    }
    
    /**
     * 接口请求封装(axios版,也可以封装其他版本的请求)
     */
    export class InterfaceAxios extends InterfaceService {
      constructor(props: ApiOptons) {
        super(props);
      }
    
      /**
       * 封装axios
       */
      private request = (requestCfg: AxiosRequestConfig): Promise<ResponseData> => {
        return axios(requestCfg)
          .then(this.checkStatus)
          .catch((err: any) => {
            // 后台接口异常,如接口不通、http状态码非200、data非json格式,判定为fatal错误
            console.log(requestCfg, err);
            return {
              code: 408,
              message: '网络异常',
            };
          });
      };
    
      /**
       * 检查网络响应状态码
       */
      private checkStatus(response: AxiosResponse<ResponseData>) {
        if (response.status >= 200 && response.status < 300) {
          return response.data;
        }
        return {
          code: 408,
          message: '网络数据异常',
        };
      }
    
      /**
       * 发送POST请求
       */
      public async post(url: string, data?: any) {
        const toastId = this.preCheck();
        const ret = await this.request({
          url,
          headers: this.headers,
          method: 'POST',
          data: Object.assign({ sysToken: this.sysToken }, data),
        });
        ret.toastId = toastId;
    
        return ret;
      }
    
      /**
       * 发送GET请求
       */
      public async get(url: string, params?: any) {
        const toastId = this.preCheck();
        const ret = await this.request({
          url,
          headers: this.headers,
          method: 'GET',
          params: Object.assign({ sysToken: this.sysToken }, params),
        });
        ret.toastId = toastId;
        return ret;
      }
    }
    
    export class ServiceManager {
      /**
       * 检查接口数据
       */
      public hasNoError(res: ResponseData) {
        if (res.toastId > 0) {
          Toast.remove(res.toastId);
        }
        if (res?.code !== 0 && res.code !== codeUnknownTask) {
          Toast.info(res?.message ?? '服务器出错');
          return false;
        }
        return true;
      }
    
      /**
       * 解析响应
       */
      public static parse<T>(
        modal: { new (x: any): T },
        response: any,
        options: ApiOptons,
      ): ResultDataType<T> {
        if (!response || !response.data) {
          response.data = new modal({});
        } else {
          if (response.data instanceof Array) {
            response.data = response.data.map((item: T) => new modal(item));
          } else if (response.data instanceof Object) {
            response.data = new modal(response.data);
          }
          return options.onlyData ? response.data : response;
        }
      }
    
      /**
       * post接口请求
       */
      public static async post<T>(
        modal: { new (x: any): T },
        url: string,
        body?: any,
        options: ApiOptons = new ApiOptons(),
      ): Promise<ResultDataType<T>> {
        // 使用合并,减少外部传入配置
        options = Object.assign(new ApiOptons(), options);
    
        const request = new InterfaceAxios(options);
        if (options.auth && !request.sysToken) {
          return {
            code: 403,
            message: '未授权',
          };
        }
    
        try {
          const response = await request.post(url, body);
          return ServiceManager.parse<T>(modal, response, options);
        } catch (err) {
          // 记录错误日志
          console.log(url, body, options, err);
          return {
            code: codeUnknownTask,
            message: '内部错误,请稍后再试',
          };
        }
      }
    
      /**
       * get接口请求
       */
      public static async get<T>(
        modal: { new (x: any): T },
        url: string,
        params?: any,
        options: ApiOptons = new ApiOptons(),
      ): Promise<ResultDataType<T>> {
        // 使用合并,减少外部传入配置
        options = Object.assign(new ApiOptons(), options);
    
        const a = new InterfaceAxios(options);
        const request = new InterfaceAxios(options);
        if (options.auth && !request.sysToken) {
          return {
            code: 403,
            message: '未授权',
          };
        }
    
        try {
          const response = await a.get(url, params);
          return ServiceManager.parse<T>(modal, response, options);
        } catch (err) {
          // 记录错误日志
          console.log(url, params, options, err);
          return {
            code: codeUnknownTask,
            message: '内部错误,请稍后再试',
          };
        }
      }
    }
    复制代码

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

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

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

    import { ResponseData, ApiOptons, SimpleModel } from './model';
    
    // 接口配置不同的三种请求
    SimpleModel.get({ id: '1' }).then((data: ResponseData) =>
      setTimeout(
        () =>
          console.log(
            '因需授权导致内部异常,返回 ResponseData:',
            typeof data,
            data,
          ),
        1000,
      ),
    );
    
    SimpleModel.get(
      { id: '1' },
      { auth: false, onlyData: false },
    ).then((data: ResponseData<SimpleModel>) =>
      setTimeout(
        () =>
          console.log(
            '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>',
            typeof data,
            data,
          ),
        2000,
      ),
    );
    
    SimpleModel.get(
      { id: '1' },
      { auth: false, onlyData: true },
    ).then((data: SimpleModel) =>
      setTimeout(
        () =>
          console.log(
            '仅返回关键数据data,返回 T 或 T[]:',
            typeof data,
            data,
            data.sexText(),
          ),
        3000,
      ),
    );
    
    复制代码

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

    加载中... 2
    加载中... 2
    因需授权导致内部异常,返回 ResponseData: object { code: 403, message: '未授权' }
    设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]> object {
      code: 0,
      message: '1',
      data: SimpleModel {
        __ownerID: undefined,
        _values: List {
          size: 2,
          _origin: 0,
          _capacity: 2,
          _level: 5,
          _root: null,
          _tail: [VNode],
          __ownerID: undefined,
          __hash: undefined,
          __altered: false
        }
      },
      toastId: 1
    }
    仅返回关键数据data,返回 T 或 T[]: object SimpleModel {
      __ownerID: undefined,
      _values: List {
        size: 2,
        _origin: 0,
        _capacity: 2,
        _level: 5,
        _root: null,
        _tail: VNode { array: [Array], ownerID: OwnerID {} },
        __ownerID: undefined,
        __hash: undefined,
        __altered: false
      }
    } 男
    复制代码

    最后再补充一个常见的复合类型Model示例:

    /**
     * 复杂类型
     */
    
    const ComplexChildOneDefault = {
      name: 'lyc',
      sex: 0,
      age: 18,
    };
    
    const ComplexChildTwoDefault = {
      count: 10,
      lastId: '20200607',
    };
    
    const ComplexChildThirdDefault = {
      count: 10,
      lastId: '20200607',
    };
    
    // const ComplexItemDefault = {
    //   userNo: 'us1212',
    //   userProfile: ComplexChildOneDefault,
    //   extraFirst: ComplexChildTwoDefault,
    //   extraTwo: ComplexChildThirdDefault,
    // };
    
    // 复合类型建议使用class,而不是上面的object。因为object里不能添加可选属性?
    class ComplexItemDefault {
      userNo = 'us1212';
      userProfile = ComplexChildOneDefault;
      extraFirst? = ComplexChildTwoDefault;
      extraSecond? = ComplexChildThirdDefault;
    }
    
    // const ComplexListDefault = {
    //   list: [],
    //   pageNo: 1,
    //   pageSize: 10,
    //   pageTotal: 0,
    // };
    
    // 有数组的复合类型,如果要指定数组元素的Model,就必须用class
    class ComplexListDefault {
      list: ComplexItemDefault[] = [];
      pageNo = 1;
      pageSize = 10;
      pageTotal = 0;
    }
    
    interface ComplexModelParams {
      id: string;
    }
    
    // 因为使用的class,所以需要 new 一个去初始化Record
    export class ComplexModel extends Record(new ComplexListDefault()) {
      static async get(params: ComplexModelParams, options?: ApiOptons) {
        return await ServiceManager.get<ComplexModel>(
          ComplexModel,
          'http://localhost:3000/test2',
          params,
          options,
        );
      }
    }
    复制代码

    下面是调用代码:

    ComplexModel.get({ id: '2' }).then((data: ResponseData) =>
      setTimeout(
        () =>
          console.log(
            '因需授权导致内部异常,返回 ResponseData:',
            typeof data,
            data,
          ),
        1000,
      ),
    );
    
    ComplexModel.get(
      { id: '2' },
      { auth: false, onlyData: false },
    ).then((data: ResponseData<ComplexModel>) =>
      setTimeout(
        () =>
          console.log(
            '设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]>',
            typeof data,
            data.data.toJSON(),
          ),
        2000,
      ),
    );
    
    ComplexModel.get(
      { id: '2' },
      { auth: false, onlyData: true },
    ).then((data: ComplexModel) =>
      setTimeout(
        () =>
          console.log(
            '仅返回关键数据data,返回 T 或 T[]:',
            typeof data,
            data.toJSON(),
          ),
        3000,
      ),
    );
    复制代码

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

    加载中... 2
    加载中... 2
    因需授权导致内部异常,返回 ResponseData: object { code: 403, message: '未授权' }
    设置返回全部数据,返回 ResponseData<T> 或 ResponseData<T[]> object {
      list: [ { userNo: '1', userProfile: [Object] } ],
      pageNo: 1,
      pageSize: 10,
      pageTotal: 0
    }
    仅返回关键数据data,返回 T 或 T[]: object {
      list: [ { userNo: '1', userProfile: [Object] } ],
      pageNo: 1,
      pageSize: 10,
      pageTotal: 0
    }
    复制代码

    总结

    1/都懂了吗?我是08年出道的高级前端架构师,有问题或者交流经验可以进我的扣扣裙 519293536 我都会尽力帮大家哦
    2/本文的代码地址:github.com/stelalae/no…

    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

  • 相关阅读:
    Apache 虚拟主机 VirtualHost 配置
    EAX、ECX、EDX、EBX寄存器的作用
    Python中文文档 目录(转载)
    八度
    POJ 3268 Silver Cow Party (最短路)
    POJ 2253 Frogger (求每条路径中最大值的最小值,Dijkstra变形)
    2013金山西山居创意游戏程序挑战赛——复赛(1) HDU 4557 非诚勿扰 HDU 4558 剑侠情缘 HDU 4559 涂色游戏 HDU 4560 我是歌手
    HDU 4549 M斐波那契数列(矩阵快速幂+欧拉定理)
    UVA 11624 Fire! (简单图论基础)
    HDU 3534 Tree (树形DP)
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/13110461.html
Copyright © 2011-2022 走看看