跳到內容

型別化 JavaScript API 客戶端

先開啟完整歌曲讀取 HTML,點「讀取歌曲列表」並選擇一首歌,標題與封面會直接顯示在頁面裡。儲存為 index.html 並透過本地 HTTP 開啟的步驟見快速開始。這個入門專案使用 https://haneoka.org/embed/api-client.js,無需安裝 npm;下文包匯入與 TypeScript 片段用於已經構建的工程。

@haneoka/api-client/haneoka 提供 createHaneokaClient,主入口 createApiClient 保留通用 HTTP 能力。使用包含 ./haneoka 子入口的匹配版本。支援瀏覽器/Node Fetch,也接受宿主 transport(Request)。

import { createHaneokaClient } from "@haneoka/api-client/haneoka";
import { ApiClientError } from "@haneoka/api-client";
const api = createHaneokaClient({ baseUrl: "https://haneoka.org/api/v1/" });
const controller = new AbortController();
try {
const servers = await api.servers({ signal: controller.signal });
const song = await api.entity("songs", "100070", {
server: "intl", locale: "en", signal: controller.signal,
});
console.log(servers, song);
} catch (error) {
if (error instanceof ApiClientError) console.error(error.code, error.status, error.requestId, error.retryable);
else throw error;
}
// 請求期間取消:controller.abort()。

baseUrl 是 /api/v1/ 根。server 預設 intl,可從 servers() 的 id 讀取;locale 為 ja/en/zh-TW/zh-CN/ko。返回 catalog 仍保留所有語言,自帶檔案的其他作者語言交給 embed-core。

let cursor: string | undefined;
do {
const page = await api.page("songs", { server: "intl", limit: 20, ...(cursor ? { cursor } : {}) });
for (const entry of page.items) console.log(entry.id, entry.value);
cursor = page.nextCursor ?? undefined;
} while (cursor !== undefined);
const batch = await api.batch("songs", ["100001", "100070"], { server: "intl" });
console.log(batch.items, batch.missing);

page 預設 limit50,驗證 ID、頁大小、cursor 與 snapshot identity;短頁/空頁仍可能繼續。batch 接受 1–100 個輸入 ID 並返回 items/missing。兩者可用 manifest 宣告的 view 引數。

index 讀取完整索引,entity 讀取一項,relation(resource,relation,key,scope) 讀取關係;relation 不接受 view。都支援 signal 和 decode(value:unknown),預設返回 JSON 型別,decode 校驗並給出業務 DTO 型別。校驗失敗使用 invalid_payload。

release 只使用服務返回的 immutable ID;客戶端自動切到 /servers/{server}/…,普通 alias 不支援 release。chartImageUrl 只構造當前圖片 URL,不下載二進位制:

const url = api.chartImageUrl("100070", "expert", {
server: "intl", locale: "en", format: "svg", height: 720, download: false,
});

ApiClientError 儲存 method/url/status/code/requestId/details/retryable。取消為 request_aborted 且不可重試,HTML/壞 JSON 錯誤頁仍保留 HTTP status;成功回應的格式錯誤為 invalid_content_type、invalid_json 或 invalid_payload。客戶端不自動重試。

構造時可提供 headers 或 transport,只向預期 origin 傳送身份憑據。公開資料無需 session。圖片 bytes 或 排行介面 的獨立 error shape 可用通用客戶端 response() 或原生 fetch。

選擇用法、修改 JSON,再點選載入並執行。每個預設呼叫對應的真實客戶端方法;分頁會將 nextCursor 寫回編輯器供下一次點選。client 是構造引數,scope 是資料讀取引數,request 是通用 HTTP 引數;validateMusicId:true 啟用示例裡的實體校驗 callback。讀取演示接受 GET/HEAD,記錄 URL/status,不記錄 header 值。

修改 JSON 參數,然後點擊載入。模組和請求只在點擊後啟動;輸出包含實際回應與傳輸紀錄。

語言

正在準備範例…

檢視範例程式碼

示例在點選執行後動態匯入已上線的 https://haneoka.org/embed/api-client.js,直接使用官方客戶端。SDK 由已提交原始碼在部署時構建;當前不可變版本以 manifest 的 release 與 modules 為準。npm仍是獨立釋出步驟。

createHaneokaClient(options={}) 提供資料方法,createApiClient(options) 提供通用 HTTP。

