這篇文章主要講解了“TypeScript在Model中的高級(jí)應(yīng)用”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“TypeScript在Model中的高級(jí)應(yīng)用”吧!
網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)公司!專注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、小程序定制開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了策勒免費(fèi)建站歡迎大家使用!
在MVC、MVVC等前端經(jīng)典常用開發(fā)模式中,V、C往往是重頭戲,可能是前端業(yè)務(wù)主要集中這兩塊。結(jié)合實(shí)際業(yè)務(wù),筆者更喜歡路由模式、插件式設(shè)計(jì),這種在迭代和維護(hù)上更能讓開發(fā)者受益(不過(guò)你需要找PM協(xié)調(diào)這事,畢竟他們理解的簡(jiǎn)化用戶體驗(yàn),多半是怎么讓用戶操作簡(jiǎn)單)。但我們今天來(lái)看看Model,看看M有什么擴(kuò)展的可能。
在讀到本文之前,你實(shí)際項(xiàng)目(如React+Redux)中請(qǐng)求服務(wù)器數(shù)據(jù),可能是如下策略:
componentDidMount 中發(fā)送redux action請(qǐng)求數(shù)據(jù);
在action中發(fā)起異步網(wǎng)絡(luò)請(qǐng)求,當(dāng)然你已經(jīng)對(duì)網(wǎng)絡(luò)請(qǐng)求有一定封裝;
在網(wǎng)絡(luò)請(qǐng)求內(nèi)部處理一定異常和邊際邏輯,然后返回請(qǐng)求到的數(shù)據(jù);
拿到數(shù)據(jù)this.setState刷新頁(yè)面,同時(shí)可能存一份到全局redux中;
正常情況下,一個(gè)接口對(duì)應(yīng)至少一個(gè)接口相應(yīng)Model,萬(wàn)一你還定義了接口請(qǐng)求的Model、一個(gè)頁(yè)面有5個(gè)接口呢?
如果項(xiàng)目已經(jīng)引入TypeScript,結(jié)合編寫Model,你的編寫體驗(yàn)肯定會(huì)如行云流水般一氣呵成!但實(shí)際開發(fā)中,你還需要對(duì)服務(wù)器返回的數(shù)據(jù)、頁(yè)面間傳遞的參數(shù)等涉及到數(shù)據(jù)傳遞的地方,做一些數(shù)據(jù)額外工作:
對(duì)null、undefined等空值的異常處理(在ES最新方案和TS支持里,新增:鏈?zhǔn)秸{(diào)用?和運(yùn)算符??,請(qǐng)讀者自行查詢使用手冊(cè));
對(duì)sex=0、1、2,time=1591509066等文案轉(zhuǎn)義;
(還有其他嗎?歡迎留言補(bǔ)充)
作為一個(gè)優(yōu)秀且成熟的開發(fā)者,你肯定也已經(jīng)做了上述額外的工作,在utils文件下編寫了幾十甚至上百的tool類函數(shù),甚至還根據(jù)函數(shù)用途做了分類:時(shí)間類、年齡性別類、數(shù)字類、......,接著你在需要的地方import,然后你開始進(jìn)行傳參調(diào)用。是的,一切看上去都很完美!
上面這個(gè)流程說(shuō)的就是筆者本人,:)。
隨著項(xiàng)目和業(yè)務(wù)的迭代,加上老板還是壓時(shí)間,最壞的情況是你遇到了并沒(méi)有遵守上述"開發(fā)規(guī)范"的同事,那結(jié)果只能是呵呵呵呵呵了。下面直接切入正題吧!
上述流程雖說(shuō)有一定設(shè)計(jì),但沒(méi)有做到高內(nèi)聚、低耦合的原則,個(gè)人覺(jué)得不利于項(xiàng)目后期迭代和局部重構(gòu)。
推薦另一個(gè)設(shè)計(jì)原則:面向?qū)ο笪宕笤瓌tSOLID[3]
下面舉個(gè)例子:
接口里字段發(fā)生變更時(shí),如性別從Sex改為Gender;
前端內(nèi)部重構(gòu),發(fā)現(xiàn)數(shù)據(jù)模型不匹配時(shí),頁(yè)面C支持從頁(yè)面A附加參數(shù)a、或頁(yè)面B附加參數(shù)b跳入,重構(gòu)后頁(yè)面B1附加參數(shù)b1也要跳轉(zhuǎn)C。從設(shè)計(jì)來(lái)說(shuō)肯定是讓B1盡量按照以前B去適配時(shí)是最好的,否則C會(huì)越來(lái)越重。
上面提過(guò)不管是頁(yè)面交互,還是業(yè)務(wù)交互,最根本根本是數(shù)據(jù)的交換傳遞,從而去影響頁(yè)面和業(yè)務(wù)。數(shù)據(jù)就是串聯(lián)頁(yè)面和業(yè)務(wù)的核心,Model就是數(shù)據(jù)的表現(xiàn)形式。
再比如現(xiàn)在前后端分離的開發(fā)模式下,在需求確認(rèn)后,開發(fā)需要做的第一件事是數(shù)據(jù)庫(kù)設(shè)計(jì)和接口設(shè)計(jì),簡(jiǎn)單的說(shuō)就是字段的約定,然后在進(jìn)行頁(yè)面開發(fā),最終進(jìn)行接口調(diào)試和系統(tǒng)調(diào)試,一直到交付測(cè)試。這期間,后端需要執(zhí)行接口單元測(cè)試、前端需要Mock數(shù)據(jù)開發(fā)頁(yè)面。
目前筆記是通過(guò)JSON形式來(lái)進(jìn)行接口管理,在項(xiàng)目初始化時(shí),將配置的接口列表借助于 dva[4]注冊(cè)到Redux Action中,然后接口調(diào)用就直接發(fā)送Action即可。最終到拿到服務(wù)器響應(yīng)的Data。
接口配置(對(duì)應(yīng)下面第二版):
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); }, }, };
第二版使用高階函數(shù),同時(shí)支持服務(wù)器地址切換,減少冗余代碼:
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; };
上面看上去還不錯(cuò),解決了接口地址管理、封裝了接口請(qǐng)求,但自己還得處理返回Data里的異常數(shù)據(jù)。
另外的問(wèn)題是,接口和對(duì)應(yīng)的請(qǐng)求與相應(yīng)的數(shù)據(jù)Model并沒(méi)有對(duì)應(yīng)起來(lái),后面再次看代碼需要一段時(shí)間才能梳理業(yè)務(wù)邏輯。
請(qǐng)讀者思考一下上面的問(wèn)題,然后繼續(xù)往下看。
一個(gè)接口必然對(duì)應(yīng)唯一一個(gè)請(qǐng)求Model和唯一一個(gè)響應(yīng)Model。對(duì),沒(méi)錯(cuò)!下面利用此機(jī)制進(jìn)一步討論。
所以通過(guò)響應(yīng)Model去發(fā)起接口請(qǐng)求,在函數(shù)調(diào)用時(shí)也能利用請(qǐng)求Model判定入?yún)⒑喜缓侠?,這樣就把主角從接口切換到Model了。這里個(gè)人覺(jué)得優(yōu)先響應(yīng)Model比較合適,更能直接明白這次請(qǐng)求后拿到的數(shù)據(jù)格式。
下面先看看通過(guò)Model發(fā)起請(qǐng)求的代碼:
SimpleModel.get( { id: '1' }, { auth: false, onlyData: false }, ).then((data: ResponseData) => setTimeout( () => console.log( '設(shè)置返回全部數(shù)據(jù),返回 ResponseData 或 ResponseData ', typeof data, data, ), 2000, ), );
其中,SimpleModel是定義的響應(yīng)Model,第一個(gè)參數(shù)是請(qǐng)求,第二個(gè)參數(shù)是請(qǐng)求配置項(xiàng),接口地址被隱藏在SimpleModel內(nèi)部了。
import { Record } from 'immutable'; import { ApiOptons } from './Common'; import { ServiceManager } from './Service'; /** * 簡(jiǎn)單類型 */ 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, 'http://localhost:3000/test', // 被隱藏的接口地址 params, options, ); } static sexMap = { 0: '保密', 1: '男', 2: '女', }; sexText() { return SimpleModel.sexMap[this.sex] ?? '保密'; } }
這里借助了immutable里的Record[5],目的是將JSON Object反序列化為Class Object,目的是提高M(jìn)odel在項(xiàng)目中相關(guān)函數(shù)的內(nèi)聚。更多介紹請(qǐng)看我另外一篇文章:JavaScript的強(qiáng)語(yǔ)言之路—另類的JSON序列化與反序列化[6]。
// utils/tool.tsx export const sexMap = { 0: '保密', 1: '男', 2: '女', }; export const sexText = (sex: number) => { return sexMap[sex] ?? '保密'; };
直接在SimpleModel內(nèi)部用this訪問(wèn)具體數(shù)據(jù),比調(diào)用utils/tool函數(shù)時(shí)傳入外部參數(shù),更為內(nèi)聚和方便維護(hù)。通過(guò)這種思路,相信你可以創(chuàng)造更多"黑魔法"的語(yǔ)法糖!
接著我們來(lái)看看 Common 文件內(nèi)容:
/** * 接口響應(yīng),最外層統(tǒng)一格式 */ export class ResponseData{ code? = 0; message? = '操作成功'; toastId? = -1; data?: T; } /** * api配置信息 */ export class ApiOptons { headers?: any = {}; // 額外請(qǐng)求頭 loading?: boolean = true; // 是否顯示loading loadingTime?: number = 2; // 顯示loading時(shí)間 auth?: boolean = true; // 是否需要授權(quán) onlyData?: boolean = true; // 只返回data } /** * 枚舉接口能返回的類型 * - T、T[] 在 ApiOptons.onlyData 為true時(shí)是生效 * - ResponseData 、ResponseData 在 ApiOptons.onlyData 為false時(shí)是生效 * - ResponseData 一般在接口內(nèi)部發(fā)生異常時(shí)生效 */ export type ResultDataType = | T | T[] | ResponseData | ResponseData | ResponseData;
Service文件內(nèi)部是封裝了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); } } /** * 未知(默認(rèn))錯(cuò)誤碼 */ const codeUnknownTask = -999; /** * 接口請(qǐng)求封裝基類 */ export class InterfaceService { /** * todo */ private static userProfile: { sysToken?: '' } = {}; public static setUser(_user: any) { InterfaceService.userProfile = _user; } constructor(props: ApiOptons) { this.options = props; } /** * 默認(rèn)配置 */ public options = new ApiOptons(); /** * todo */ public get sysToken(): string { return InterfaceService.userProfile?.sysToken ?? ''; } /** * 構(gòu)建header */ public get headers(): Object { return { Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', 'app-info-key': 'xxx', // 自定義字段 }; } /** * 請(qǐng)求前置條件??筛鶕?jù)自己情況重構(gòu)此函數(shù) */ preCheck() { if (this.options.loading && this.options.loadingTime > 0) { return Toast.loading('加載中...', this.options?.loadingTime ?? 3); } return -1; } /** * 下載json,返回對(duì)象 */ public static async getJSON(url: string) { try { const res = await fetch(url); return await res.json(); } catch (e) { console.log(e); return {}; } } } /** * 接口請(qǐng)求封裝(axios版,也可以封裝其他版本的請(qǐng)求) */ export class InterfaceAxios extends InterfaceService { constructor(props: ApiOptons) { super(props); } /** * 封裝axios */ private request = (requestCfg: AxiosRequestConfig): Promise=> { return axios(requestCfg) .then(this.checkStatus) .catch((err: any) => { // 后臺(tái)接口異常,如接口不通、http狀態(tài)碼非200、data非json格式,判定為fatal錯(cuò)誤 console.log(requestCfg, err); return { code: 408, message: '網(wǎng)絡(luò)異常', }; }); }; /** * 檢查網(wǎng)絡(luò)響應(yīng)狀態(tài)碼 */ private checkStatus(response: AxiosResponse ) { if (response.status >= 200 && response.status < 300) { return response.data; } return { code: 408, message: '網(wǎng)絡(luò)數(shù)據(jù)異常', }; } /** * 發(fā)送POST請(qǐng)求 */ 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; } /** * 發(fā)送GET請(qǐng)求 */ 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 { /** * 檢查接口數(shù)據(jù) */ public hasNoError(res: ResponseData) { if (res.toastId > 0) { Toast.remove(res.toastId); } if (res?.code !== 0 && res.code !== codeUnknownTask) { Toast.info(res?.message ?? '服務(wù)器出錯(cuò)'); return false; } return true; } /** * 解析響應(yīng) */ public static parse ( modal: { new (x: any): T }, response: any, options: ApiOptons, ): ResultDataType { 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接口請(qǐng)求 */ public static async post ( modal: { new (x: any): T }, url: string, body?: any, options: ApiOptons = new ApiOptons(), ): Promise > { // 使用合并,減少外部傳入配置 options = Object.assign(new ApiOptons(), options); const request = new InterfaceAxios(options); if (options.auth && !request.sysToken) { return { code: 403, message: '未授權(quán)', }; } try { const response = await request.post(url, body); return ServiceManager.parse (modal, response, options); } catch (err) { // 記錄錯(cuò)誤日志 console.log(url, body, options, err); return { code: codeUnknownTask, message: '內(nèi)部錯(cuò)誤,請(qǐng)稍后再試', }; } } /** * get接口請(qǐng)求 */ public static async get ( modal: { new (x: any): T }, url: string, params?: any, options: ApiOptons = new ApiOptons(), ): Promise > { // 使用合并,減少外部傳入配置 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: '未授權(quán)', }; } try { const response = await a.get(url, params); return ServiceManager.parse (modal, response, options); } catch (err) { // 記錄錯(cuò)誤日志 console.log(url, params, options, err); return { code: codeUnknownTask, message: '內(nèi)部錯(cuò)誤,請(qǐng)稍后再試', }; } } }
Service文件里內(nèi)容有點(diǎn)長(zhǎng),主要有下面幾個(gè)類:
Toast:模擬請(qǐng)求接口時(shí)的loading,可通過(guò)接口調(diào)用時(shí)來(lái)配置;
InterfaceService:接口請(qǐng)求的基類,內(nèi)部記錄當(dāng)前用戶的Token、多環(huán)境服務(wù)器地址切換(代碼中未實(shí)現(xiàn))、單次請(qǐng)求的接口配置、自定義Header、請(qǐng)求前的邏輯檢查、直接請(qǐng)求遠(yuǎn)端JSON配置文件;
InterfaceAxios:繼承于InterfaceService,即axios版的接口請(qǐng)求,內(nèi)部發(fā)起實(shí)際請(qǐng)求。你可以封裝fetch版本的。
ServiceManager:提供給Model使用的請(qǐng)求類,傳入響應(yīng)Model和對(duì)應(yīng)服務(wù)器地址后,等異步請(qǐng)求拿到數(shù)據(jù)后再將相應(yīng)數(shù)據(jù)Data解析成對(duì)應(yīng)的Model。
下面再貼一下完整的Model發(fā)起請(qǐng)求示例:
import { ResponseData, ApiOptons, SimpleModel } from './model'; // 接口配置不同的三種請(qǐng)求 SimpleModel.get({ id: '1' }).then((data: ResponseData) => setTimeout( () => console.log( '因需授權(quán)導(dǎo)致內(nèi)部異常,返回 ResponseData:', typeof data, data, ), 1000, ), ); SimpleModel.get( { id: '1' }, { auth: false, onlyData: false }, ).then((data: ResponseData) => setTimeout( () => console.log( '設(shè)置返回全部數(shù)據(jù),返回 ResponseData 或 ResponseData ', typeof data, data, ), 2000, ), ); SimpleModel.get( { id: '1' }, { auth: false, onlyData: true }, ).then((data: SimpleModel) => setTimeout( () => console.log( '僅返回關(guān)鍵數(shù)據(jù)data,返回 T 或 T[]:', typeof data, data, data.sexText(), ), 3000, ), );
控制臺(tái)打印結(jié)果。注意,返回的 data 可能是JSON Object,也可能是 Immutable-js Record Object。
加載中... 2 加載中... 2 因需授權(quán)導(dǎo)致內(nèi)部異常,返回 ResponseData:object { code: 403, message: '未授權(quán)' } 設(shè)置返回全部數(shù)據(jù),返回 ResponseData或 ResponseData 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 } 僅返回關(guān)鍵數(shù)據(jù)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 } } 男
最后再補(bǔ)充一個(gè)常見(jiàn)的復(fù)合類型Model示例:
/** * 復(fù)雜類型 */ 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, // }; // 復(fù)合類型建議使用class,而不是上面的object。因?yàn)閛bject里不能添加可選屬性? class ComplexItemDefault { userNo = 'us1212'; userProfile = ComplexChildOneDefault; extraFirst? = ComplexChildTwoDefault; extraSecond? = ComplexChildThirdDefault; } // const ComplexListDefault = { // list: [], // pageNo: 1, // pageSize: 10, // pageTotal: 0, // }; // 有數(shù)組的復(fù)合類型,如果要指定數(shù)組元素的Model,就必須用class class ComplexListDefault { list: ComplexItemDefault[] = []; pageNo = 1; pageSize = 10; pageTotal = 0; } interface ComplexModelParams { id: string; } // 因?yàn)槭褂玫腸lass,所以需要 new 一個(gè)去初始化Record export class ComplexModel extends Record(new ComplexListDefault()) { static async get(params: ComplexModelParams, options?: ApiOptons) { return await ServiceManager.get( ComplexModel, 'http://localhost:3000/test2', params, options, ); } }
下面是調(diào)用代碼:
ComplexModel.get({ id: '2' }).then((data: ResponseData) => setTimeout( () => console.log( '因需授權(quán)導(dǎo)致內(nèi)部異常,返回 ResponseData:', typeof data, data, ), 1000, ), ); ComplexModel.get( { id: '2' }, { auth: false, onlyData: false }, ).then((data: ResponseData) => setTimeout( () => console.log( '設(shè)置返回全部數(shù)據(jù),返回 ResponseData 或 ResponseData ', typeof data, data.data.toJSON(), ), 2000, ), ); ComplexModel.get( { id: '2' }, { auth: false, onlyData: true }, ).then((data: ComplexModel) => setTimeout( () => console.log( '僅返回關(guān)鍵數(shù)據(jù)data,返回 T 或 T[]:', typeof data, data.toJSON(), ), 3000, ), );
接著是打印結(jié)果。這次Immutable-js Record Object就調(diào)用了data.toJSON()轉(zhuǎn)換成原始的JSON Object。
加載中... 2 加載中... 2 因需授權(quán)導(dǎo)致內(nèi)部異常,返回 ResponseData:object { code: 403, message: '未授權(quán)' } 設(shè)置返回全部數(shù)據(jù),返回 ResponseData或 ResponseData object { list: [ { userNo: '1', userProfile: [Object] } ], pageNo: 1, pageSize: 10, pageTotal: 0 } 僅返回關(guān)鍵數(shù)據(jù)data,返回 T 或 T[]:object { list: [ { userNo: '1', userProfile: [Object] } ], pageNo: 1, pageSize: 10, pageTotal: 0 }
感謝各位的閱讀,以上就是“TypeScript在Model中的高級(jí)應(yīng)用”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)TypeScript在Model中的高級(jí)應(yīng)用這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!