Skip to content

Commit

Permalink
feat: support sync requests of XHR
Browse files Browse the repository at this point in the history
  • Loading branch information
condorheroblog committed Dec 27, 2024
1 parent 72c2a7d commit 6b33714
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 250 deletions.
4 changes: 4 additions & 0 deletions packages/playground/react-sample/fake/index.fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,8 @@ export default defineFakeRoute([
res.end(body);
},
},
{
url: "/response-is-any",
response: "The response can not be a function." as any,
},
]);
26 changes: 26 additions & 0 deletions packages/playground/react-sample/fake/sync-request.fake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineFakeRoute } from "vite-plugin-fake-server/client";

import { resultSuccess } from "./nest/utils";

export default defineFakeRoute({
url: "/sync-request",
/**
* The sleep function for synchronous requests will block the browser's rendering.
* When making a synchronous request, it is recommended to set the delay to 0.
*/
timeout: 0,
response: () => {
return resultSuccess({
timestamp: Date.now(),
status: "success",
code: 200,
message: "ok",
data: {
description: `
The sleep function for synchronous requests will block the browser's rendering.
When making a synchronous request, it is recommended to set the delay to 0.
`,
},
});
},
});
108 changes: 73 additions & 35 deletions packages/playground/react-sample/src/components/the-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { BUTTON_LIST } from "#src/components";
import type { Option } from "#src/components/the-nav/type";

import { REQUEST_TYPE, TheLoading } from "#src/components";
import { useEffect, useState } from "react";

function getType(value: unknown) {
if (value === undefined) {
return "";
}
return Object.prototype.toString.call(value).slice(8, -1);
}

