在MVC、MVVC等前端經典經常使用開發模式中,V、C每每是重頭戲,多是前端業務主要集中這兩塊。結合實際業務,筆者更喜歡路由模式、插件式設計,這種在迭代和維護上更能讓開發者收益(不過你須要找PM協調這事,畢竟他們理解的簡化用戶體驗,多半是怎麼讓用戶操做簡單)。但咱們今天來看看Model,看看M有什麼擴展的可能。前端
若是讀者熟悉iOS開發,應該聽過VIPER開發模式,下面推薦java
在讀到本文以前,你實際項目(如React+Redux)中請求服務器數據,多是以下策略:node
正常狀況下,一個接口對應至少一個接口響應Model,萬一你還定義了接口請求的Model、一個頁面有5個接口呢?ios
若是項目已經引入TypeScript,結合編寫Model,你的編寫體驗確定會如行雲流水般一鼓作氣!但實際開發中,你還須要對服務器返回的數據、頁面間傳遞的參數等涉及到數據傳遞的地方,作一些數據額外工做:git
做爲一個優秀且成熟的開發者,你確定也已經作了上述額外的工做,在utils文件下編寫了幾十甚至上百的tool類函數,甚至還根據函數用途作了分類:時間類、年齡性別類、數字類、......,接着你在須要的地方import,而後你開始進行傳參調用。是的,一切看上去都很完美!github
上面這個流程說的就是筆者本人,:)。typescript
隨着項目和業務的迭代,加上老闆仍是壓時間,最壞的狀況是你遇到了並無遵照上述"開發規範"的同事,那結果只能是呵呵呵呵呵了。下面直接切入正題吧!數據庫
上述流程雖然說有必定設計,但沒有作到高內聚、低耦合的原則,我的以爲不利於項目後期迭代和局部重構。json
推薦另外一個設計原則:面向對象五大原則SOLID
下面舉個例子:
上面提過不論是頁面交互,仍是業務交互,最根本根本是數據的交換傳遞,從而去影響頁面和業務。 數據就是串聯頁面和業務的核心,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發起請求的代碼:
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文件裏內容有點長,主要有下面幾個類:
下面再貼一下完整的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
}
複製代碼
本文的代碼地址:github.com/stelalae/no…,歡迎follow me~
如今接口調用是否是很優雅?!只關心請求和影響的數據格式,多使用高內聚低耦合,這對項目持續迭代很是有幫助的。使用TypeScript和Immutable-js來處理數據,在大型應用中愈來愈深刻,從數據管理出發能夠優化上層UI顯示和業務邏輯。
參考地址: