Advanced use of TypeScript in the Model

This article github address, nuggets address

In classic front-end development modes such as MVC and MVVC, V and C are often the focus, which may be the main focus of front-end business. Combined with the actual business, I prefer routing mode and plug-in design, which can benefit developers more in iteration and maintenance (but you need to find PM to coordinate this matter, after all, they understand how to simplify user experience, mostly how to make users operate simply). But let’s take a look at Model today and see how M can be extended.

If you are familiar with iOS development, you may have heard of the VIPER development model, which is recommended below

  • IOS developers use the Viper architecture to build complex pages
  • Build iOS apps with VIPER
  • Build iOS app VIPER with building blocks

background

Before you read this article, your actual project (such as React+Redux) might request server data using one of the following strategies:

  1. ComponentDidMount sends redux Action request data.
  2. Make an asynchronous network request in an Action. Of course, you have already wrapped the network request.
  3. Handle certain exceptions and marginal logic inside the network request, and then return the requested data;
  4. Get the data this.setState to refresh the page and possibly save a copy to the global redux;

Normally, one interface corresponds to at least one interface responding to the Model, but what if you also define the Model for the interface request and have five interfaces on a page?

If you already have TypeScript in your project, and write models, your writing experience will flow like a stream! In practice, however, you need to do some extra work on the data returned by the server, parameters passed from page to page, etc., where data is passed:

  • Exception handling of null values, such as null, undefined (in the latest ES solution and TS support, added: chain call? And operators?? , please check the user manual by yourself);
  • Sex =0, 1, 2, time=1591509066;
  • Anything else? Welcome to add message)

As a good and mature developer, you must have done the extra work mentioned above, writing dozens or even hundreds of tool class functions in the utils file, even classifying them according to their purpose: time class, age gender class, number class,…… “, and then you import where you need to, and then you start making pass-through calls. Yes, everything looks perfect!

The above process is the author himself :).

The current

As projects and businesses iterate, and your boss pushes the clock, the worst that can happen is that you run into colleagues who don’t follow the “development norms” described above. Let’s get straight to the point.

Although the above process has a certain design, it does not achieve the principle of high cohesion and low coupling, which is not conducive to the later iteration and local reconstruction of the project.

Another recommended design principle: the five SOLID principles of object orientation

Here’s an example:

  • The interface field is changed, for example, from Sex to Gender.
  • During internal front-end reconstruction, when data model mismatch is found, page C can jump from additional parameter A of page A or additional parameter B of page B. After reconstruction, additional parameter B1 of page B1 also jumps to PAGE C. From the point of view of design, it must be the best to make B1 adapt to B as far as possible, otherwise C will become heavier and heavier.

As mentioned above, whether page interaction or business interaction, the most fundamental is the exchange and transfer of data, thus affecting the page and business. Data is the core of the concatenation of pages and business, and the Model is the representation of the data.

For example, under the current development mode of the separation of the front and back ends, after the confirmation of requirements, the first thing to be done is database design and interface design, which is simply the convention of fields, and then the page development, and finally the interface debugging and system debugging, until the delivery test. During this time, the back end needs to perform interface unit tests and the front end needs to Mock data to develop pages.

How to solve

Interface management

At present, note interface management is carried out through JSON form. When the project is initialized, the configured interface list is registered in the Redux Action by means of DVA, and then the interface call can directly send the Action. And finally get the Data that the server responds to.

Interface configuration (corresponding to 2nd edition below) :

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',
  },
]
Copy the code

First edition:

import { apiComm, apiMy } from 'services';

export default {
  namespace: 'bill'.state: {},
  reducers: {
    updateState(state, { payload }) {
      return{... state, ... payload }; }},effects: {
    *findBydoctorIdBill({ payload, callback }, { call }) {
      const res = yieldcall(apiMy.findBydoctorIdBill, payload); ! apiComm.IsSuccess(res) && callback(res.data); }, *findByDoctorIdDetail({ payload, callback }, { call }) {const res = yieldcall(apiMy.findByDoctorIdDetail, payload); ! apiComm.IsSuccess(res) && callback(res.data); }, *findStatementDetails({ payload, callback }, { call }) {const res = yieldcall(apiMy.findStatementDetails, payload); ! apiComm.IsSuccess(res) && callback(res.data); ,}}};Copy the code

