مسیر مقیاس پذیری (1): نحوه مدیریت خدمات در فرانت اند.

مقدمه: اهمیت مدیریت صحیح خدمات در فرانت اند
دانستن نحوه مدیریت خدمات به شیوه ای مقیاس پذیر و خوانا بسیار مهم است، نه تنها برای عملکرد برنامه، بلکه برای حفظ سلامت و رفاه توسعه دهندگان. خدمات بخشی از برنامه است که با خارج ارتباط برقرار می کند، مانند تماس با API ها، تعامل با پایگاه های داده، یا حتی مدیریت مجوزهای تلفن، مانند دسترسی به مخاطبین. مدیریت خوب این خدمات تضمین می کند که برنامه شما می تواند مقیاس پذیر باشد و در آینده باعث سردرد نشود.
در این مقاله قصد داریم نحوه مدیریت سرویس های فرانت اند را به روشی مقیاس پذیر با استفاده از الگوی مخزن بررسی کنیم. این رویکرد امکان دسترسی به سرویسها را به شیوهای کنترلشده و واضح فراهم میکند و تماسهای API را محصور میکند و استفاده مجدد از کد را تسهیل میکند و همچنین قابلیت نگهداری آن را تسهیل میکند.
در طول این مقاله، نحوه پیادهسازی این راهحل، روشهای خوبی که باید دنبال کنید و چگونه میتواند به شما کمک کند تا اطمینان حاصل کنید که کد شما مقیاسپذیر و نگهداری آسان است، خواهیم دید.
مفاهیم اساسی برای مدیریت خدمات: DTOها و آداپتورها
در یک معماری به خوبی سازماندهی شده، درک چگونگی ساختار لایه های مختلف یک برنامه مهم است. یکی از لایه های اساسی لایه زیرساخت است که در آن خدماتی که با خارج در تعامل هستند مدیریت می شوند.
در این لایه زیرساخت، دو مفهوم کلیدی که اغلب ظاهر میشوند عبارتند از DTO (اشیاء انتقال داده) و آداپتورها.
-
DTO (Data Transfer Object): DTO ها رابط هایی هستند که داده ها را در پاسخ های API ها یا پایگاه های داده نشان می دهند. آنها تضمین می کنند که اطلاعاتی که از سرویس های خارجی دریافت می کنیم با فرمت خاصی مطابقت دارد که برنامه ما به راحتی می تواند آن را مدیریت کند. به عنوان مثال، یک DTO می تواند ساختار یک شی کاربر را که از یک API دریافت می کنیم، تعریف کند.
-
آداپتورها: آداپتورها عملکردهایی هستند که وظیفه ترجمه داده ها از DTOها به رابط های دامنه برنامه یا بالعکس را بر عهده دارند. یعنی می توانند برای ترجمه داده هایی که دریافت می کنیم یا برای ترجمه داده هایی که ارسال می کنیم باشد. به این ترتیب، اگر اطلاعات دریافتی ما در آینده تغییر کند، فقط باید روی آداپتور تمرکز کنیم و نه در کل برنامه.
استفاده از DTO ها و آداپتورها به لایه زیرساخت اجازه می دهد تا انعطاف پذیر باشد و به راحتی با تغییرات در سرویس های خارجی سازگار باشد بدون اینکه بر منطق برنامه تأثیر بگذارد. علاوه بر این، جدایی واضحی بین لایه ها حفظ می کند و هر یک از آنها مسئولیت های خاص خود را انجام می دهند.
نمونه ای از داده هایی که دریافت می کنیم:
// getUserProfile.adapter.ts
const userProfileAdapter = (data: UserProfileDto): UserProfile => ({
username: data.user_username,
profilePicture: data.profile_picture,
about: data.user_about_me,
})
export deafult userProfileAdapter;
نمونه ای از داده هایی که ارسال می کنیم:
الگوی مخزن
الگوی مخزن مبتنی بر این ایده است که منطق دسترسی به داده شما جدا از منطق برنامه یا تجارت شما باشد. این روشی را فراهم می کند تا روش هایی را که یک موجودیت در برنامه شما دارد به صورت متمرکز و محصور شده داشته باشید.
در ابتدا باید رابط مخزن خود را با تعریف متدهایی که این موجودیت خواهد داشت ایجاد کنیم.
// UserProfileRepository.model.ts
export interface IUserProfileRepository {
getUserProfile: (id: string) => Promise;
createUserProfile: (payload: UserProfile) => Promise;
}
و ما به ایجاد مخزن خود ادامه می دهیم.
// userProfile.repository.ts
export default function userProfileRepository(): IUserProfileRepository {
const url = `${BASE_URL}/profile`;
return {
getUserProfile: getProfileById(id: string) {
try {
const res = await axios.get(`${url}/${id}`);
return userProfileAdapter (userDto);
} catch(error){
throw error;
}
},
createUserProfile: create(payload: UserProfile) {
try {
const body = createUserProfilAdapter(payload);
await axios.post(`${url}/create`, payload);
} catch(error) {
throw error;
}
}
}
}
این رویکرد به ما اجازه می دهد تا تماس های API را کپسوله کنیم و داده هایی را که در قالب DTO دریافت می کنیم از طریق آداپتور به فرمت داخلی خود تبدیل کنیم.
در زیر نموداری از معماری یا ساختاری را مشاهده خواهید کرد که در آن لایه زیرساخت شامل سرویسها، DTO و آداپتورها میشود. این ساختار به ما این امکان را می دهد که بین منطق تجاری و داده های خارجی تفکیک واضحی داشته باشیم.
رسیدگی به خطا
ما می توانیم کد خود را با ایجاد یک کنترل کننده خطا بیشتر کنیم.
// errorHandler.ts
export function errorHandler(error: unknown): void {
if (axios.isAxiosError(error)) {
if (error.response) {
throw Error(`Error: ${error.response.status} - ${error.response.data}`);
} else if (error.request) {
throw Error("Error: No response received from server");
} else {
throw Error(`Error: ${error.message}`);
}
} else {
throw Error("Error: An unexpected error occurred");
}
}
// userProfile.repository.ts
import { errorHandler } from './errorHandler';
export default function userProfileRepository(): IUserProfileRepository {
const url = `${BASE_URL}/profile`;
return {
async getUserProfile(id: string) {
try {
const res = await axios.get(`${url}/${id}`);
return userProfileAdapter(res.data);
} catch (error) {
errorHandler(error);
}
},
async createUserProfile(payload: UserProfile) {
try {
const body = createUserProfileAdapter(payload);
await axios.post(`${url}/create`, body);
} catch (error) {
errorHandler(error);
}
}
}
}
این به ما امکان می دهد منطق ارائه را از منطق تجاری جدا کنیم و اطمینان حاصل کنیم که لایه UI فقط مسئول رسیدگی به پاسخ ها و به روز رسانی وضعیت برنامه است.
اجرای خدمات
هنگامی که منطق دسترسی به داده ها را در مخزن خود کپسوله کردیم، گام بعدی ادغام آن با رابط کاربری (UI) است.
ذکر این نکته ضروری است که هیچ راه واحدی برای پیاده سازی الگوی Repository وجود ندارد. انتخاب پیاده سازی بستگی زیادی به معماری برنامه شما و میزان وفاداری شما به تعاریف آن دارد. در تجربه من، یکی از مفیدترین و کاربردی ترین راه ها برای ادغام آن، استفاده از قلاب بوده است که در ادامه به توسعه آن می پردازیم.
export function useGetUserProfile(id: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const repository = userProfileRepository();
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const userProfile = await repository.getUserProfile(id);
setData(userProfile);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchData();
}, [id]);
return { data, loading, error };
}
رابط UserProfileProps { id: string; } تابع صادرات UserProfile({ id }: UserProfileProps) { const { data, loading, error } = useUserProfile(id); در صورت (بارگیری) بازگشت ; if (error) { toast({ variant: "مخرب"، عنوان: "اوه اوه مشکلی پیش آمد."توضیحات: خطا, }); } بازگشت (
{data?.about}
);
}
در حال حاضر، مؤلفه UserProfile نیازی به دانستن هیچ چیز در مورد نحوه به دست آوردن اطلاعات پروفایل ندارد، تنها وظیفه نمایش نتایج یا پیام های خطا را بر عهده دارد.
در صورت نیاز میتوان این مورد را بیشتر بهبود بخشید، بهعنوان مثال با استفاده از react query در قلاب برای داشتن کنترل بیشتر بر ذخیرهسازی و بهروزرسانی دادهها.
export function useGetUserProfile(id: string) {
const repository = userProfileRepository();
return useQuery({
queryKey: ['userProfile', id],
queryFn: () => repository.getUserProfile(id),
});
}
نتیجه گیری
در این مقاله، نحوه پیادهسازی الگوی Repository در فرانتاند، کپسولهسازی فراخوانهای API و حفظ تفکیک واضح مسئولیتها از طریق استفاده از DTO و آداپتورها را بررسی کردهایم. این رویکرد نه تنها مقیاس پذیری و نگهداری کد را بهبود می بخشد، بلکه به روز رسانی منطق کسب و کار را بدون نگرانی در مورد جزئیات ارتباط با سرویس های خارجی آسان تر می کند. علاوه بر این، با یکپارچه سازی React Query، ما یک لایه کارآمدی اضافی در مدیریت داده ها، مانند کش و به روز رسانی خودکار به دست می آوریم.
به یاد داشته باشید که هیچ راه واحدی برای مدیریت خدمات وجود ندارد (به عنوان مثال، الگوی حماسی برای مدیریت عوارض جانبی نیز وجود دارد). نکته مهم این است که از شیوه های خوب استفاده کنیم تا اطمینان حاصل کنیم که کد ما در طول زمان انعطاف پذیر، تمیز و نگهداری آسان باقی می ماند.
امیدوارم این راهنما برای بهبود نحوه مدیریت خدمات در پروژه های فرانت اند برای شما مفید بوده باشد. اگر آن را دوست داشتید، در نظر داشته باشید، مقاله را لایک کنید یا به اشتراک بگذارید تا توسعه دهندگان بیشتری بتوانند از آن بهره ببرند. با تشکر برای خواندن!