نحوه نوشتن کلاینت API مناسب در TypeScript

در این مقاله، من به طور مفصل در مورد اجرای API مشتری در TypeScript برای کار با API های شخص ثالث و خودم صحبت خواهم کرد. کلاینت می تواند با نقاط پایانی عمومی و محافظت شده کار کند و به یک فریم ورک خاص وابسته نیست، که آن را برای استفاده در React، Vue، Svelte و سایر فریم ورک ها مناسب می کند.
ایجاد یک برنامه پیچیده تر از لیست ToDo است، اغلب ما نیاز به تعامل با برخی از داده های ذخیره شده در سرور داریم. اینها میتوانند پیشبینیهای آبوهوای پردازششده توسط API شخص ثالث، و همچنین دادههای مشتریان ما، اعم از ورود به سیستم و رمز عبور آنها یا لیست خرید در فروشگاه باشد. هنگام کار با یک برنامه SPA (برنامه تک صفحه ای)، باید این داده ها را از سمت مشتری دریافت، اصلاح و ارسال کنیم. بنابراین، باید نوعی لایه مسئول تعامل با سرور داشته باشید. در این مقاله، استفاده از کلاینت API را با کتابخانه React در نظر خواهیم گرفت، اگرچه می توان آن را با خیال راحت در همان Vue، Svelte و غیره استفاده کرد.
چرا همه پرس و جوها را در مؤلفه هایی که در آنها استفاده می شود ثبت نمی کنید؟
ساده است: اگر رابط API را که با آن کار می کنید تغییر دهید، باید تمام کدها را مرور کنید و تمام نقاط تغییری را که روی آن تأثیر گذاشته است را پیدا کنید. می توانید سعی کنید این منطق را در قلاب های React قرار دهید، زیرا اکنون در مورد آن صحبت می کنیم، اما این راه حل در پروژه های دیگر با فریمورک های دیگر قابل استفاده نخواهد بود.
پیاده سازی TypeScript
برای شروع، ما دامنه هایی را که API در آن قرار دارد در نوعی پیکربندی قرار می دهیم که با آن کار می کند .env
فایل:
REACT_APP_API_BASE_URL="http://localhost:8083"
export default {
get apiBaseUrl(): string {
return process.env.REACT_APP_API_BASE_URL || "";
},
}
سپس ما خود یک کلاینت انتزاعی می نویسیم، نه به این دامنه. نیاز خواهد داشت محورها و axios-extensions کتابخانه ها به کار
کد مشتری:
import axios, {AxiosInstance, AxiosRequestConfig} from "axios";
import {
Forbidden,
HttpError,
Unauthorized
} from '../errors';
import {Headers} from "../types";
export class ApiClient {
constructor(
private readonly baseUrl: string,
private readonly headers: Headers,
private readonly authToken: string = ""
) {}
public async get(endpoint: string = "", params?: any, signal?: AbortSignal): Promise<any> {
try {
const client = this.createClient(params);
const response = await client.get(endpoint, { signal });
return response.data;
} catch (error: any) {
this.handleError(error);
}
}
public async post(endpoint: string = "", data?: any, signal?: AbortSignal): Promise<any> {
try {
const client = this.createClient();
const response = await client.post(endpoint, data, { signal });
return response.data;
} catch (error) {
this.handleError(error);
}
}
public async uploadFile(endpoint: string = "", formData: FormData): Promise<any> {
try {
const client = this.createClient();
const response = await client.post(endpoint, formData, {
headers: {
"Content-Type": "multipart/form-data",
}
})
return response.data;
} catch (error) {
this.handleError(error);
}
}
private createClient(params: object = {}): AxiosInstance {
const config: AxiosRequestConfig = {
baseURL: this.baseUrl,
headers: this.headers,
params: params
}
if (this.authToken) {
config.headers = {
Authorization: `Bearer ${this.authToken}`,
}
}
return axios.create(config);
}
private handleError(error: any): never {
if (!error.response) {
throw new HttpError(error.message)
} else if (error.response.status === 401) {
throw new Unauthorized(error.response.data);
} else if (error.response.status === 403) {
throw new Forbidden(error.response.data);
} else {
throw error
}
}
}
مشتری از انواع سفارشی مانند Headers
، که در واقع فقط یک فرهنگ لغت است [key: string]: رشتهو خطاهای مختلفی که جهانی را به ارث می برند Error
کلاس (غیر مجاز، ممنوع، HTTPEerror)، به طوری که در آینده درک علت آنها آسانتر خواهد بود.
این کلاس فقط سه متد عمومی دارد که هر بار که از آنها استفاده می شود یک کلاینت axios ایجاد می کند. این کلاینت می تواند با افزودن هدر با توکن حامل، هم با نقاط پایانی API عمومی و هم با نقاط محافظت شده کار کند. نحوه دریافت این رمز توسط مشتری بعداً مورد بحث قرار خواهد گرفت. هر دو روش دریافت و ارسال از گزینه اختیاری استفاده می کنند abort Signal
پارامتر، که به شما امکان می دهد بسته به اقدامات کاربر، ارسال درخواست را قطع کنید.
در صورت ارسال هر فایلی به سرور، کلاینت از uploadFile()
روش، ارسال درخواست به سرور با نوع محتوا: چند بخشی/فرم-داده سرتیتر.
برای محصور کردن منطق ایجاد این مشتریان، ما یک کارخانه می نویسیم.
کد کارخانه:
import {Headers} from "../../types";
import {ApiClient} from "../../clients";
export class ApiClientFactory {
constructor(
private readonly baseUrl: string,
private readonly headers: Headers = {}
) {}
public createClient(): ApiClient {
return new ApiClient(this.baseUrl, this.headers);
}
public createAuthorizedClient(authToken: string): ApiClient {
return new ApiClient(this.baseUrl, this.headers, authToken);
}
}
هیچ کار پیچیده ای انجام نمی دهد: فقط یک مشتری معمولی یا یک مشتری مجاز ایجاد می کند و یک توکن را به سازنده ارسال می کند.
اجرای خاص
اکنون باید این کلاینت انتزاعی را با یک نقطه پایانی خاص تطبیق دهیم. به عنوان مثال، بیایید مدیری ایجاد کنیم که آخرین وضعیت نمایه کاربر را از سرور دریافت کند:
import {ApiClientInterface} from "./clients";
import {Profile} from "./models";
export class ProfileManager {
constructor(private readonly apiClient: ApiClientInterface) {}
public async get(): Promise<Profile> {
return this.apiClient.get("");
}
}
در این مثال، ما به مدلی که برای پروفایل استفاده می کنیم اهمیتی نمی دهیم. بیایید فرض کنیم که با مقدار ارسال شده از سرور سازگار است.
خود کلاس manager از ترکیب استفاده می کند و شی کلاینت را در حالت خود ذخیره می کند تا تمام درخواست های API را به آن فوروارد کند و در صورت لزوم می تواند منطق خود را به مقدار دریافتی اضافه کند (انجام اعتبارسنجی، ایجاد نقطه پایانی خود، و به زودی).
اغلب APIها منطق دامنه را با افزودن یک پیشوند خاص به نقاط انتهایی خود گروه بندی می کنند. همچنین مواردی از مهاجرت API از یک نسخه به نسخه جدیدتر وجود دارد. برای تأمین همه اینها، ما یک کارخانه برای این مدیر خاص ایجاد خواهیم کرد.
کد کارخانه:
import {ApiClientFactory} from "./clients";
import {Headers} from "../types";
import {ProfileManager} from "../ProfileManager";
export class ProfileManagerFactory {
private readonly apiClientFactory: ApiClientFactory;
constructor(baseUrl: string, headers: Headers) {
this.apiClientFactory = new ApiClientFactory(
`${baseUrl}/api/v1/profile`,
headers
);
}
public createProfileManager(authToken: string): ProfileManager {
return new ProfileManager(
this.apiClientFactory.createAuthorizedClient(authToken)
);
}
}
هنگام ایجاد این کارخانه، URL دامنه و هدرهای درخواست به سازنده ارسال می شود. سپس این پارامترها به سازنده کارخانه کلاینت API ارسال میشوند و نسخه API و همان پیشوند را که نشاندهنده بخشی از منطق دامنه پس از URL ارسال شده است اضافه میکند. هنگام ایجاد یک مدیر نمایه کاربر، مجوز لازم است، بنابراین یک نشانه به روش ارسال می شود که بر اساس آن یک کلاینت با هدر مجوز ایجاد می شود.
تزریق وابستگی
اکنون تنها چیزی که باقی می ماند این است که تابعی بنویسیم که مسئول ارائه یک مدیر پروفایل فعال در هر بخشی از کد باشد، چه یک جزء React یا یک کلاس TypeScript مستقل. چیزی شبیه به این خواهد بود:
export async function createProfileManager(): Promise<apiClient.ProfileManager> {
const factory = new apiClient.ProfileManagerFactory(apiClientConfig.apiBaseUrl, getBaseHeaders());
return factory.createProfileManager(await getAuthToken());
}
ابتدا یک کارخانه از این مدیران در داخل ایجاد می شود که دامنه سرور و هدرهای پایه به آن منتقل می شود که به شکل زیر است:
function getBaseHeaders(): apiClient.Headers {
return {
"Accept-Language": "en"
}
}
در صورت تمایل، می توانید هر یک از هدرهای خود را در سطح عملکرد ایجاد مدیر اضافه کنید.
من در مورد روش به دست آوردن توکن API و عملکرد آن صحبت نمی کنم getAuthToken()
عملکرد در این مقاله، زیرا این موضوع شایسته انتشار جداگانه است.
async function getAuthToken(): Promise<string> {
// ЗThere would be a token receipt code here, but for now...
return localStorage.getItem("auth-token");
}
استفاده در قطعات
نمونه ای از مدیر پروفایل در زیر ارائه شده است:
useEffect(() => {
(async () => {
try {
await initProfile();
} catch (error: any) {
await handleError(error);
} finally {
setLoading(false);
}
})()
}, []);
const initProfile = async () => {
const manager = await createProfileManager();
const profile = await manager.get();
await dispatch(set(profile));
}
وقتی تابع در useEffect
هوک، یک مدیر پروفایل به صورت ناهمزمان ایجاد می شود، که سپس وضعیت فعلی نمایه کاربر را از سرور درخواست می کند. در این مثال، ما به سادگی وضعیت دریافتی را در ذخیرهسازی Redux مینویسیم تا بتوانیم هر بار بدون درخواست مجدد از سرور، با این پروفایل کار کنیم. در صورت خطای مشتری، handleError()
تابع راه اندازی می شود که بسته به نوع خطا، همانطور که قبلا ذکر کردم، اقدامات خاصی را انجام می دهد.
نتایج
این پیاده سازی مستقل از چارچوبی است که با آن کار می کنید، حتی می توان از آن در JS (TS) بومی استفاده کرد. چیزهای زیادی وجود دارد که می توانید در آن اصلاح کنید، به عنوان مثال، یک الگوی “Builder” برای ایجاد یک کلاینت API و انتقال پارامترها، سیگنال های ابطال و سایر موارد به آن اضافه کنید، یا یک سیستم احراز هویت متغیر از طریق یک توکن JWT ایجاد کنید. همه چیز در اختیار شماست). در مقاله بعدی روش به دست آوردن و کار با توکن های API روی کلاینت را به شما خواهم گفت.
من را در Github دنبال کنید <3