欄位型別/必填預設值作用、限制與錯誤
baseUrlstring;通用必填,Haneoka 可選Haneoka 為 https://haneoka.org/api/v1/API 目錄,通用客戶端 trim 並補末尾斜線,空值 TypeError;URL/Request 構造時拒絕非法地址。瀏覽器/SSR 使用絕對 HTTP(S) 地址。
headers可選 HeadersInit:Headers、record、pairs不附加共用頭,逐次請求標頭按大小寫不敏感覆蓋;非法值由 Headers 拒絕。資料 scope 沒有逐次 headers 欄位。
transport可選 (Request)=>Promise<Response>fetch接收完整 Request 和 signal,返回真實 Response。拒絕包裝為 network_error/request_aborted;主動拋 ApiClientError 保留原錯誤。可以配置宿主傳輸、截止時間和憑據。

Haneoka 請求預設 omit/cors/no-referrer,覆蓋 registry 和資料讀取。通用客戶端保持 Request 預設行為,callback 可建立新 Request。構造引數沒有 timeout/retry/maxBytes/cache/dispose;這些策略由 signal/transport 和呼叫方負責。

import { createApiClient } from "@haneoka/api-client";
import { createHaneokaClient } from "@haneoka/api-client/haneoka";
const api = createHaneokaClient({
baseUrl: "https://haneoka.org/api/v1/",
headers: { Accept: "application/json" },
transport(request) { console.log(request.method, request.url); return fetch(request); },
});
const general = createApiClient({ baseUrl: "https://haneoka.org/api/v1/" });

下面覆蓋 HaneokaScope/HaneokaReadOptions;servers 只接受 signal,圖片 URL 使用單獨引數列。

引數型別/預設約束、作用與失敗
resource必填 string 位置引數manifest 名稱,1–64 位小寫字母/數字/連字元,首尾字母數字。保留組 game/search/account/admin/announcements/auth/catalog/community/garupa/me/release/releases/servers/sources/ui-marks 拒絕。非法語法/保留組 TypeError,不存在404。
id、relation key、image songId對應方法必填 string1–256 位,首位字母數字,其餘還可 . _ : ~ -;客戶端編碼為一段,不能含 /。非法 TypeError,缺項按端點404。
server可選 string;intl活動 slug,1–40 位小寫字母數字/連字元、首尾字母數字。語法 TypeError,不活動404,與 locale 獨立。
locale可選 ja/en/zh-TW/zh-CN/ko;省略加 locale query,保留 catalog 多語欄位;非法 TypeError。省略時端點協商語言,自帶 embed 檔案可使用其他語言。
release可選 string;當前版本返回的 r- 加20位小寫十六進位制;切至 /servers/{server}/…,語法 TypeError,不可用404。普通 HTTP alias 拒絕 release query。
signal可選 AbortSignal;無取消請求,傳輸取消為 request_aborted/status0/retryablefalse;取消後新操作使用新 controller。
view可選 string;canonical resource名稱語法同 resource,供 index/entity/batch/page;非法 TypeError,缺 view404,relation 不能提供。
decode可選 (unknown)=>T;JSON-value decoder同步校驗/轉換。index/entity/relation 整體呼叫,batch/page 每實體呼叫;拋錯為 invalid_payload,ApiClientError 原樣保留。泛型本身不做校驗。
方法引數/結果組合與行為
servers(scope={})僅 signal;Promise<{id,displayName,region}[]>校驗 registry.releases,id 是 server slug。
index(resource,options={})shared read options;完整 index 或 decode 值可選 view,不自動分頁。
entity(resource,id,options={})必填 ID;單項 Promise可選 view。
batch(resource,ids,options={})readonly string[];{items:Record<id,T>,missing:string}1–100 個輸入 ID,重複項也先計數;空/超量/非法 ID TypeError。缺失項是 batch 成功結果。
page(resource,options={})shared + limit/cursor;下表 envelope端點需 collection/view entity storage。
relation(resource,relation,key,options={})relation 名語法同 resource,key 同 ID使用 manifest 關係;view+relation TypeError。現存空關係可返回{},關係不存在404。
chartImageUrl(songId,difficulty,options={})返回 string同步構造,不請求,無 signal/decode。

limit?:number 預設50,整數1–100;錯誤在請求前 TypeError。cursor?:string 預設無,由服務返回並原樣傳回,非法/錯 scope 的 cursor 被端點拒絕。它固定 server/resource/view/首 release。保留 scope;快照不可用時重新取第一頁。

