TypeScript在Model中的高級應用

這篇文章主要講解了“TypeScript在Model中的高級應用”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“TypeScript在Model中的高級應用”吧!

網站建設哪家好,找成都創新互聯公司!專注于網頁設計、網站建設、微信開發、小程序定制開發、集團企業網站建設等服務項目。為回饋新老客戶創新互聯還提供了策勒免費建站歡迎大家使用!

前言

在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。

接口配置(對應下面第二版):

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[5],目的是將JSON Object反序列化為Class Object,目的是提高Model在項目中相關函數的內聚。更多介紹請看我另外一篇文章:JavaScript的強語言之路&mdash;另類的JSON序列化與反序列化[6]

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

感謝各位的閱讀,以上就是“TypeScript在Model中的高級應用”的內容了,經過本文的學習后,相信大家對TypeScript在Model中的高級應用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創新互聯,小編將為大家推送更多相關知識點的文章,歡迎關注!

當前文章:TypeScript在Model中的高級應用
分享URL:http://m.kartarina.com/article40/jecoeo.html

成都網站建設公司_創新互聯,為您提供搜索引擎優化域名注冊標簽優化建站公司定制網站商城網站

廣告

聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯

外貿網站制作
主站蜘蛛池模板: 中文一国产一无码一日韩| HEYZO无码综合国产精品| 伊人蕉久中文字幕无码专区| 97无码免费人妻超级碰碰夜夜| 亚洲最大无码中文字幕| 成人免费午夜无码视频| 亚洲av无码成人影院一区| 国产乱子伦精品无码码专区 | 亚洲av无码乱码国产精品| 无码av免费毛片一区二区| 国产做无码视频在线观看| 精品无码日韩一区二区三区不卡 | 秋霞鲁丝片无码av| 无码av高潮喷水无码专区线| 久久久无码精品亚洲日韩软件| 亚洲国产成人精品无码区在线秒播| 大胆日本无码裸体日本动漫| 中文无码久久精品| 亚洲精品无码你懂的网站| 亚洲成a人无码亚洲成av无码| 中文国产成人精品久久亚洲精品AⅤ无码精品 | 精品亚洲AV无码一区二区三区| 久久久精品无码专区不卡| 国产午夜鲁丝片AV无码免费| 中文字幕无码乱码人妻系列蜜桃| 无码国内精品人妻少妇蜜桃视频| 色窝窝无码一区二区三区色欲| 久久久久久国产精品无码下载| 日韩少妇无码一区二区三区| 波多野结衣VA无码中文字幕电影| 免费无码一区二区三区| 无码国产色欲XXXXX视频| 亚洲AV无码一区二区二三区软件| 亚洲欧洲日产国码无码久久99 | 精品无码久久久久久久久水蜜桃| 无码专区永久免费AV网站| 无码中文2020字幕二区| 无码无套少妇毛多18PXXXX| 日韩夜夜高潮夜夜爽无码| 国产精品第一区揄拍无码 | 亚洲中文字幕不卡无码|