Version 2 uses higher-order functions and supports server address switching to reduce redundant code:

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;
};
Copy the code

This looks fine, addressing the interface address management and encapsulating the interface request, but it still has to deal with the abnormal Data in the returned Data.

Another problem is that the interface does not correspond to the corresponding request and response data models, and it will take a little time to sort out the business logic when you look at the code again.

Ask the reader to consider the above questions and then keep reading.

The Model management

An interface must correspond to a single request Model and a single response Model. Yes, that’s right! The following uses this mechanism for further discussion.

So by responding to the Model to initiate the interface request, the function call can also use the request Model to determine whether the participation is not reasonable, thus switching the protagonist from the interface to the Model. Personally, I think it is more appropriate to respond to Model first, so that I can directly understand the data format obtained after this request.

Let’s look at the code that initiates the request through Model:

SimpleModel.get(
  { id: '1' },
  { auth: false, onlyData: false },
).then((data: ResponseData<SimpleModel>) = >
  setTimeout(
    (a)= >
      console.log(
        ResponseData
      
        or ResponseData
       []>
      .typeof data,
        data,
      ),
    2000,),);Copy the code

SimpleModel is the response Model defined. The first parameter is the request, the second parameter is the request configuration item, and the interface address is hidden inside SimpleModel.

import { Record } from 'immutable';

import { ApiOptons } from './Common';
import { ServiceManager } from './Service';

/** * simple type */
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'.// Hidden interface address
      params,
      options,
    );
  }

  static sexMap = {
    0: 'secret'.1: 'male'.2: 'woman'}; sexText() {return SimpleModel.sexMap[this.sex] ?? 'secret'; }}Copy the code

Record is an IMmutable Record, which is intended to serialize a JSON Object to a Class Object. The purpose is to improve the cohesion of Model’s related functions in the project. For more information, see my other article: The Strong Language Path of JavaScript: Alternative JSON serialization and deserialization.

// utils/tool.tsx
export const sexMap = {
  0: 'secret'.1: 'male'.2: 'woman'};export const sexText = (sex: number) = > {
  return sexMap[sex] ?? 'secret';
};
Copy the code

Using this to access concrete data directly inside SimpleModel is much more cohesive and maintainable than passing in external arguments when calling utils/tool. With this in mind, you can create more “dark magic” grammar candy!

Let’s look at the contents of the Common file:

/** * interface response, outermost unified format */
export class ResponseData<T = any> {
  code? = 0;
  message? = 'Operation successful';
  toastId? = - 1; data? : T; }/** * API configuration information */
export classApiOptons { headers? :any = {}; // Additional request headersloading? :boolean = true; // Whether to display loadingloadingTime? :number = 2; // Displays the loading timeauth? :boolean = true; // Whether authorization is requiredonlyData? :boolean = true; // Only data is returned
}

/** * enumerates the types that the interface can return * -t and T[] are valid when ApiOptons. OnlyData is true * -responseData 
      
        and ResponseData
       
         are available at ApiOptons. OnlyData ResponseData is available when false * -responseData is available when an exception occurs within the interface */
       []>
      
export type ResultDataType<T> =
  | T
  | T[]
  | ResponseData<T>
  | ResponseData<T[]>
  | ResponseData;

Copy the code

The inside of the Service file encapsulates axios:

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ApiOptons, ResponseData, ResultDataType } from './Common';

/** * simulates 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); }}/** * Unknown error code */
const codeUnknownTask = - 999.;

/** * the interface requests to encapsulate the base class */
export class InterfaceService {
  /** * todo */
  private staticuserProfile: { sysToken? :' ' } = {};
  public static setUser(_user: any) {
    InterfaceService.userProfile = _user;
  }

  constructor(props: ApiOptons) {
    this.options = props;
  }
  /** * Default configuration */
  public options = new ApiOptons();