回應型別用途
itemsreadonly {id:string,value:T}[]不超過 limit;8分片預算可能短頁/空頁仍可繼續。
total非負 safe integermanifest 總數。
limit1–100整數客戶端要求與請求有效值相同。
nextCursor非空 string/null只有 null 結束。
release.schemaliteral haneoka-resource-release-identity-v1envelope 版本。
release.serverstring匹配請求/預設 server。
release.releaseIdstring若顯式給 release,必須匹配。
release.sourceIdstring保留真實來源快照 ID。

客戶端校驗 envelope/ID/snapshot,不重排或自動翻頁。batch 與 page 的方法引數分離,端點禁止 id 批次混分頁。

欄位預設/範圍作用/錯誤
difficulty必填 easy/normal/hard/expert/special/master非法 TypeError;端點要求該曲有該難度。
server可選,intl同 scope slug 校驗。
locale可選支援語言圖片文字語言。
format可選 svg/png,svg非法 TypeError。
height可選整數360–1440;省略時端點720譜面段高,非法 TypeError;最終畫布尺寸看回應頭。
download可選 boolean;省略true→1、false→0;省略沿端點 attachment 預設。

沒有 release/view/decode/signal 圖片選項。獲取位元組需單獨 fetch;譜面圖片說明字型/渲染部署和錯誤。

欄位型別/預設作用與約束
path必填 string 位置引數相對地址去掉前導 / 後基於 baseUrl;絕對 HTTP(S) 保留 origin。非法 URL/Request 拋原生錯誤。
methodDELETE/GET/HEAD/PATCH/POST/PUT;GETrequest/response 使用,get 強制 GET;非法 body/method 組合由 Request 拒絕。
query可選 readonly record值為 string/number/boolean/null/undefined 或其 readonly array。鍵排序追加,陣列重複,null/undefined 省略,保留已有 URL query。
headers可選 HeadersInit;構造 headers逐次覆蓋。
body可選 BodyInit/null;省略JSON 由呼叫方編碼並設定 MIME,stream/FormData 遵循原生 Fetch。
signal可選 AbortSignal;無取消與自定超時。
decode可選 (unknown)=>T;原 JSONget/request 使用;HEAD/204/205 輸入 undefined,拋錯為 invalid_payload;response 不接此欄位。

get 解析成功 JSON,request 允許 method。兩者非2xx 拋 ApiClientError,有正文需 application/json 或 +json,JSON 格式失敗單獨報錯。response 返回真實 Response(包括失敗),呼叫方檢查 ok/status/消費body。url 構造 string 不請求。

const dto = await general.request("songs/100070", {
method: "GET", query: { server: "intl", locale: "en" },
headers: { Accept: "application/json" }, signal: new AbortController().signal,
});
const raw = await general.response("songs/100070", { method: "HEAD" });
console.log(raw.status, raw.headers.get("X-Haneoka-Release-Id"));
console.log(general.url("songs", { id: ["100001", "100070"] }));

ApiClientError 擴充套件 Error,公開 name/message/繼承 cause/code/details/method/url/status/可選requestId/retryable。HTTP 使用返回 error.code/message/details,HTML/壞錯誤 JSON 回退 http_STATUS 並保留status;requestId 來自 X-Request-Id。網路失敗 status0/network_error/retryabletrue,取消 request_aborted/retryablefalse;HTTP408/429/5xx 標記可重試。成功回應格式/校驗失敗分別 invalid_content_type/invalid_json/invalid_payload,客戶端不自動重試。

自定義傳輸可構造:code/message/method/url 必填,cause/details/requestId 可選,retryable 預設false,status 預設0。此客戶端沒有 subscribe/events/dispose,結果透過 Promise/Response 提供,AbortController 與回應流歸呼叫方。

包的 ./browser 原始碼入口合併 createApiClient、ApiClientError 和 createHaneokaClient,沒有額外引數或瀏覽器副作用;官方託管 facade 的出口以實際 release manifest 為準。

穩定地址為 https://haneoka.org/embed/{name}.js,name 可用 api-client、core、vega、cassiopeia、home-spot、vega-theme。需要固定版本時,讀取 manifest,將同一回應中的 modules 路徑基於 https://haneoka.org/embed/ 解析。舊版本的固定地址保留其對應行為。

import { createHaneokaClient } from "https://haneoka.org/embed/api-client.js";
const api = createHaneokaClient();
console.log(await api.entity("songs", "100070", { server: "intl", locale: "en" }));

當前生產版本由 manifest 標識;穩定入口隨新版本更新,manifest 中的固定路徑保持對應版本行為。