เริ่มต้น
จะอธิบายวิธีการใช้ typed-rx-http Core client ก่อน จากนั้นจะอธิบายวิธีการเชื่อมต่อ HTTP adapter สาธารณะของเจ้าของโครงการและบริการสร้างอัตโนมัติ Swagger เฉพาะในโครงการที่จำเป็น.
1. ติดตั้ง
npm install @byeolnaerim/typed-rx-http rxjs2. ใช้ Core client โดยตรงโดยไม่ต้องสร้างอัตโนมัติ
ก่อนอื่นวิธีการใช้งานที่พื้นฐานที่สุดคือ createHttpClient<Paths>()สร้าง client หนึ่งครั้งและเรียกใช้ callApi ของ client นั้น. Paths เป็นประเภท paths ที่สร้างจาก OpenAPI โดยทั่วไป แต่สามารถใช้ประเภทที่กำหนดเองที่มีโครงสร้างเดียวกันได้.
httpClient.ts
tsimport {
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
tsimport {
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
tsimport {
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
tsximport { search } from "@handler/service/auto/ApiMembersService";
import { firstValueFrom } from "rxjs";
const response = await firstValueFrom(
search({
params: {
keyword: "kim",
page: 0,
size: 20,
},
}),
);