  /** * todo */
  public get sysToken(): string {
    returnInterfaceService.userProfile? .sysToken ??' ';
  }

  /** * Build header */
  public get headers(): Object {
    return {
      Accept: 'application/json'.'Content-Type': 'application/json; charset=utf-8'.'app-info-key': 'xxx'.// Customize the field
    };
  }

  /** * request preconditions. You can refactor this function */ to suit your needs
  preCheck() {
    if (this.options.loading && this.options.loadingTime > 0) {
      return Toast.loading('Loading... '.this.options? .loadingTime ??3);
    }
    return - 1;
  }

  /** * download json, return object */
  public static async getJSON(url: string) {
    try {
      const res = await fetch(url);
      return await res.json();
    } catch (e) {
      console.log(e);
      return{}; }}}/** * Interface request encapsulation (AXIos version, can also encapsulate other versions of the request) */
export class InterfaceAxios extends InterfaceService {
  constructor(props: ApiOptons) {
    super(props);
  }

  /** * encapsulates axios */
  private request = (requestCfg: AxiosRequestConfig): Promise<ResponseData> => {
    return axios(requestCfg)
      .then(this.checkStatus)
      .catch((err: any) = > {
        // The background interface is abnormal. For example, the interface is abnormal, the HTTP status code is not 200, and the data format is not JSON. The fatal error is determined
        console.log(requestCfg, err);
        return {
          code: 408,
          message: 'Network exception'}; }); };/** * Check network response status code */
  private checkStatus(response: AxiosResponse<ResponseData>) {
    if (response.status >= 200 && response.status < 300) {
      return response.data;
    }
    return {
      code: 408,
      message: 'Network data exception'}; }/** * Send a POST request */
  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;
  }

  /** * Send a GET request */
  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;
    returnret; }}export class ServiceManager {
  /** * Check the interface data */
  public hasNoError(res: ResponseData) {
    if (res.toastId > 0) {
      Toast.remove(res.toastId);
    }
    if(res? .code ! = =0&& res.code ! == codeUnknownTask) { Toast.info(res?.message ??'Server error');
      return false;
    }
    return true;
  }

  /** * Parse the response */
  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);
      }
      returnoptions.onlyData ? response.data : response; }}/** * Post interface request */
  public static async post<T>(
    modal: { new (x: any): T },
    url: string, body? :any,
    options: ApiOptons = new ApiOptons(),
  ): Promise<ResultDataType<T>> {
    // Use merge to reduce external incoming configuration
    options = Object.assign(new ApiOptons(), options);

    const request = new InterfaceAxios(options);
    if(options.auth && ! request.sysToken) {return {
        code: 403,
        message: 'Unauthorized'}; }try {
      const response = await request.post(url, body);
      return ServiceManager.parse<T>(modal, response, options);
    } catch (err) {
      // Log the error
      console.log(url, body, options, err);
      return {
        code: codeUnknownTask,
        message: 'Internal error, please try again later'}; }}/** * get Interface request */
  public static async get<T>(
    modal: { new (x: any): T },
    url: string, params? :any,
    options: ApiOptons = new ApiOptons(),
  ): Promise<ResultDataType<T>> {
    // Use merge to reduce external incoming configuration
    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: 'Unauthorized'}; }try {
      const response = await a.get(url, params);
      return ServiceManager.parse<T>(modal, response, options);
    } catch (err) {
      // Log the error
      console.log(url, params, options, err);
      return {
        code: codeUnknownTask,
        message: 'Internal error, please try again later'}; }}}Copy the code

The Service file is a bit long, with the following classes:

  • Toast: Simulate loading when requesting an interface, which can be configured when the interface is called;
  • InterfaceService: the base class for interface request, which internally records the current user’s Token, multi-environment server address switch (not implemented in the code), interface configuration for a single request, custom headers, logical check before the request, and direct request for remote JSON configuration files.
  • InterfaceAxios: Inherits InterfaceService, which is the interface request of axios version, and initiates the actual request internally. You can package the fetch version.
  • ServiceManager: provides the request class for Model. After passing in the response Model and the corresponding server address, the response Data is parsed into the corresponding Model after the asynchronous request gets the Data.

