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

来源:https://www.cnblogs.com/chengxuyuanaa/archive/2020/06/12/13110461.html
-Advertisement-
Play Games

TypeScript在Model中的高級應用 在MVC、MVVC等前端經典常用開發模式中,V、C往往是重頭戲,可能是前端業務主要集中這兩塊。結合實際業務,筆者更喜歡路由模式、插件式設計,這種在迭代和維護上更能讓開發者收益(不過你需要找PM協調這事,畢竟他們理解的簡化用戶體驗,多半是怎麼讓用戶操作簡單 ...


TypeScript在Model中的高級應用

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

如果讀者熟悉iOS開發,應該聽過VIPER開發模式,下麵推薦

背景

在讀到本文之前,你實際項目(如React+Redux)中請求伺服器數據,可能是如下策略:

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

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

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

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

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

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

現況

隨著項目和業務的迭代,加上老闆還是壓時間,最壞的情況是你遇到了並沒有遵守上述"開發規範"的同事,那結果只能是呵呵呵呵呵了。下麵直接切入正題吧!

上述流程雖說有一定設計,但沒有做到高內聚、低耦合的原則,個人覺得不利於項目後期迭代和局部重構。

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

下麵舉個例子:

  • 介面里欄位發生變更時,如性別從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…

本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.啟動oracle埠監聽 lsnrctl start 2.啟動資料庫 sqlplus / as sysdba 回車 開啟資料庫:startup 關閉監聽 lsnrctl stop 關閉資料庫服務 shutdown ...
  • 一、findViewById函數和Toast類 (1)Button button1 = (Button) findViewById(R.id.button_1); 通過findViewById方法,去佈局中找出R.id.button_1名稱的Button按鈕,這個方法會返回一個View對象,然後進行 ...
  • 原文在這裡 作者簡介:Jose,剛大學畢業,現帶領團隊負責維護Flutter的Material庫, Material是一個幫助團隊建設高質量用戶體驗的設計體系。 假設你的ui里有一個widget,並且您希望在該widget的頂部覆蓋一個浮動widget。 可能該widget被旋轉或應用了其他轉換,如 ...
  • 大家都知道,子線程不能操作UI控制項,在我們Android的日常開發中,經常會遇到網路請求數據通過線程間通信,將數據發送到UI線程中去更新UI,在Flutter中是如何處理非同步更新UI的呢? 今天我們介紹一下FutureBuilder。 ...
  • java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object ...
  • 耗電優化最終目的:通過儘可能降低CPU、GPU功耗來降低手機電量消耗。 (1)儘可能少用定時器; (2)優化I/O操作(所謂的I/O操作也就是文件操作,我們簡稱為I/O操作。怎麼優化呢?儘量不要頻繁寫入小數據,最好批量一次性寫入。讀寫大量主要的數據時,考慮用dispatch_io,其提供了基於GCD ...
  • Hilt是Google Android官方新推薦的依賴註入工具. 基於Dagger, 面向Android定製, 可以和Dagger共存. ...
  • CSS:頁面美化和佈局控制 一、CSS的使用:CSS與html結合使用方式 1.內聯樣式 * 在標簽內使用style屬性指定css代碼,如: <div style="color:red;">hello css</div> 2.內部樣式 * 在head標簽內,定義style標簽,style標簽的標簽體 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...