深入淺出TypeScript在Model中的高級應(yīng)用
前言
在MVC、MVVC等前端經(jīng)典常用開發(fā)模式中,V、C往往是重頭戲,可能是前端業(yè)務(wù)主要集中這兩塊。結(jié)合實(shí)際業(yè)務(wù),筆者更喜歡路由模式、插件式設(shè)計(jì),這種在迭代和維護(hù)上更能讓開發(fā)者受益(不過你需要找PM協(xié)調(diào)這事,畢竟他們理解的簡化用戶體驗(yàn),多半是怎么讓用戶操作簡單)。但我們今天來看看Model,看看M有什么擴(kuò)展的可能。
背景
在讀到本文之前,你實(shí)際項(xiàng)目(如React+Redux)中請求服務(wù)器數(shù)據(jù),可能是如下策略:
- componentDidMount 中發(fā)送redux action請求數(shù)據(jù);
- 在action中發(fā)起異步網(wǎng)絡(luò)請求,當(dāng)然你已經(jīng)對網(wǎng)絡(luò)請求有一定封裝;
- 在網(wǎng)絡(luò)請求內(nèi)部處理一定異常和邊際邏輯,然后返回請求到的數(shù)據(jù);
- 拿到數(shù)據(jù)this.setState刷新頁面,同時可能存一份到全局redux中;
正常情況下,一個接口對應(yīng)至少一個接口相應(yīng)Model,萬一你還定義了接口請求的Model、一個頁面有5個接口呢?
如果項(xiàng)目已經(jīng)引入TypeScript,結(jié)合編寫Model,你的編寫體驗(yàn)肯定會如行云流水般一氣呵成!但實(shí)際開發(fā)中,你還需要對服務(wù)器返回的數(shù)據(jù)、頁面間傳遞的參數(shù)等涉及到數(shù)據(jù)傳遞的地方,做一些數(shù)據(jù)額外工作:
- 對null、undefined等空值的異常處理(在ES最新方案和TS支持里,新增:鏈?zhǔn)秸{(diào)用?和運(yùn)算符??,請讀者自行查詢使用手冊);
- 對sex=0、1、2,time=1591509066等文案轉(zhuǎn)義;
- (還有其他嗎?歡迎留言補(bǔ)充)
作為一個優(yōu)秀且成熟的開發(fā)者,你肯定也已經(jīng)做了上述額外的工作,在utils文件下編寫了幾十甚至上百的tool類函數(shù),甚至還根據(jù)函數(shù)用途做了分類:時間類、年齡性別類、數(shù)字類、......,接著你在需要的地方import,然后你開始進(jìn)行傳參調(diào)用。是的,一切看上去都很完美!
上面這個流程說的就是筆者本人,:)。
現(xiàn)況
隨著項(xiàng)目和業(yè)務(wù)的迭代,加上老板還是壓時間,最壞的情況是你遇到了并沒有遵守上述"開發(fā)規(guī)范"的同事,那結(jié)果只能是呵呵呵呵呵了。下面直接切入正題吧!
上述流程雖說有一定設(shè)計(jì),但沒有做到高內(nèi)聚、低耦合的原則,個人覺得不利于項(xiàng)目后期迭代和局部重構(gòu)。
推薦另一個設(shè)計(jì)原則:面向?qū)ο笪宕笤瓌tSOLID[3]
下面舉個例子:
- 接口里字段發(fā)生變更時,如性別從Sex改為Gender;
- 前端內(nèi)部重構(gòu),發(fā)現(xiàn)數(shù)據(jù)模型不匹配時,頁面C支持從頁面A附加參數(shù)a、或頁面B附加參數(shù)b跳入,重構(gòu)后頁面B1附加參數(shù)b1也要跳轉(zhuǎn)C。從設(shè)計(jì)來說肯定是讓B1盡量按照以前B去適配時是最好的,否則C會越來越重。
上面提過不管是頁面交互,還是業(yè)務(wù)交互,最根本根本是數(shù)據(jù)的交換傳遞,從而去影響頁面和業(yè)務(wù)。數(shù)據(jù)就是串聯(lián)頁面和業(yè)務(wù)的核心,Model就是數(shù)據(jù)的表現(xiàn)形式。
再比如現(xiàn)在前后端分離的開發(fā)模式下,在需求確認(rèn)后,開發(fā)需要做的第一件事是數(shù)據(jù)庫設(shè)計(jì)和接口設(shè)計(jì),簡單的說就是字段的約定,然后在進(jìn)行頁面開發(fā),最終進(jìn)行接口調(diào)試和系統(tǒng)調(diào)試,一直到交付測試。這期間,后端需要執(zhí)行接口單元測試、前端需要Mock數(shù)據(jù)開發(fā)頁面。
如何解決
接口管理
目前筆記是通過JSON形式來進(jìn)行接口管理,在項(xiàng)目初始化時,將配置的接口列表借助于 dva[4] 注冊到Redux Action中,然后接口調(diào)用就直接發(fā)送Action即可。最終到拿到服務(wù)器響應(yīng)的Data。
接口配置(對應(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ù),同時支持服務(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;
- };
上面看上去還不錯,解決了接口地址管理、封裝了接口請求,但自己還得處理返回Data里的異常數(shù)據(jù)。
另外的問題是,接口和對應(yīng)的請求與相應(yīng)的數(shù)據(jù)Model并沒有對應(yīng)起來,后面再次看代碼需要一段時間才能梳理業(yè)務(wù)邏輯。
請讀者思考一下上面的問題,然后繼續(xù)往下看。
Model管理
一個接口必然對應(yīng)唯一一個請求Model和唯一一個響應(yīng)Model。對,沒錯!下面利用此機(jī)制進(jìn)一步討論。
所以通過響應(yīng)Model去發(fā)起接口請求,在函數(shù)調(diào)用時也能利用請求Model判定入?yún)⒑喜缓侠恚@樣就把主角從接口切換到Model了。這里個人覺得優(yōu)先響應(yīng)Model比較合適,更能直接明白這次請求后拿到的數(shù)據(jù)格式。
下面先看看通過Model發(fā)起請求的代碼:
- SimpleModel.get(
- { id: '1' },
- { auth: false, onlyData: false },
- ).then((data: ResponseData<SimpleModel>) =>
- setTimeout(
- () =>
- console.log(
- '設(shè)置返回全部數(shù)據(jù),返回 ResponseData<T> 或 ResponseData<T[]>',
- typeof data,
- data,
- ),
- 2000,
- ),
- );
其中,SimpleModel是定義的響應(yīng)Model,第一個參數(shù)是請求,第二個參數(shù)是請求配置項(xiàng),接口地址被隱藏在SimpleModel內(nèi)部了。
- 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,目的是提高M(jìn)odel在項(xiàng)目中相關(guān)函數(shù)的內(nèi)聚。更多介紹請看我另外一篇文章:JavaScript的強(qiáng)語言之路—另類的JSON序列化與反序列化[6]。
- // utils/tool.tsx
- export const sexMap = {
- 0: '保密',
- 1: '男',
- 2: '女',
- };
- export const sexText = (sex: number) => {
- return sexMap[sex] ?? '保密';
- };
直接在SimpleModel內(nèi)部用this訪問具體數(shù)據(jù),比調(diào)用utils/tool函數(shù)時傳入外部參數(shù),更為內(nèi)聚和方便維護(hù)。通過這種思路,相信你可以創(chuàng)造更多"黑魔法"的語法糖!
接著我們來看看 Common 文件內(nèi)容:
- /**
- * 接口響應(yīng),最外層統(tǒng)一格式
- */
- 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; // 是否需要授權(quán)
- onlyData?: boolean = true; // 只返回data
- }
- /**
- * 枚舉接口能返回的類型
- * - T、T[] 在 ApiOptons.onlyData 為true時是生效
- * - ResponseData<T>、ResponseData<T[]> 在 ApiOptons.onlyData 為false時是生效
- * - ResponseData 一般在接口內(nèi)部發(fā)生異常時生效
- */
- export type ResultDataType<T> =
- | T
- | T[]
- | ResponseData<T>
- | ResponseData<T[]>
- | 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))錯誤碼
- */
- 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;
- }
- /**
- * 默認(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', // 自定義字段
- };
- }
- /**
- * 請求前置條件??筛鶕?jù)自己情況重構(gòu)此函數(shù)
- */
- 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狀態(tài)碼非200、data非json格式,判定為fatal錯誤
- console.log(requestCfg, err);
- return {
- code: 408,
- message: '網(wǎng)絡(luò)異常',
- };
- });
- };
- /**
- * 檢查網(wǎng)絡(luò)響應(yīng)狀態(tài)碼
- */
- private checkStatus(response: AxiosResponse<ResponseData>) {
- if (response.status >= 200 && response.status < 300) {
- return response.data;
- }
- return {
- code: 408,
- message: '網(wǎng)絡(luò)數(shù)據(jù)異常',
- };
- }
- /**
- * 發(fā)送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;
- }
- /**
- * 發(fā)送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 {
- /**
- * 檢查接口數(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ù)器出錯');
- return false;
- }
- return true;
- }
- /**
- * 解析響應(yīng)
- */
- 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: '未授權(quán)',
- };
- }
- 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: '內(nèi)部錯誤,請稍后再試',
- };
- }
- }
- /**
- * 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: '未授權(quán)',
- };
- }
- 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: '內(nèi)部錯誤,請稍后再試',
- };
- }
- }
- }
Service文件里內(nèi)容有點(diǎn)長,主要有下面幾個類:
- Toast:模擬請求接口時的loading,可通過接口調(diào)用時來配置;
- InterfaceService:接口請求的基類,內(nèi)部記錄當(dāng)前用戶的Token、多環(huán)境服務(wù)器地址切換(代碼中未實(shí)現(xiàn))、單次請求的接口配置、自定義Header、請求前的邏輯檢查、直接請求遠(yuǎn)端JSON配置文件;
- InterfaceAxios:繼承于InterfaceService,即axios版的接口請求,內(nèi)部發(fā)起實(shí)際請求。你可以封裝fetch版本的。
- ServiceManager:提供給Model使用的請求類,傳入響應(yīng)Model和對應(yīng)服務(wù)器地址后,等異步請求拿到數(shù)據(jù)后再將相應(yīng)數(shù)據(jù)Data解析成對應(yīng)的Model。
下面再貼一下完整的Model發(fā)起請求示例:
- import { ResponseData, ApiOptons, SimpleModel } from './model';
- // 接口配置不同的三種請求
- 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<SimpleModel>) =>
- setTimeout(
- () =>
- console.log(
- '設(shè)置返回全部數(shù)據(jù),返回 ResponseData<T> 或 ResponseData<T[]>',
- 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,
- ),
- );
控制臺打印結(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<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
- }
- 僅返回關(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ǔ)充一個常見的復(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 一個去初始化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,
- );
- }
- }
下面是調(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<ComplexModel>) =>
- setTimeout(
- () =>
- console.log(
- '設(shè)置返回全部數(shù)據(jù),返回 ResponseData<T> 或 ResponseData<T[]>',
- 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<T> 或 ResponseData<T[]> 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
- }
總結(jié)
本文的代碼地址:
https://github.com/stelalae/node_demo,歡迎follow me~
現(xiàn)在接口調(diào)用是不是很優(yōu)雅?!只關(guān)心請求和影響的數(shù)據(jù)格式,多使用高內(nèi)聚低耦合,這對項(xiàng)目持續(xù)迭代非常有幫助的。使用TypeScript和Immutable-js來處理數(shù)據(jù),在大型應(yīng)用中越來越深入,從數(shù)據(jù)管理出發(fā)可以優(yōu)化上層UI顯示和業(yè)務(wù)邏輯。