Here’s another example of a complete Model request:

import { ResponseData, ApiOptons, SimpleModel } from './model';

// The interface is configured with three different requests
SimpleModel.get({ id: '1' }).then((data: ResponseData) = >
  setTimeout(
    (a)= >
      console.log(
        [ResponseData:] [ResponseData:].typeof data,
        data,
      ),
    1000,),); SimpleModel.get( { id:'1' },
  { auth: false, onlyData: false },
).then((data: ResponseData<SimpleModel>) = >
  setTimeout(
    (a)= >
      console.log(
        ResponseData
      
        or ResponseData
       []>
      .typeof data,
        data,
      ),
    2000,),); SimpleModel.get( { id:'1' },
  { auth: false, onlyData: true },
).then((data: SimpleModel) = >
  setTimeout(
    (a)= >
      console.log(
        'Return only key data data, return T or T[] :'.typeof data,
        data,
        data.sexText(),
      ),
    3000,),);Copy the code

The console prints the result. Note that the data returned may be a JSON Object or an IMMUTes-js Record Object.

Loading in... 2 Loading... ResponseData: object {code: 403, message:'Unauthorized'ResponseData<T> or 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[] : 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}} maleCopy the code

Finally, a common example of the compound type Model:

/** * Complex type */

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,
// };

// It is recommended to use class instead of object above. Because you can't add optional properties to object, right?
class ComplexItemDefault {
  userNo = 'us1212';
  userProfile = ComplexChildOneDefault;
  extraFirst? = ComplexChildTwoDefault;
  extraSecond? = ComplexChildThirdDefault;
}

// const ComplexListDefault = {
// list: [],
// pageNo: 1,
// pageSize: 10,
// pageTotal: 0,
// };

If you want to specify the Model of an array element, you must use class
class ComplexListDefault {
  list: ComplexItemDefault[] = [];
  pageNo = 1;
  pageSize = 10;
  pageTotal = 0;
}

interface ComplexModelParams {
  id: string;
}

// A new Record is required to initialize the Record because of the class used
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, ); }}Copy the code

Here is the calling code:

ComplexModel.get({ id: '2' }).then((data: ResponseData) = >
  setTimeout(
    (a)= >
      console.log(
        [ResponseData:] [ResponseData:].typeof data,
        data,
      ),
    1000,),); ComplexModel.get( { id:'2' },
  { auth: false, onlyData: false },
).then((data: ResponseData<ComplexModel>) = >
  setTimeout(
    (a)= >
      console.log(
        ResponseData
      
        or ResponseData
       []>
      .typeof data,
        data.data.toJSON(),
      ),
    2000,),); ComplexModel.get( { id:'2' },
  { auth: false, onlyData: true },
).then((data: ComplexModel) = >
  setTimeout(
    (a)= >
      console.log(
        'Return only key data data, return T or T[] :'.typeof data,
        data.toJSON(),
      ),
    3000,),);Copy the code

Next comes the printing of the results. This Immutable Record Object calls data.tojson () to convert to the original JSON Object.

Loading in... 2 Loading... ResponseData: object {code: 403, message:'Unauthorized'ResponseData<T> or ResponseData<T[]> object {list: [{userNo:'1', userProfile: [Object]}], pageNo: 1, pageSize: 10, pageTotal: 0} Return only key data, return T or T[] : Object {list: [{userNo:'1', userProfile: [Object] } ],
  pageNo: 1,
  pageSize: 10,
  pageTotal: 0
}
Copy the code

conclusion

The code address of this article: github.com/stelalae/no… Welcome to follow me~

Aren’t interface calls elegant now? ! Data formats that are only concerned with request and impact, often with high cohesion and low coupling, are very helpful for the continuous iteration of the project. Using TypeScript and Immutable JS to process data, increasingly in large applications, optimizes upper-level UI display and business logic for data management purposes.


Reference Address:

  1. A summary of TypeScript usage in complex Immutable. Js data structures
  2. Let’s play TypeScript! You’re already on!
  3. Strong Language path of JavaScript – Alternative JSON serialization and deserialization