برنامه نویسی

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

ماهیت بدون نظر React یک شمشیر دو لبه است:

  • از یک طرف شما آزادی انتخاب دارید.
  • از طرف دیگر بسیاری از پروژه ها با یک سفارشی و معماری اغلب کثیف.

این مقاله شروع یک سری در مورد معماری نرم‌افزار و برنامه‌های React است که در آن ما یک پایه کد را با بسیاری از اقدامات بد انتخاب می‌کنیم و آن را مرحله به مرحله اصلاح می‌کنیم.

در اینجا ما با استخراج پیکربندی درخواست مشترک API از کامپوننت ها به یک سرویس گیرنده API مشترک شروع می کنیم. تغییرات در کد کوچک است، اما تاثیر آن بر قابلیت نگهداری در دراز مدت بسیار زیاد خواهد بود. بیایید با مشاهده یک مثال کد شروع کنیم.

فهرست مطالب

  1. کد بد: پیکربندی درخواست API با کد سخت در همه جا
  2. چرا این کد بد است؟
  3. راه حل: یک سرویس گیرنده API مشترک
  4. چرا این کد بهتر است؟
  5. مراحل بعدی بازسازی: معرفی یک لایه API
  6. 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

  1. از مسیر پایه اطلاعی ندارد /api
  2. نمی داند که ما از Axios برای واکشی داده ها استفاده می کنیم.

اینها جزئیات پیاده سازی هستند که UI نباید به آنها اهمیت دهد.

در حالی که اینها تغییرات مهمی هستند، هنوز با انجام آن فاصله زیادی داریم.

مراحل بعدی بازسازی: معرفی یک لایه API

بسیار خوب، مشتری API مشترک قبلاً کد را بهبود بخشیده است.

اما اگر فکر می کنید هنوز کد خوب به نظر نمی رسد، من با شما موافقم. با استفاده از یک سرویس گیرنده API مشترک، ما شروع به جدا کردن رابط کاربری خود از کدهای مرتبط با API کردیم.

اما ببینید چقدر دانش در مورد API هنوز داخل کامپوننت است!

کد UI هنوز اطلاعات زیادی در مورد درخواست های API مانند روش درخواست، نوع داده پاسخ، مسیر نقطه پایانی و مدیریت پارامتر دارد.

در هفته‌های آینده، ما این سفر بازسازی را به سمت معماری پاک (تر) React ادامه خواهیم داد.

React Job Simulator

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));

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نوشته های مشابه

دکمه بازگشت به بالا