Expand All @@ -15,7 +18,8 @@ export function TheCard({
responseType,
body,
headers,
}: (typeof BUTTON_LIST)[number]) {
sync,
}: Option) {
const [responseOrigin, setResponseOrigin] = useState<{ [key: string]: any }>({});
const [responseData, setResponseData] = useState<any>();
const [loading, setLoading] = useState(false);
Expand All @@ -32,33 +36,77 @@ export function TheCard({
: `${baseURL}${value}`.replace(":id", Math.random().toString());

if (type === REQUEST_TYPE[0]) {
setLoading(true);
const xhr = new XMLHttpRequest();
if (!sync) {
setLoading(true);
const xhr = new XMLHttpRequest();

if (responseType === "xml") {
xhr.responseType = "document";
// Force the response to be parsed as XML
xhr.overrideMimeType("application/xml");
}
else {
xhr.responseType = "json";
}
if (responseType === "xml") {
xhr.responseType = "document";
// Force the response to be parsed as XML
xhr.overrideMimeType("application/xml");
}
else {
xhr.responseType = "json";
}

if (method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD") {
xhr.open(method, `${requestURL}?${queryParams}`, true);
xhr.setRequestHeader("Content-Type", "application/json");
for (const key in headers) {
xhr.setRequestHeader(key.toString(), headers[key as keyof typeof headers]);
if (method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD") {
xhr.open(method, `${requestURL}?${queryParams}`, true);
xhr.setRequestHeader("Content-Type", "application/json");
for (const key in headers) {
xhr.setRequestHeader(key.toString(), headers[key as keyof typeof headers]);
}
xhr.send();
}
else {
xhr.open(method, requestURL, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
}
xhr.send();

xhr.addEventListener("load", () => {
const showHeader = xhr.getResponseHeader("show-header");
const accessControlAllowOrigin = xhr.getResponseHeader("access-control-allow-origin");
setResponseOrigin({
status: xhr.status,
statusText: xhr.statusText,
url: xhr.responseURL,
headers: new Headers({
"show-header": showHeader || "",
"access-control-allow-origin": accessControlAllowOrigin || "",
}),
});
setResponseData(xhr.response);

if (responseType === "xml") {
const xmlContainer = document.getElementById("xmlContainer");
const xmlDoc = xhr.responseXML!;
const xmlRoot = xmlDoc.querySelector("#xml-root");
xmlContainer!.appendChild(xmlRoot!);
}
});

xhr.addEventListener("loadend", () => {
setLoading(false);
});
}
else {
xhr.open(method, requestURL, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
}

xhr.addEventListener("load", () => {
setLoading(true);
const xhr = new XMLHttpRequest();
// Note: It's a sync request
const isAsync = false;
if (method.toUpperCase() === "GET" || method.toUpperCase() === "HEAD") {
xhr.open(method, `${requestURL}?${queryParams}`, isAsync);
xhr.setRequestHeader("Content-Type", "application/json");
for (const key in headers) {
xhr.setRequestHeader(key.toString(), headers[key as keyof typeof headers]);
}
xhr.send();
}
else {
xhr.open(method, requestURL, isAsync);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
}
const showHeader = xhr.getResponseHeader("show-header");
const accessControlAllowOrigin = xhr.getResponseHeader("access-control-allow-origin");
setResponseOrigin({
Expand All @@ -71,18 +119,8 @@ export function TheCard({
}),
});
setResponseData(xhr.response);

if (responseType === "xml") {
const xmlContainer = document.getElementById("xmlContainer");
const xmlDoc = xhr.responseXML!;
const xmlRoot = xmlDoc.querySelector("#xml-root");
xmlContainer!.appendChild(xmlRoot!);
}
});

xhr.addEventListener("loadend", () => {
setLoading(false);
});
}
}
else if (type === REQUEST_TYPE[1]) {
setLoading(true);
Expand Down
48 changes: 43 additions & 5 deletions packages/playground/react-sample/src/components/the-nav/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Option } from "./type";

import { useLocation, useNavigate } from "react-router";

export const REQUEST_TYPE = ["XHR", "Fetch"];
export const OPTIONS = [
export const OPTIONS: Option[] = [
{
label: "Response In Mock",
value: "response-in-mock",
Expand Down Expand Up @@ -121,6 +123,28 @@ export const OPTIONS = [
method: "POST",
body: { name: "CondorHero", age: 18 },
},

/* Only For XHR */
{
label: "Sync Request",
value: "sync-request",
method: "GET",
onlyXHR: true,
sync: true,
},
{
label: "Sync External URL",
value: "https://my-json-server.typicode.com/typicode/demo/comments",
method: "GET",
onlyXHR: true,
sync: true,
},

{
label: "Response Is Any",
value: "response-is-any",
method: "GET",
},
{
label: "Custom Response Header",
value: "custom-response-header",
Expand All @@ -139,7 +163,15 @@ export const OPTIONS = [
},
];

export const BUTTON_LIST = REQUEST_TYPE.flatMap(typeItem => OPTIONS.map(item => ({ ...item, type: typeItem })));
export const BUTTON_LIST = REQUEST_TYPE.flatMap(
typeItem => OPTIONS.map(
(item) => {
return (typeItem === "Fetch" && item.onlyXHR)
? undefined
: { ...item, type: typeItem };
},
),
).filter(Boolean) as Option[];

export function TheNav() {
const navigate = useNavigate();
Expand Down Expand Up @@ -175,17 +207,23 @@ export function TheNav() {
}

return (
<nav className="md:mt-5 md:mb-18 mt-4 mb-10 flex gap-10 flex-col">
<nav className="flex flex-col gap-10 mt-4 mb-10 md:mt-5 md:mb-18">
{REQUEST_TYPE.map((requestItem, requestIndex) => {
const buttons = OPTIONS.map((item) => {
if (requestItem === "Fetch" && item.onlyXHR) {
return null;
}
return item;
}).filter(Boolean) as Option[];
return (
<div key={requestItem} className="flex gap-4 max-md:gap-3">
<div className="opacity-80 text-sm">{requestItem}</div>
<div className="text-sm opacity-80">{requestItem}</div>
<div
className={
`flex max-md:gap-2 gap-4 flex-wrap${REQUEST_TYPE.length - 1 !== requestIndex ? " pb-5 border-b" : ""}`
}
>
{OPTIONS.map(({ value, label, disabled }) => {
{buttons.map(({ value, label, disabled }) => {
return (
<div className="relative" key={`${requestItem}-${value}`}>
<button
Expand Down
31 changes: 31 additions & 0 deletions packages/playground/react-sample/src/components/the-nav/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Request method type
type Method = "GET" | "POST" | "PUT" | "DELETE";

// Response type
type ResponseType = "xml" | undefined;

// Request header value type
type HeaderValue = string;

// Option interface
export interface Option {
type?: "XHR" | "Fetch"
// Displayed label text
label: string
// Value/path
value: string
// HTTP method
method: Method
// Sync request
sync?: boolean
// Optional response type
responseType?: ResponseType
// Optional request headers
headers?: Record<string, HeaderValue>
// Optional request body
body?: Record<string, unknown>
// Only use XHR
onlyXHR?: boolean
// Optional disabled status
disabled?: boolean
}
2 changes: 1 addition & 1 deletion packages/playground/react-sample/src/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function App() {
return (
<>
<TheHead />
<section className="max-md:px-4 px-10">
<section className="px-10 max-md:px-4">
<TheNav />
<main className="space-y-14">
{option
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function createFakeMiddleware(
}
}
else {
res.end();
res.end(JSON.stringify(response, null, 2));
}
}

Expand Down
Loading

0 comments on commit 6b33714

Please sign in to comment.