เริ่มต้น

จะอธิบายวิธีการใช้ typed-rx-http Core client ก่อน จากนั้นจะอธิบายวิธีการเชื่อมต่อ HTTP adapter สาธารณะของเจ้าของโครงการและบริการสร้างอัตโนมัติ Swagger เฉพาะในโครงการที่จำเป็น.


1. ติดตั้ง

bash
npm install @byeolnaerim/typed-rx-http rxjs

2. ใช้ Core client โดยตรงโดยไม่ต้องสร้างอัตโนมัติ

ก่อนอื่นวิธีการใช้งานที่พื้นฐานที่สุดคือ createHttpClient<Paths>()สร้าง client หนึ่งครั้งและเรียกใช้ callApi ของ client นั้น. Paths เป็นประเภท paths ที่สร้างจาก OpenAPI โดยทั่วไป แต่สามารถใช้ประเภทที่กำหนดเองที่มีโครงสร้างเดียวกันได้.

httpClient.ts
ts
import {
  createHeaderStore,
  createHttpClient,
} from "@byeolnaerim/typed-rx-http";
import type { 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,
});

3. ตัวเลือก: rxjsHttpService/commonService คืออะไร

สิ่งที่เรียกว่า rxjsHttpService.ts หรือ commonService.ts ในเอกสารไม่ใช่ชื่อไฟล์ที่สร้างโดยไลบรารี แต่เป็นไฟล์ HTTP adapter ที่โครงการเป็นเจ้าของโดยตรง บริการ REST ที่สร้างอัตโนมัติหลายรายการจะไม่สร้าง createHttpClient แยกกัน แต่จะใช้ฟังก์ชัน callApi/callApiStream/cache ที่ไฟล์นี้ส่งออกเป็นจุดเข้าใช้ร่วม.

รูปแบบที่เล็กที่สุดคือการสร้าง headerStore และ createHttpClient หนึ่งครั้งตามโค้ดทั้งหมดด้านล่าง และส่งออกวิธีการของไลบรารีเป็นฟังก์ชันของโครงการอีกครั้ง หากไม่ใช้ฟังก์ชัน cache ในบริการที่สร้างอัตโนมัติ โครงสร้างนี้ก็เพียงพอในการทำความเข้าใจ.

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;

ในโครงการ Next.js ที่แท้จริง หากต้องการจัดการการรับรองความถูกต้องของเซสชัน, คุกกี้/การอนุญาตตามคำขอ SSR, CSR cache และ SSR cache ในไฟล์เดียว สามารถขยายได้ตามด้านล่าง ไฟล์นี้ก็ไม่ใช่สิ่งที่สร้างอัตโนมัติ แต่เป็นโค้ดของโครงการ.

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

4. ตัวเลือก: หากใช้บริการสร้างอัตโนมัติ Swagger

หลังจากเชื่อมต่อ generator ของ OpenAPI service จะไม่เขียน URL และประเภทการตอบสนองใหม่ในหน้าจอ. นำเข้าฟังก์ชันที่สร้างขึ้นและส่ง path, params, body ที่จำเป็นเท่านั้น.

MemberSearchPage.tsx
tsx
import { search } from "@handler/service/auto/ApiMembersService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  search({
    params: {
      keyword: "kim",
      page: 0,
      size: 20,
    },
  }),
);

เอกสารถัดไป