HTTP Client

أولاً، سأشرح كيفية استخدام عميل Core مباشرة، ثم سأشرح كمثال تكامل اختياري لمحول HTTP المشترك الذي تستخدمه الخدمة المولدة تلقائيًا.


استخدام عميل Core مباشرة

Paths التي يتم حقنها في createHttpClient<Paths>() هي عقد الطلب. يتم تحديد url وmethod من مفتاح Paths، بينما يتم تحديد queryString وpathVariable من parameters، ويتم تحديد body من requestBody.

نوع الاستجابة هو R الذي يحدده المستدعي في callApi<R>(). لا يستنتج Core نوع الاستجابة تلقائيًا من استجابات OpenAPI.

httpClient.ts
ts
import {
  createHeaderStore,
  createHttpClient,
} from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

export const headerStore = createHeaderStore({
  "Content-Type": "application/json",
});

export const client = createHttpClient<Paths>({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  headerStore,
});

export const getAdminList = () => {
  return client.callApi<{ items: any[]; total: number }>({
    url: "/api/account/search/admin/get-list",
    method: "get",
    queryString: { pageNumber: 1, pageSize: 20 },
  });
};

شكل الاستجابة يتم اختياره حسب API.

لا يفرض typed-rx-http استخدام ResponseWrapper. إذا كانت استجابة الخادم عبارة عن غلاف، يتم تحديد نوع الغلاف كـ R، وإذا لم يكن كذلك، يتم تحديد نوع الاستجابة الفعلي كما هو.

ts
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };

return client.callApi<ResponseWrapper<{ requestId: string }>>({
  url: "/api/bid/create/bid/request",
  method: "post",
  body,
});

اختياري: محول عام للمشروع + خدمة مولدة تلقائيًا.

تكوين commonService أدناه ليس هيكلًا أساسيًا لـ Core، بل هو نمط دمج مشروع يهدف إلى تطبيق مصادقة headers وSSR وسياسات الذاكرة المؤقتة على عدة خدمات مولدة في وقت واحد.

ما تفعله commonService في المشروع.

commonService ليس اسم فئة خاصة تقدمها @byeolnaerim/typed-rx-http، بل هو ملف مملوك للمستخدم يُستخدم لجمع سياسات HTTP المشتركة في مكان واحد. اسم الملف حر، وفي المثال أدناه، نستخدم اسم المشروع الفعلي rxjsHttpService.ts.

بدلاً من تكرار createHttpClient في الشاشة والخدمة المولدة تلقائيًا، نستخدم callApi و callApiStream التي أنشأها هذا الملف كنقطة دخول مشتركة. لذلك، إذا قمت بتعديل قواعد تجديد المصادقة، وتمرير رؤوس SSR، ومعالجة التخزين المؤقت والأخطاء مرة واحدة، فسيتم تطبيقها على جميع الخدمات المولدة.

ابدأ بمشاهدة الكود الكامل لـ commonService/rxjsHttpService.

1. الحد الأدنى من الشكل المكتمل

جوهر commonService هو الكود أدناه. يتم إنشاء headerStore و typed HTTP client مرة واحدة، ويتم تصدير callApi/callApiStream التي تستخدمها الخدمة المولدة بشكل مشترك. يمكن أيضًا عرض uploadFile و createSSEObservable من نفس العميل.

rxjsHttpService.ts
ts
import {
	createHeaderStore,
	createHttpClient,
	type ServiceArguments,
} from "@byeolnaerim/typed-rx-http";
import type { Observable } from "rxjs";

import type { paths } from "./@types/ApiTypes";

export const headerStore = createHeaderStore({
	"Content-Type": "application/json",
});

export const service = createHttpClient<paths>({
	baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
	headerStore,
});

export const callApi = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return service.callApi<R, TPath, TMethod>(serviceArguments);
};

export const callApiStream = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return service.callApiStream<R, TPath, TMethod>(serviceArguments);
};

export const uploadFile = service.uploadFile;
export const createSSEObservable = service.createSSEObservable;
2. Next.js + مصادقة الجلسة + توسيع ذاكرة التخزين المؤقت CSR/SSR

