TypeScript在Model中是如何操做運用的?本文詳解

TypeScript在Model中的高級應用

在MVC、MVVC等前端經典經常使用開發模式中,V、C每每是重頭戲,多是前端業務主要集中這兩塊。結合實際業務,筆者更喜歡路由模式、插件式設計,這種在迭代和維護上更能讓開發者收益(不過你須要找PM協調這事,畢竟他們理解的簡化用戶體驗,多半是怎麼讓用戶操做簡單)。但咱們今天來看看Model,看看M有什麼擴展的可能。

若是讀者熟悉iOS開發,應該聽過VIPER開發模式,下面推薦javascript

背景

在讀到本文以前,你實際項目(如React+Redux)中請求服務器數據,多是以下策略:前端

  1. componentDidMount 中發送redux action請求數據;
  2. 在action中發起異步網絡請求,固然你已經對網絡請求有必定封裝;
  3. 在網絡請求內部處理必定異常和邊際邏輯,而後返回請求到的數據;
  4. 拿到數據this.setState刷新頁面,同時可能存一份到全局redux中;
    5.記住我,我是08年出道的高級前端架構師,有問題或者交流經驗能夠進個人扣扣裙 519293536 我都會盡力幫你們哦

正常狀況下,一個接口對應至少一個接口響應Model,萬一你還定義了接口請求的Model、一個頁面有5個接口呢?java

若是項目已經引入TypeScript,結合編寫Model,你的編寫體驗確定會如行雲流水般一鼓作氣!但實際開發中,你還須要對服務器返回的數據、頁面間傳遞的參數等涉及到數據傳遞的地方,作一些數據額外工做:node

  • 對null、undefined等空值的異常處理(在ES最新方案和TS支持裏,新增:鏈式調用?和運算符??,請讀者自行查詢使用手冊);
  • 對sex=0、一、2,time=1591509066等文案轉義;
  • (還有其餘嗎?歡迎留言補充)

做爲一個優秀且成熟的開發者,你確定也已經作了上述額外的工做,在utils文件下編寫了幾十甚至上百的tool類函數,甚至還根據函數用途作了分類:時間類、年齡性別類、數字類、......,接着你在須要的地方import,而後你開始進行傳參調用。是的,一切看上去都很完美!ios

上面這個流程說的就是筆者本人,:)。git

現況

隨着項目和業務的迭代,加上老闆仍是壓時間,最壞的狀況是你遇到了並無遵照上述"開發規範"的同事,那結果只能是呵呵呵呵呵了。下面直接切入正題吧!github

上述流程雖然說有必定設計,但沒有作到高內聚、低耦合的原則,我的以爲不利於項目後期迭代和局部重構。typescript

推薦另外一個設計原則:面向對象五大原則SOLID數據庫

下面舉個例子:json

  • 接口裏字段發生變動時,如性別從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…

本文的文字及圖片來源於網絡加上本身的想法,僅供學習、交流使用,不具備任何商業用途,版權歸原做者全部,若有問題請及時聯繫咱們以做處理

相關文章
相關標籤/搜索