Path To A Clean(er) React Architecture – A Shared API Client

ماهیت بدون نظر React یک شمشیر دو لبه است:
- از یک طرف شما آزادی انتخاب دارید.
- از طرف دیگر بسیاری از پروژه ها با یک سفارشی و معماری اغلب کثیف.
این مقاله شروع یک سری در مورد معماری نرمافزار و برنامههای React است که در آن ما یک پایه کد را با بسیاری از اقدامات بد انتخاب میکنیم و آن را مرحله به مرحله اصلاح میکنیم.
در اینجا ما با استخراج پیکربندی درخواست مشترک API از کامپوننت ها به یک سرویس گیرنده API مشترک شروع می کنیم. تغییرات در کد کوچک است، اما تاثیر آن بر قابلیت نگهداری در دراز مدت بسیار زیاد خواهد بود. بیایید با مشاهده یک مثال کد شروع کنیم.
فهرست مطالب
- کد بد: پیکربندی درخواست API با کد سخت در همه جا
- چرا این کد بد است؟
- راه حل: یک سرویس گیرنده API مشترک
- چرا این کد بهتر است؟
- مراحل بعدی بازسازی: معرفی یک لایه API
- PS: سرویس گیرنده API مشترک با fetch
به عنوان جایگزینی برای محتوای متنی زیر می توانید این ویدیو را نیز تماشا کنید.
https://www.youtube.com/watch?v=GpRYT3CQ-Y0
کد بد: پیکربندی درخواست API با کد سخت در همه جا
در اینجا یکی از اجزای پروژه مثال آمده است: مؤلفه ای که داده ها را از دو نقطه پایانی API واکشی می کند و داده ها را ارائه می دهد.
این فقط یک مؤلفه است اما تعداد زیادی فراخوانی API در کد وجود دارد.
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import { LoadingSpinner } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { UserResponse, UserShoutsResponse } from "@/types";
import { UserInfo } from "./user-info";
export function UserProfile() {
const { handle } = useParams<{ handle: string }>();
const [user, setUser] = useState<UserResponse>();
const [userShouts, setUserShouts] = useState<UserShoutsResponse>();
const [hasError, setHasError] = useState(false);
useEffect(() => {
axios
.get<UserResponse>(`/api/user/${handle}`)
.then((response) => setUser(response.data))
.catch(() => setHasError(true));
axios
.get<UserShoutsResponse>(`/api/user/${handle}/shouts`)
.then((response) => setUserShouts(response.data))
.catch(() => setHasError(true));
}, [handle]);
if (hasError) {
return <div>An error occurred</div>;
}
if (!user || !userShouts) {
return <LoadingSpinner />;
}
return (
<div className="max-w-2xl w-full mx-auto flex flex-col p-6 gap-6">
<UserInfo user={user.data} />
<ShoutList
users={[user.data]}
shouts={userShouts.data}
images={userShouts.included}
/>
</div>
);
}
چرا این کد بد است؟
این کد از تنظیمات یکسانی برای هر درخواست API استفاده می کند (در اینجا مسیر پایه /api
به عنوان مثال).
مشکل: اگر API را به عنوان مثال تغییر دادیم، از نسخه دیگری در مسیری مانند استفاده کنید /api/v2
ما باید هر درخواستی را تغییر دهیم. اگر میخواهیم یک هدر در سطح برنامه (مثلاً برای احراز هویت) اضافه کنیم، باید آن را در همه جا اضافه کنیم.
این به خصوص در پروژه های بزرگتر با تعداد زیادی تماس API قابل نگهداری نیست.
بهعلاوه، هرگونه تغییر در درخواستهای API به طور کلی با این خطر همراه خواهد بود که بهروزرسانی یکی از درخواستها را از دست بدهیم و بخشهایی از برنامه را خراب کنیم.
اما راه حل چیست؟
راه حل: یک سرویس گیرنده API مشترک
به اشتراک گذاری پیکربندی درخواست اساسی بین همه درخواست ها کلیدی است. برای رسیدن به این هدف ابتدا همه را استخراج می کنیم axios
ارجاع به یک فایل مشترک
// /src/api/client.ts
import axios from "axios";
export const apiClient = axios.create({
baseURL: "/api",
});
این فقط یک مثال ساده است. میتوانیم پیکربندیهای رایجتری را به این کلاینت api اضافه کنیم، مانند هدرهای گسترده یا منطقی که یک توکن را از حافظه محلی به درخواست ارسال میکند.
به هر حال.
اکنون ما از کلاینت API جدید خود در کامپوننت استفاده می کنیم.
import { apiClient } from "@/api/client";
...
export function UserProfile() {
const { handle } = useParams<{ handle: string }>();
const [user, setUser] = useState<UserResponse>();
const [userShouts, setUserShouts] = useState<UserShoutsResponse>();
const [hasError, setHasError] = useState(false);
useEffect(() => {
apiClient
.get<UserResponse>(`/user/${handle}`)
.then((response) => setUser(response.data))
.catch(() => setHasError(true));
apiClient
.get<UserShoutsResponse>(`/user/${handle}/shouts`)
.then((response) => setUserShouts(response.data))
.catch(() => setHasError(true));
}, [handle]);
...
}
حق با شماست، به نظر نمی رسد این تغییر بزرگی باشد، پس…
چرا این کد بهتر است؟
بیایید این سناریو را تصور کنیم: مسیر پایه API تغییر می کند /api
به /api/v2
.
این امر زمانی رایج است که تغییرات شکسته باید در API معرفی شوند. با سرویس گیرنده API مشترک خود، فقط باید یک خط در کد را به روز کنیم.
// /src/api/client.ts
import axios from "axios";
export const apiClient = axios.create({
baseURL: "/api/v2",
});
و همانطور که گفته شد، ما به راحتی میتوانیم هدرهای گسترده برنامه یا حتی میانافزار را اضافه کنیم تا مثلاً یک توکن را از فضای ذخیرهسازی محلی به سرصفحههای درخواست کپی کنیم.
نه تنها که! ما همچنین شروع به جداسازی کد UI از API کردیم. واکشی داده ها کمی ساده تر شد:
apiClient.get<UserResponse>(`/user/${handle}`)
حالا UI
- از مسیر پایه اطلاعی ندارد
/api
- نمی داند که ما از Axios برای واکشی داده ها استفاده می کنیم.
اینها جزئیات پیاده سازی هستند که UI نباید به آنها اهمیت دهد.
در حالی که اینها تغییرات مهمی هستند، هنوز با انجام آن فاصله زیادی داریم.
مراحل بعدی بازسازی: معرفی یک لایه API
بسیار خوب، مشتری API مشترک قبلاً کد را بهبود بخشیده است.
اما اگر فکر می کنید هنوز کد خوب به نظر نمی رسد، من با شما موافقم. با استفاده از یک سرویس گیرنده API مشترک، ما شروع به جدا کردن رابط کاربری خود از کدهای مرتبط با API کردیم.
اما ببینید چقدر دانش در مورد API هنوز داخل کامپوننت است!
در هفتههای آینده، ما این سفر بازسازی را به سمت معماری پاک (تر) React ادامه خواهیم داد.
PS: سرویس گیرنده API مشترک با fetch
در حالی که استفاده می کردم axios
در این مثال هیچ الزامی برای آن وجود ندارد.
در حقیقت، axios
در حالی که به اندازه بسته نرم افزاری شما اضافه می کند fetch
به صورت بومی پشتیبانی می شود. بنابراین دلیل خوبی برای کنار گذاشتن آن وجود دارد.
fetch
متأسفانه با همان راحتی ارائه نمی شود، بنابراین ما به کد بیشتری برای ایجاد یک “نمونه” قابل اشتراک نیاز داریم.
const API_BASE_URL = '/api';
export const apiClient = async (endpoint, options = {}) => {
const config = {
method: 'GET', // Default to GET method
...options,
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
},
};
return fetch(`${API_BASE_URL}${endpoint}`, config);
};
این تابع سرویس گیرنده API یک تنظیم پیش فرض را برای هر درخواست تضمین می کند در حالی که API کلی واکشی را دست نخورده نگه می دارد.
اگرچه ممکن است ارزش ایجاد یک کلاینت API انتزاعی تر را داشته باشد. به عنوان مثال، شما همچنین می توانید یک API بیشتر شبیه Axios با یک کلاس جاوا اسکریپت ایجاد کنید.
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(url, options) {
const response = await fetch(`${this.baseURL}${url}`, options);
if (!response.ok) {
const error = new Error('HTTP Error');
error.status = response.status;
error.response = await response.json();
throw error;
}
return response.json();
}
get(url) {
return this.request(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
}
post(url, data) {
return this.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}
// You can add more methods (put, delete, etc.) here as needed
}
export const apiClient = new APIClient('/api');
این سرویس گیرنده API اکنون می تواند مانند زیر استفاده شود.
import { apiClient } from './apiClient';
// To make a GET request to `/api/user/some-user-handle
apiClient.get('/user/some-user-handle')
.then(data => console.log(data))
.catch(error => console.error('Fetching user failed', error));
// To make a POST request
apiClient.post('/login', { username: "some-user", password: "asdf1234" })
.then(data => console.log(data))
.catch(error => console.error('Login failed', error));