فيما يلي مثال كامل لمشروع يضيف مصادقة الجلسة، ورأس SSR وذاكرة التخزين المؤقت CSR/SSR إلى المحول العام نفسه. إذا كان المشروع يستورد ملف cache wrapper، فإن هذا الشكل هو المعيار. يمكنك نسخه مباشرة ثم ضبط setLogin import و ApiTypes و CacheNames ومسار API المصادقة والمتغيرات البيئية والمسار غير المصرح به وفقًا لمشروعك.

rxjsHttpService.ts
ts
import {
	createCsrCache,
	createHeaderStore,
	createHttpClient,
	createSessionAuth,
} from "@byeolnaerim/typed-rx-http";

import type {
	CacheForService,
	ServiceArguments,
} from "@byeolnaerim/typed-rx-http";

import { defer, Observable } from "rxjs";

import { setLogin } from "@ui/handler/hooks/useAccounts";

import type { paths } from "./@types/ApiTypes";

import type { CacheNames } from "./@types/CacheNames";

export const redirectToUnauthorizedOnServer401 = async () => {
	const { headers } = await import("next/headers");
	const { redirect } = await import("next/navigation");

	const h = await headers();
	const pageUrl = h.get("x-page-url") || "/";
	const redirectUri = encodeURIComponent(pageUrl);

	redirect(`/unauthorized?redirect_uri=${redirectUri}&logout=true`);
};

export const headerStore = createHeaderStore({
	"Content-Type": "application/json",
});

const headersProvider = async () => {
	if (typeof window !== "undefined") {
		/**
		 * 라이브러리쪽 코드가
		 * if (headersProvider) return headersProvider();
		 * if (headerStore) return headerStore.get();
		 * 이렇게 되어있음 같이 못씀, 방향자체는 맞음. 라이브러리쪽에서 marge해버리면 csr/ssr 구분못함
		 */
		return headerStore.get();
	}

	const { cookies } = await import("next/headers");

	const cookieStore = await cookies();
	const cookieHeader = cookieStore
		.getAll()
		.map((c) => `${c.name}=${c.value}`)
		.join("; ");

	const authorization = cookieStore.get("Authorization")?.value;
	const accessToken = cookieStore.get("accessToken")?.value;

	return {
		...(cookieHeader ? { Cookie: cookieHeader } : {}),
		...(authorization
			? {
					Authorization: authorization.startsWith("Bearer ")
						? authorization
						: `Bearer ${authorization}`,
				}
			: {}),
		...(!authorization && accessToken
			? {
					Authorization: accessToken.startsWith("Bearer ")
						? accessToken
						: `Bearer ${accessToken}`,
				}
			: {}),
	};
};

export const service = createHttpClient<paths>({
	baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
	onServer401: redirectToUnauthorizedOnServer401,
	defaultErrorMessage: () => {
		return "알 수 없는 오류가 발생했습니다.";
	},
	headerStore,
	headersProvider,
	dropAuthWhenCacheControl: true,
});

const sessionAuth = createSessionAuth({
	headerStore,
	tokenUrl: "/api/auth/token",
	refreshUrl: "/api/auth/token/refresh",
	logoutUrl: "/api/auth/logout",
	onLoginChange: (loggedIn) => {
		if (typeof window !== "undefined") {
			setLogin(loggedIn);
		}
	},
});

export const callApi = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApi<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

export const callApiStream = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApiStream<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

// CSR cache
const csrCache = createCsrCache<CacheNames>();

export const callApiClientCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	return csrCache.callApiCsrCache(callApi, serviceArguments, cacheForService);
};

export const removeCsrCache = csrCache.removeCsrCache;

export const callApiServerCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	if (typeof window !== "undefined") {
		return callApiClientCache(serviceArguments, cacheForService);
	}

	return new Observable<R>((subscriber) => {
		let closed = false;

		import("@byeolnaerim/typed-rx-http/next")
			.then(({ callApiSsrCache }) => {
				if (closed) {
					return;
				}

				const subscription = callApiSsrCache<paths, R, CacheNames>({
					baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
					serviceArguments: serviceArguments as ServiceArguments<
						paths,
						keyof paths,
						keyof paths[keyof paths] & string,
						R
					>,
					cacheForService,
					headersProvider,
					onServer401: redirectToUnauthorizedOnServer401,
				}).subscribe(subscriber);

				subscriber.add(() => {
					closed = true;
					subscription.unsubscribe();
				});
			})
			.catch((error) => {
				if (!closed) {
					subscriber.error(error);
				}
			});

		return () => {
			closed = true;
		};
	});
};

