自上次分享了「TS - 裝飾器與註解」後,有幾個小夥伴私信問我在在真實的工作中是如何使用這種寫法的,在瞭解到很多小夥伴並沒有接觸到相關使用後,專門整理這份使用案例來分享給大家,希望能幫助到有需要的開發者
需要強調的一點是在Typescript中不是註解而是稱為裝飾器,由於在Java中註解說習慣了,這裡我就順便這樣寫了,二者還是有很大區別的,可以檢視我的往期文章瞭解更多
文章首發公眾號,掃碼檢視等多優質內容
註解意義
註解本身就是一種設計模式的提現,它的存在當然有很必要的意義的
試想一下,你正在開發一個應用,面對大量重複的配置和硬編碼邏輯,難免感到心煩意亂。這時,註解就像一位貼心的助手,它幫你標記出關鍵點,自動完成那些繁瑣的操作。你只需專注於業務邏輯,其餘的交給它來處理——既省時又省力,讓開發變得更加優雅!
註解是一種以後設資料形式描述程式碼行為的設計模式,透過在類、方法、屬性或引數上標記特定資訊,實現邏輯的擴充套件或控制。它不僅能提升程式碼的語義化表達,讓邏輯更清晰易讀,還可以解耦業務邏輯與配置邏輯,避免繁瑣的手動操作。註解的廣泛應用,使複雜功能的實現更加靈活高效,例如依賴注入、許可權校驗、日誌記錄等場景,都能透過註解輕鬆完成
總之可以總結為以下幾點:
- 提高程式碼清晰度:透過語義化的註解,讓程式碼更加直觀易讀
- 降低耦合度:解耦核心邏輯和配置邏輯,使程式碼更靈活可擴充套件
- 增強程式碼複用性:將通用功能封裝為註解,減少重複程式碼
- 支援動態功能擴充套件:透過註解結合反射和 AOP 實現動態程式設計
- 簡化框架使用:降低開發者使用框架的學習曲線,提高開發效率
- 便於維護和擴充套件:透過調整註解後設資料,快速適應需求變化
使用約束
在 TypeScript 中使用裝飾器有一些限制和約束,這是由於 TypeScript 的設計規範以及裝飾器本身的特性決定的。以下是使用裝飾器時需要注意的主要約束:
- 裝飾器的目標有限,不支援在函式外獨立使用裝飾器,只能在類、方法、屬性、引數中使用
- 由於是在Runtime階段的動態修改,因此不支援型別的靜態檢查
- 裝飾器只能為屬性設定後設資料,而不能直接訪問或修改其值
- 結合Reflect後設資料可能會對應能有一定的影響
接下來我們就進入正文,透過一些案例來了解如何使用它
NestJS
對於大多數人而言註解使用通常都會在NestJS中開始的,它被稱為前端版本的Spring框架,正因為NestJS框架優秀的設計模式,它也是嚴格意義上的NodeJS框架;這裡我們以它作為入手參考
在NestJS中會看到很多註解使用形式,以下程式碼為簡化程式碼:
定義控制器:
@Controller("/api/material")
export class MaterialController {
constructor(private materialService: MaterialService) {}
@Get("categories")
async findMaterialCategory(
@Query() query: QueryMaterialCategoryDTO,
@Query() pagination: PaginationDTO,
@Query() order: OrderDTO
) {
return await this.materialService.findMaterialCategory(query, pagination, order);
}
}
定義service:
@Injectable()
export class MaterialService {
constructor(
@InjectRepository(MaterialCategoryEntity)
private materialCategoryRepository: Repository<MaterialCategoryEntity>,
@InjectRepository(MaterialEntity)
private materialRepository: Repository<MaterialEntity>
) {}
async findMaterialCategory(query: QueryMaterialCategoryDTO, pagination: PaginationDTO, order: OrderDTO): IResult<IPager<MaterialCategory>> {
return null;
}
}
使用NestJS不光要學會如何使用,知道其背後的執行邏輯意義更大;上面程式碼分別定義了一個控制器和服務層,控制器用來定義路由功能,服務層用來查詢資料,透過將他們分開寫可以提高健壯性,實現低耦合,當然這也是後端傳統的MC寫法方式
我們知道以上程式碼就完成了路由註冊,那麼你知道NestJS是怎麼知道的嗎❓
其實,它很簡單,如果你看了我上一篇的文章,那麼應該就會明白,肯定有一個存放路由配置的容器,然後NestJS從中拿到對應的配置就可以,大體邏輯就是這樣。那麼實現邏輯就在 對應的註解邏輯中,如:Controller
、Get
等等,下面我們用虛擬碼簡單實現下:
// 存放模組名
const MODULE_ROUTES = new Map<string, string>();
// 存放具體定義的路由
const APP_ROUTES = new Map<string, { method: string; handler: Function }>();
export function Controller(modulePath: string = '') {
return (target: Constructor) => {
MODULE_ROUTES.set(target.name, modulePath);
return target;
};
}
export function Get(path: string = '') {
return function (target: InstanceType<Constructor>, propertyKey: string) {
const moduleName = target.constructor.name;
APP_ROUTES.set(`%${moduleName}%${path}`, {
method: 'get',
handler: target[propertyKey]
});
};
}
// 假設這是我們定義的APP路由模組
@Controller("/app")
export class AppController {
@Get('/home')
home() {
return 'home';
}
}
上面已經把Controller
和Get
的核心定義好了,但是儲存的路由還是不能用的,需要簡單的進行處理後才可以交給應用進行處理
// 這是維護的應用路由表
const router: Record<string, {[key in string]: Function}> = {};
// 生成最終的路由表
function generateRouter() {
for (const [moduleName, modulePath] of MODULE_ROUTES) {
for (const [routePath, {method, handler}] of APP_ROUTES) {
const routePrefix = `%${moduleName}%`;
const isCurrentModule = routePath.startsWith(routePrefix);
if (!isCurrentModule) continue;
const currentPath = routePath.replace(routePrefix, modulePath);
if (!router[currentPath]) router[currentPath] = {};
router[currentPath][method] = handler;
}
}
return router;
}
透過上面的程式碼我們就可以真正的做到了路由的自動註冊和獲取,接下來我們來模擬一下他的效果:
const router = generateRouter();
// 假設這是HTTP請求的路徑
const requestPath = '/app/home';
const requestMethod = 'get';
const matchedRoute = router[requestPath];
const result = matchedRoute[requestMethod]();
console.log(result)
expect(result).toBe('home');
結果符合預期‼️ 以上只是簡單的模擬路由的處理思路,當然有很多細節還是要處理的。讀者可以將以上程式碼全部複製到一個檔案中,執行就可以看到結果
總之可以發現以上使用註解讓程式設計更加簡單,透過上面案例小夥伴們應該也瞭解了它的優勢,接下來我們就來看看其他專案中如何使用
HTTP請求
在專案中通常都會將同模組的內容放在一起,網路請求也是可以的。我們可以模仿Spring的Controller,同類的Controller放在一個class中,裡面放當前模組的RESTful,這也是最佳的實踐。有了Controller類後,註解就可以無限發揮了
RESTful
以下為一個簡單的user
模組的請求:
@Controller("/user")
export class UserController extends BaseRequest {
@Get("/:id")
async getUserInfo(id: number, config?: IHttpRequestConfig) {
return this.request({ urlPath: { id }, captureError: false, ...config });
}
}
在上文已經對NestJS的路由實現已經做了簡單的實現了,這裡詳細實現就不做演示了,思路都是一致的,以上request
例項為axios
CancelToken
CancelToken主要是用來取消掉重複的請求的,減少不必要的網路請求。它是axios賦予的功能,其本質原理還是AbortController的作用
@Controller("/user")
export class UserController extends BaseRequest {
@Get("/:id")
@Token() // 這裡增加Token,用來標識當前請求可以隨時被取消
async getUserInfo(id: number, config?: IHttpRequestConfig) {
return this.request({ urlPath: { id }, captureError: false, ...config });
}
}
// Token的實現
export function Token(signal?: AbortSignal) {
return function (target: ICtor): InstanceType<typeof target> {
return class extends target {
async send() {
if (!this.requestConfig.ignoreCancelToken) {
this.requestConfig.signal = signal || this.requestConfig.signal;
CancelToken.instance.register(this.requestConfig);
}
const res = await super.send();
CancelToken.instance.cancel(this.requestConfig);
return res;
}
};
};
}
以上CancelToken
類主要邏輯就是來存放當前請求的唯一標識,這裡不展開了;當上面的Token加上後當請求的內容一模一樣時前面的請求就會被終止掉
思考:終止後的請求伺服器還會收到嗎?
錯誤碼
每個介面的請求除了真正意義上的錯誤外,通常為了方便提示使用者都會有相應的錯誤碼,客戶端可以根據錯誤碼來提示不同的文案
@Controller("/user")
export class UserController extends BaseRequest {
@Post("/:id/avatar")
@CaptureError(UploadUserAvatarErrorMessage) // 新增錯誤碼註解
async uploadUserAvatar(id: number, config?: IHttpRequestConfig) {
return await request({ urlPath: { id }, captureError: false, ...config });
}
}
// 註解實現
export function CaptureError(errors: IDict) {
return function (target: object, key: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
const res = await origin.call(this, ...args);
return res;
} catch (err) {
throw new BaseError(errors, err);
}
};
};
}
export class BaseError extends Error {
message: string;
constructor(errorCodeMessageMap: IDict, error: any) {
super();
const code = error.code;
const codeMsg = errorCodeMessageMap[code];
if (!code || !codeMsg) this.message = error.message;
else this.message = errorCodeMessageMap[code];
}
}
上面就是通用的處理錯誤碼的邏輯,接著繼續完善下錯誤碼的提示:
/** 上傳使用者頭像狀態碼,包含了多語言 */
export const UploadUserAvatarErrorMessage = {
404: i18n.global.t("error.NOT_FOUND"),
};
// 模擬頁面請求
const userController = new UserController();
try {
const res = await userController.uploadUserAvatar({ id: 1 });
console.log(res);
} catch (err) {
Toast(err.message);
}
實體類
瞭解後端框架的同學應該都知道ORM思想,透過這種方式可以快速方便的將資料轉換成目標物件,並且可以做一些格式處理,那麼在前端如如何使用呢?
相信大家在工作中都是使用以下方式進行資料的處理的
// 定義介面資料模型
interface IUser {
name: string;
age: number;
date: string;
cars: Car[];
}
interface Car {
name: string;
color: string;
}
// 建立表單物件
function createUserFormModel(): IUser {
return {
name: null,
age: null,
date: null,
cars: null
};
}
// 在頁面中使用
const userModel = createUserFormModel();
<div>
<Input value={userModel.name} />
// ...
</div>
以上便是一個最基礎的頁面表單的提交過程,然而實際情況會面臨很多複雜的情況,如格式轉換、輔助屬性等等,如果欄位變多後表單的處理將會變得非常臃腫,而且使用interface
方式後還得重新寫一遍所有的欄位屬性,效率上大打折扣
接下來我們透過以下幾個步驟來簡化使用
- 使用
class
代替interface
class User {
name: string;
age: number;
date: string;
cars: Car[];
}
class Car {
name: string;
color: string;
}
// 建立表單物件直接new即可
const user = new User();
但直接透過new
方式不能準確獲得型別提示,並且預設屬性值都是undefined
,如果有要特殊處理的初始值將會變得麻煩,因此我們可以透過代理工廠來建立目標物件
export function createEntity<T extends ICtor>(Ctor: T, initValue?: Partial<InstanceType<T>>): InstanceType<T> {
return new Ctor(initValue);
}
const user = createEntity(User, { /* 這裡就有了型別提示 */ });
透過以上的方式就有了基本的實體類,有了類就可以透過註解施加一點魔法了,透過註解我們慢慢來補充一些通用的功能:
@Entity // 標識這是個實體類
class User {
name: string;
@ToJSON<Student, "age">((_, v) => (v ? v >> 0 : undefined))
age: number;
@FromJSON<Student>((_, v) => (v ? dayjs(parse(v, "yy/MM-dd HH", new Date())) : null))
date: string;
@FieldType(Array(Car))
cars: Car[];
}
@Entity
class Car {
name: string;
color: string;
}
上面簡單的補充下了一些註解,對於其內部的邏輯其實也很簡單,只需要建立相應的容器存放對應的邏輯即可,這裡不做具體介紹;希望讀者可以自己仔細研究下如何實現,這裡有個IOC線上演示
日誌
日誌就比較常見了,透過註解也可以很方便的做一些log了,這裡簡單的演示下,讀者可以自行腦補
function Logger(...args) {
return function (target: any, property: string) {
console.log(target, property);
};
}
class UserConstroller {
@Logger()
getUserInfo() {}
}
其他
以上我們介紹了很多前端專案的註解使用場景,除了這些還有很多地方可以使用,如在設計一些框架或庫時,註解模式也是一種很巧妙的設計實現方式;其發揮的作用還是很廣泛的,取決於每一位開發者的設計思想
總結
本篇文章透過介紹不同的場景下使用ts的註解功能,大大提升了編碼效率。裝飾器是 TypeScript 提供的一種用於擴充套件類、方法、屬性或引數行為的語法,主要透過超程式設計的方式實現功能增強。它是一種宣告性語法,可以透過標註的方式減少重複程式碼,實現邏輯的分離和靈活性擴充套件