أدوار حسب تكوين الكود

paths

هذا هو نوع مسارات OpenAPI المستخدم عند فحص URL وmethod وpath variable وquery وbody type بواسطة createHttpClient وServiceArguments.

headerStore

يتم تخزين رؤوس مشتركة مثل Content-Type وAuthorization في المتصفح ومشاركتها مع مصادقة الجلسة واتصال RSocket.

headersProvider

في CSR، يتم إرجاع headerStore، وفي SSR، يتم قراءة Cookie وAuthorization من الطلب الحالي لتوفير رؤوس مختلفة لكل طلب.

service

عميل HTTP typed فعلي مع base URL ورؤوس مشتركة ومعالجة 401 من الخادم ورسالة خطأ أساسية.

sessionAuth

يتم مزامنة رمز الجلسة قبل الطلب، وإذا كان 401 يتم التحديث ثم إعادة محاولة Observable الأصلي مرة واحدة، وإذا فشل يتم تنفيذ تدفق تسجيل الخروج.

callApi / callApiStream

دالة مشتركة يتم استيرادها بواسطة خدمة REST العامة التي تم إنشاؤها تلقائيًا وخدمة NDJSON stream. كلاهما يعيد RxJS Observable.

callApiClientCache / callApiServerCache

غلاف ذاكرة التخزين المؤقت الذي يستخدم ذاكرة التخزين المؤقت CSR في المتصفح ويقوم بتحميل ذاكرة التخزين المؤقت SSR من @byeolnaerim/typed-rx-http/next ديناميكيًا.

طريقة الاتصال بالخدمة التي تم إنشاؤها تلقائيًا.

يحسب المُولد المسار النسبي لملف commonServiceFile ويضيف الاستيراد لكل خدمة. الشاشة تستورد الوظائف المُنشأة بدلاً من استدعاء الملفات العامة مباشرة، وتقوم الوظيفة المُنشأة بتحويل path وparams وbody إلى pathVariable وqueryString وbody من ServiceArguments وتمريرها إلى callApi.

generateSwagger.cjs
js
generateSwaggerFromHttp({
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

استجابة واحدة هي firstValueFrom.

يمكن استخدام واجهات برمجة التطبيقات REST التي تستجيب مرة واحدة وتنتهي مثل البحث، التخزين، والحذف بشكل طبيعي حتى داخل الدالة غير المتزامنة الموجودة.firstValueFromإذا تم استلامها.

BusinessSearchPage.tsx
tsx
import { workplacesSearch } from "@handler/service/auto/ApiBusinessService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  workplacesSearch({
    params: {
      source: "EIW",
      q: keyword,
      size: 20,
      nextToken: nextToken || undefined,
    },
  }),
);

في الشاشة، قيمة الإرجاعيظهر فقط، لكن الخدمة التي تم إنشاؤها تحتوي على URL، وطريقة HTTP ونوع الاستجابة. يتم دائمًا تمرير معلمات الاستعلام كـ{ params: { ... } }ل.

الاشتراك في عدة استجابات

طلبات مثل تقدم العمل أو تدفق RSocket التي تصل عدة قيم هيsubscribeتستقبل، ويتم إلغاء الاشتراك عند اختفاء المكون.

BatchJobPage.tsx
tsx
const subscription = processBatchJobs({
  body: targets,
}).subscribe({
  next: (event) => {
    setProcessedCount(event.processedCount);
  },
  error: (error) => {
    setErrorMessage(error.message);
  },
  complete: () => {
    setIsSubmitting(false);
  },
});

return () => subscription.unsubscribe();

عند استخدام callApi مباشرة

يمكنك استدعاء callApi للخدمة العامة مباشرةً عند عدم استخدام مولد الخدمة أو عند التحقق من نقطة النهاية المؤقتة قبل الإنشاء. يتم فحص نوع الطلب في المسارات ويتم تحديد نوع الاستجابة بواسطة المستدعي بشكل عام.

TemporaryService.ts
ts
return callApi<HealthResponse>({
  url: "/health",
  method: "get",
});
© 2026 بيولناريم. جميع الحقوق محفوظة.مقدمةسياسة معالجة المعلومات الشخصية