API
全域 API
除了 ECMAScript 標準庫之外,NSH 應用程式的 JS 環境還有一組標準全域物件:fetch 連同 Headers、Request 和 Response;Streams Standard;URL 和 URLSearchParams;TextEncoder / TextDecoder;Blob、File 和 FormData;EventTarget、Event 和 AbortController;計時器、requestAnimationFrame 和 console;以及用於數字和日期格式化的 Intl。它們以 Rust 原生實現,遵循 WHATWG 標準,毋須 import,因此許多建基於這些標準的 JavaScript 程式庫(HTTP 客戶端、URL 與編碼輔助工具、串流工具)可以原封不動地運作。
本頁列出應用程式擁有的所有全域物件。這些屬於 TSX 環境:Rust 程式碼改用 Rust 的標準庫、crates.io 以及 NSH 的 nsh_core::prelude 服務(fetch、計時器、任務;見 以 Rust 構建),並列於表格的 Rust 一欄。UI 以 元素和應用程式自己的元件構建,應用程式使用的其他一切則來自它所 import 的模組。
所有這些全域物件的 TypeScript 宣告都會生成到 .nsh/types/nsh/globals.d.ts(見專案結構)。
| 分類 | 全域物件 | Rust |
|---|---|---|
| 控制台與計時 | console、performance |
log crate(log::info! 等)、nsh_core::time::now() |
| 計時器與幀 | setTimeout、setInterval、clearTimeout、clearInterval、queueMicrotask、requestAnimationFrame、cancelAnimationFrame |
set_timeout、set_interval、timer(delay).await、request_animation_frame、next_frame().await、Spawned::cancel() |
| Base64 與錯誤 | atob、btoa、DOMException |
base64 crate、Result |
| 事件與中止訊號 | EventTarget、Event、CustomEvent、AbortController、AbortSignal |
訊號與閉包;Spawned::cancel() 會結束任務 |
| URL | URL、URLSearchParams |
url crate |
| 文字編碼 | TextEncoder、TextDecoder |
str::as_bytes、String::from_utf8 |
| Blob 與表單資料 | Blob、File、FormData |
HttpRequest 的 Vec<u8> 本文 |
| Fetch | fetch、Headers、Request、Response |
fetch、HttpRequest、HttpResponse |
| 數字與日期格式化 | Intl.NumberFormat、Intl.DateTimeFormat、toLocaleString |
format! 及 crates.io 的格式化 crate,配合 locale() |
| 串流 | ReadableStream、WritableStream、TransformStream、讀取器、寫入器、控制器、ByteLengthQueuingStrategy、CountQueuingStrategy、TextEncoderStream、TextDecoderStream |
fetch 提供完整本文;background 任務在原生目標上執行長時間連線 |
原生宿主還提供 Buffer、crypto、process 和 WebSocket;其模組、權限和資源限制見標準模組。
#控制台與計時
declare var console: {
log(...data: unknown[]): void;
info(...data: unknown[]): void;
debug(...data: unknown[]): void;
warn(...data: unknown[]): void;
error(...data: unknown[]): void;
};
declare var performance: {
/** Milliseconds since the JS context started, with sub-millisecond precision. */
now(): number;
};
在 Rust 中,log crate 的巨集(log::info!、log::warn! 等)會寫入同一份日誌,並同樣顯示在 nsh run 的終端機中。console 每次呼叫向執行環境的日誌(target 為 js)寫入一行:log 和 info 屬 info 級別,debug、warn 和 error 則屬各自的級別。引數以空格連接;字串原樣輸出,錯誤連同其堆疊追蹤,函式輸出為 [Function name],物件和陣列輸出為 JSON(遇到循環參照時改用 String(value))。
當裝置連接到開發伺服器時,其日誌行會顯示在執行 nsh run / nsh dev 的終端機中,並以裝置名稱作前綴;見開發伺服器與除錯。
performance.now() 是 requestAnimationFrame 傳給其回呼的時鐘;在 Rust 中,nsh_core::time::now() 以及 Instant 從 request_animation_frame 和 next_frame() 取得的值就是該時鐘。performance 沒有其他成員。
#計時器與幀
function setTimeout(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function setInterval(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function clearTimeout(id: number | undefined): void;
function clearInterval(id: number | undefined): void;
function queueMicrotask(callback: () => void): void;
/** Runs `callback` at the next display frame with its time in milliseconds. */
function requestAnimationFrame(callback: (time: number) => void): number;
function cancelAnimationFrame(id: number): void;
Rust:set_timeout(delay, f) 和 set_interval(period, f) 會返回 Spawned 控制代碼,其 cancel() 可清除計時器,timer(delay).await 在任務中等待,request_animation_frame(f) / next_frame().await 給出下一幀的時間;它們全部都會隨建立它們的元件一同結束。計時器的行為與瀏覽器一致:缺少、為負數或非有限的延遲視為 0,返回的 id 是正整數,清除未知的 id 則不會有任何作用。handler 必須是函式(不會評估字串處理器)。計時器不會把巢狀延遲限制為 4 ms。
requestAnimationFrame 回呼只會執行一次,在引擎產生的下一幀執行,且全部使用相同的 performance.now() 時間。若回呼再請求另一幀,則會在隨後的一幀被呼叫,因此常見的迴圈寫法可以正常運作:
spawn(async move {
let start = next_frame().await;
loop {
let time = next_frame().await;
let elapsed = time.duration_since(start).as_secs_f32() * 1000.0;
progress.set((elapsed / 300.0).min(1.0));
if elapsed >= 300.0 {
break;
}
}
});let start = 0;
function tick(time: number) {
if (!start) start = time;
progress.set(Math.min(1, (time - start) / 300));
if (time - start < 300) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);#Base64 與錯誤
function atob(data: string): string;
function btoa(data: string): string;
class DOMException extends Error {
constructor(message?: string, name?: string);
readonly name: string;
readonly message: string;
readonly code: number;
}
atob / btoa 在二進位字串與 Base64 之間轉換,與瀏覽器相同。DOMException 是被中止的操作用來 reject 的錯誤:沒有原因的 AbortController.abort() 會產生名為 "AbortError" 的 DOMException,而 AbortSignal.timeout 則產生名為 "TimeoutError" 的錯誤。
#事件與中止訊號
class EventTarget {
constructor();
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
options?: AddEventListenerOptions | boolean): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
options?: { capture?: boolean } | boolean): void;
dispatchEvent(event: Event): boolean;
}
interface AddEventListenerOptions {
capture?: boolean;
once?: boolean;
passive?: boolean;
signal?: AbortSignal;
}
class Event {
constructor(type: string, init?: { bubbles?: boolean; cancelable?: boolean; composed?: boolean });
readonly type: string;
readonly target: EventTarget | null;
readonly currentTarget: EventTarget | null;
readonly eventPhase: number; // NONE 0, CAPTURING_PHASE 1, AT_TARGET 2, BUBBLING_PHASE 3
readonly bubbles: boolean;
readonly cancelable: boolean;
readonly defaultPrevented: boolean;
readonly composed: boolean;
readonly isTrusted: boolean;
readonly timeStamp: number;
returnValue: boolean;
cancelBubble: boolean;
readonly srcElement: EventTarget | null; // legacy: `target`
initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void; // legacy
composedPath(): EventTarget[];
preventDefault(): void;
stopPropagation(): void;
stopImmediatePropagation(): void;
}
class CustomEvent<T = any> extends Event {
constructor(type: string, init?: EventInit & { detail?: T });
readonly detail: T;
}
class AbortController {
readonly signal: AbortSignal;
abort(reason?: any): void;
}
class AbortSignal extends EventTarget {
readonly aborted: boolean;
readonly reason: any;
onabort: ((event: Event) => void) | null;
throwIfAborted(): void;
static abort(reason?: any): AbortSignal;
static timeout(milliseconds: number): AbortSignal;
static any(signals: Iterable<AbortSignal>): AbortSignal;
}
這些是 DOM Standard 的類別,只有一個差異:這裡沒有樹狀結構,因此事件只會派發給其目標。捕獲監聽器先執行,然後才是其他監聽器,與 DOM 在目標處的行為相同;bubbles 沒有作用。這些類別可以被繼承,因此 EventTarget 可成為一個小型事件發射器:
class Channel extends EventTarget {
send(message: string) {
this.dispatchEvent(new CustomEvent("message", { detail: message }));
}
}
const channel = new Channel();
channel.addEventListener("message", (e) => console.log((e as CustomEvent<string>).detail));
channel.send("hello");
這些事件與元素的事件(onPress、onChange 等)無關,後者是 props;見觸控與手勢。
AbortSignal 可取消 fetch 請求和串流管道,並移除以 { signal } 加入的監聽器。在 Rust 中,請求在任務內執行,取消該任務即可取消請求:
let search = spawn(async move {
let _ = fetch("https://api.example.com/search?q=nsh").await;
});
// later, for example when the screen closes (a task also ends with its component):
search.cancel();const controller = new AbortController();
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]);
fetch("https://api.example.com/search?q=nsh", { signal }).catch((e) => {
if (e.name === "AbortError" || e.name === "TimeoutError") return;
throw e;
});
// later, for example when the screen closes:
controller.abort();#URL
class URL {
constructor(url: string | URL, base?: string | URL);
static canParse(url: string | URL, base?: string | URL): boolean;
static parse(url: string | URL, base?: string | URL): URL | null;
/** A `blob:` URL `fetch` reads `blob` from, until revoked. */
static createObjectURL(blob: Blob): string;
static revokeObjectURL(url: string): void;
href: string;
readonly origin: string;
protocol: string;
username: string;
password: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
readonly searchParams: URLSearchParams;
toString(): string;
toJSON(): string;
}
class URLSearchParams {
constructor(init?: string | Record<string, string> | Iterable<[string, string]> | URLSearchParams);
readonly size: number;
append(name: string, value: string): void;
delete(name: string, value?: string): void;
get(name: string): string | null;
getAll(name: string): string[];
has(name: string, value?: string): boolean;
set(name: string, value: string): void;
sort(): void;
forEach(callback: (value: string, key: string, parent: URLSearchParams) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, string]>;
keys(): IterableIterator<string>;
values(): IterableIterator<string>;
[Symbol.iterator](): IterableIterator<[string, string]>;
toString(): string;
}
URL 依照 URL Standard 解析。URL 的 searchParams 在兩個方向上都是即時連動的:更改參數會改寫查詢字串,而設定 search 或 href 則會重新解析它。Rust 程式碼使用遵循同一標準的 url crate:
let mut url = url::Url::parse("https://api.example.com")?.join("/v1/items")?;
url.query_pairs_mut().append_pair("page", "2").append_pair("tag", "new");
url.as_str(); // "https://api.example.com/v1/items?page=2&tag=new"const url = new URL("/v1/items", "https://api.example.com");
url.searchParams.set("page", "2");
url.searchParams.append("tag", "new");
url.href; // "https://api.example.com/v1/items?page=2&tag=new"URLSearchParams 的迭代器(以及 Headers 和 FormData 的迭代器)會遍歷建立迭代器時所取的快照,並具備迭代器輔助方法(map、filter、toArray 等)。
URL.createObjectURL(blob) 會返回一個 blob: URL,在呼叫 revokeObjectURL 之前,fetch(連同 GET)都可以讀取它。<Image> 可將其顯示為其 src 或 placeholder(Image)。
#文字編碼
class TextEncoder {
readonly encoding: "utf-8";
encode(input?: string): Uint8Array;
encodeInto(source: string, destination: Uint8Array): { read: number; written: number };
}
/** UTF-8, UTF-16LE / BE and windows-1252 (with the labels of latin1 and ASCII). */
class TextDecoder {
constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
decode(input?: BufferSource, options?: { stream?: boolean }): string;
}
TextDecoder 支援的編碼比瀏覽器少:UTF-8、UTF-16LE、UTF-16BE 和 windows-1252(標籤 latin1、iso-8859-1、ascii 等也指向它,與 Encoding Standard 的規定一致)。其他標籤會拋出 RangeError。使用 { stream: true } 時,被切在兩個區塊之間的多位元組序列會保留至下一次呼叫。
#Blob 與表單資料
class Blob {
constructor(blobParts?: Iterable<BlobPart>, options?: { type?: string; endings?: "transparent" | "native" });
readonly size: number;
readonly type: string;
slice(start?: number, end?: number, contentType?: string): Blob;
arrayBuffer(): Promise<ArrayBuffer>;
bytes(): Promise<Uint8Array>;
text(): Promise<string>;
stream(): ReadableStream<Uint8Array>;
}
class File extends Blob {
constructor(fileBits: Iterable<BlobPart>, fileName: string,
options?: { type?: string; endings?: "transparent" | "native"; lastModified?: number });
readonly name: string;
readonly lastModified: number;
readonly webkitRelativePath: string;
}
class FormData {
constructor();
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): File | string | null;
getAll(name: string): (File | string)[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
forEach(callback: (value: File | string, key: string, parent: FormData) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, File | string]>;
keys(): IterableIterator<string>;
values(): IterableIterator<File | string>;
[Symbol.iterator](): IterableIterator<[string, File | string]>;
}
type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string;
Blob 將其位元組保存在記憶體中,而 slice 會共用這些位元組而非複製。Blob 只是記憶體內的值:File 背後沒有檔案系統存取,FormData 也沒有由表單元素建構的建構函式。
FormData 本文會以 multipart/form-data 發送,而 Response.formData() / Request.formData() 可解析 multipart/form-data 和 application/x-www-form-urlencoded 兩種格式。
const form = new FormData();
form.append("title", "Holiday");
form.append("photo", new Blob([bytes], { type: "image/jpeg" }), "photo.jpg");
await fetch("https://api.example.com/upload", { method: "POST", body: form });
#Fetch
/**
* HTTP through the platform's network stack (NSURLSession,
* HttpURLConnection), streaming both ways; `data:` and `blob:` URLs are
* answered locally, and `asset:/` URLs with the app's bundled files.
*/
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
interface RequestInit {
method?: string;
headers?: HeadersInit;
/** A ReadableStream is sent as it is read (chunked), with `duplex: "half"`. */
body?: BodyInit | null;
duplex?: "half";
signal?: AbortSignal | null;
/** "manual" gives the 3xx response itself. */
redirect?: "follow" | "manual" | "error";
credentials?: "omit" | "same-origin" | "include";
cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached";
mode?: "same-origin" | "no-cors" | "cors";
referrer?: string;
referrerPolicy?: string;
integrity?: string;
keepalive?: boolean;
priority?: "high" | "low" | "auto";
}
type HeadersInit = Headers | Record<string, string> | Iterable<[string, string]>;
type BodyInit = ReadableStream<Uint8Array> | Blob | BufferSource | FormData | URLSearchParams | string;
fetch 透過平台自身的網絡堆疊發送 HTTP 和 HTTPS 請求:在 iOS 和 Mac Catalyst 上使用 NSURLSession,在 Android 上使用 HttpURLConnection。因此系統的代理伺服器設定、TLS 設定和憑證信任都會生效。data: URL 和 blob: URL(來自 URL.createObjectURL)會在本機直接回應,不經過網絡;任何其他協定都會 reject。Rust 的 fetch(request) 經由同一個堆疊和同樣的 capabilities,接受 URL 或 HttpRequest,並以包含完整本文的 HttpResponse resolve(Networking):
#[derive(serde::Deserialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
async fn load_todos(token: &str) -> Result<Vec<Todo>, String> {
let request = HttpRequest::get("https://api.example.com/todos").header("Authorization", format!("Bearer {token}"));
let response = fetch(request).await?;
if !response.ok() {
return Err(format!("HTTP {}", response.status()));
}
serde_json::from_slice(&response.body).map_err(|e| e.to_string())
}type Todo = { id: number; title: string; done: boolean };
async function loadTodos(): Promise<Todo[]> {
const res = await fetch("https://api.example.com/todos", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}#Headers、Request 與 Response
class Headers {
constructor(init?: HeadersInit);
append(name: string, value: string): void;
delete(name: string): void;
get(name: string): string | null;
getSetCookie(): string[];
has(name: string): boolean;
set(name: string, value: string): void;
forEach(callback: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, string]>;
keys(): IterableIterator<string>;
values(): IterableIterator<string>;
[Symbol.iterator](): IterableIterator<[string, string]>;
}
interface Body {
/** The body as it arrives (a byte stream), or null. */
readonly body: ReadableStream<Uint8Array> | null;
readonly bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
bytes(): Promise<Uint8Array>;
formData(): Promise<FormData>;
json(): Promise<any>;
text(): Promise<string>;
}
class Request implements Body {
constructor(input: string | URL | Request, init?: RequestInit);
readonly method: string;
readonly url: string;
readonly headers: Headers;
readonly destination: string;
readonly referrer: string;
readonly referrerPolicy: string;
readonly mode: string;
readonly credentials: string;
readonly cache: string;
readonly redirect: "follow" | "manual" | "error";
readonly integrity: string;
readonly keepalive: boolean;
readonly isReloadNavigation: boolean;
readonly isHistoryNavigation: boolean;
readonly signal: AbortSignal;
readonly duplex: "half";
clone(): Request;
// ...and the Body members
}
class Response implements Body {
constructor(body?: BodyInit | null, init?: { status?: number; statusText?: string; headers?: HeadersInit });
static json(data: unknown, init?: ResponseInit): Response;
static error(): Response;
static redirect(url: string | URL, status?: number): Response;
readonly type: "basic" | "default" | "error";
readonly url: string;
readonly redirected: boolean;
readonly status: number;
readonly ok: boolean;
readonly statusText: string;
readonly headers: Headers;
clone(): Response;
// ...and the Body members
}
#串流傳輸
回應的 body 是一個隨網絡傳來資料而逐步填充的位元組串流,因此大型或長時間的回應(下載、server-sent events、串流輸出的模型結果)可以在抵達的同時處理。當有超過 1 MiB 的資料等待讀取時,請求會暫停,直至應用程式再次讀取。
const res = await fetch("https://api.example.com/events");
const lines = res.body!.pipeThrough(new TextDecoderStream());
for await (const chunk of lines) {
console.log(chunk);
}
ReadableStream 請求本文會在網絡要求時逐塊上傳;請按 Fetch Standard 的要求傳入 duplex: "half":
await fetch("https://api.example.com/upload", {
method: "PUT",
body: blob.stream(),
duplex: "half",
});
#與瀏覽器的差異
- 沒有 CORS,也沒有 origin。 應用程式不是網頁:只要其 capabilities 允許,它可以與任何主機通訊。
mode會被接受但被忽略。 - 沒有禁用標頭。 應用程式可以設定
Cookie、User-Agent、Origin及任何其他標頭。若未給出Accept標頭,則會發送Accept: */*。 redirect: "manual"會直接交付 3xx 回應本身,包括其狀態碼和Location標頭,而不是不透明的過濾回應。"follow"會跟隨重新導向(每一跳都會按 capabilities 檢查);"error"則在回應為重新導向時 reject。FormData本文一律帶有Content-Type: multipart/form-data; boundary=…,即使已設定Content-Type標頭亦然。HTTP 程式庫通常會設定預設的內容類型,只在偵測到瀏覽器時才清除;沒有 boundary,伺服器便無法解析本文。credentials、cache、referrer、referrerPolicy、integrity、keepalive和priority會被驗證並保留在Request物件上,但不會傳給網絡堆疊:快取和 cookie 處理由平台的堆疊負責。- Capabilities。 當應用程式在
nsh.json中宣告了networkcapabilities(或嵌入式應用程式授予的權限較少)時,向不被允許的主機發出的請求不會到達網絡,fetch會以TypeErrorreject,其code為"ERR_ACCESS_DENIED",permission為"network",而resource為被拒絕的 URL。見沙盒與 capabilities。 - 錯誤。 網絡故障會以
TypeErrorreject,其訊息以Network request failed:開頭;中止則以訊號的reasonreject。
有關在元件中載入資料的指南,見 Networking。
#串流
完整的 WHATWG Streams Standard:可讀串流(預設串流和位元組串流、BYOB 讀取器、tee、pipeTo / pipeThrough、ReadableStream.from、非同步迭代)、可寫串流、轉換串流、兩種佇列策略,以及文字編碼串流。
class ReadableStream<R = any> {
constructor(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number });
constructor(underlyingSource?: UnderlyingDefaultSource<R>, strategy?: QueuingStrategy<R>);
static from<R>(iterable: Iterable<R | PromiseLike<R>> | AsyncIterable<R>): ReadableStream<R>;
readonly locked: boolean;
cancel(reason?: any): Promise<void>;
getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
getReader(): ReadableStreamDefaultReader<R>;
pipeThrough<T>(transform: { writable: WritableStream<R>; readable: ReadableStream<T> },
options?: StreamPipeOptions): ReadableStream<T>;
pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;
tee(): [ReadableStream<R>, ReadableStream<R>];
values(options?: { preventCancel?: boolean }): AsyncIterableIterator<R>;
[Symbol.asyncIterator](options?: { preventCancel?: boolean }): AsyncIterableIterator<R>;
}
class ReadableStreamDefaultReader<R = any> {
constructor(stream: ReadableStream<R>);
readonly closed: Promise<void>;
read(): Promise<{ done: false; value: R } | { done: true; value?: R }>;
releaseLock(): void;
cancel(reason?: any): Promise<void>;
}
class ReadableStreamBYOBReader {
constructor(stream: ReadableStream<Uint8Array>);
readonly closed: Promise<void>;
read<T extends ArrayBufferView>(view: T, options?: { min?: number }): Promise<ReadableStreamReadResult<T>>;
releaseLock(): void;
cancel(reason?: any): Promise<void>;
}
class ReadableStreamDefaultController<R = any> {
readonly desiredSize: number | null;
close(): void;
enqueue(chunk?: R): void;
error(e?: any): void;
}
class ReadableByteStreamController {
readonly byobRequest: ReadableStreamBYOBRequest | null;
readonly desiredSize: number | null;
close(): void;
enqueue(chunk: ArrayBufferView): void;
error(e?: any): void;
}
class ReadableStreamBYOBRequest {
readonly view: ArrayBufferView | null;
respond(bytesWritten: number): void;
respondWithNewView(view: ArrayBufferView): void;
}
class WritableStream<W = any> {
constructor(underlyingSink?: UnderlyingSink<W>, strategy?: QueuingStrategy<W>);
readonly locked: boolean;
abort(reason?: any): Promise<void>;
close(): Promise<void>;
getWriter(): WritableStreamDefaultWriter<W>;
}
class WritableStreamDefaultWriter<W = any> {
constructor(stream: WritableStream<W>);
readonly closed: Promise<void>;
readonly desiredSize: number | null;
readonly ready: Promise<void>;
abort(reason?: any): Promise<void>;
close(): Promise<void>;
releaseLock(): void;
write(chunk?: W): Promise<void>;
}
class WritableStreamDefaultController {
readonly signal: AbortSignal;
error(e?: any): void;
}
class TransformStream<I = any, O = any> {
constructor(transformer?: Transformer<I, O>, writableStrategy?: QueuingStrategy<I>,
readableStrategy?: QueuingStrategy<O>);
readonly readable: ReadableStream<O>;
readonly writable: WritableStream<I>;
}
class TransformStreamDefaultController<O = any> {
readonly desiredSize: number | null;
enqueue(chunk?: O): void;
error(reason?: any): void;
terminate(): void;
}
class ByteLengthQueuingStrategy {
constructor(init: { highWaterMark: number });
readonly highWaterMark: number;
readonly size: (chunk: ArrayBufferView) => number;
}
class CountQueuingStrategy {
constructor(init: { highWaterMark: number });
readonly highWaterMark: number;
readonly size: (chunk: any) => 1;
}
class TextEncoderStream {
readonly encoding: "utf-8";
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<string>;
}
class TextDecoderStream {
constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
readonly readable: ReadableStream<string>;
readonly writable: WritableStream<BufferSource>;
}
底層的 source、sink 和 transformer 物件(UnderlyingDefaultSource、UnderlyingByteSource、UnderlyingSink、Transformer)以及 StreamPipeOptions(preventAbort、preventCancel、preventClose、signal)均為標準形態;其完整宣告見 globals.d.ts。
TextEncoderStream 會將被切在兩個字串區塊之間的代理對保持在一起,TextDecoderStream 則對被切在兩個位元組區塊之間的多位元組序列做同樣處理。
一個將文字拆分為行的轉換串流:
function lines(): TransformStream<string, string> {
let rest = "";
return new TransformStream({
transform(chunk, controller) {
const parts = (rest + chunk).split("\n");
rest = parts.pop()!;
for (const line of parts) controller.enqueue(line);
},
flush(controller) {
if (rest) controller.enqueue(rest);
},
});
}
const res = await fetch("https://api.example.com/log");
for await (const line of res.body!.pipeThrough(new TextDecoderStream()).pipeThrough(lines())) {
console.log(line);
}
#數字與日期格式化
QuickJS 本身沒有 Intl;NSH 加入了一個,並以 Rust 進行格式化:Intl.NumberFormat、Intl.DateTimeFormat 和 Intl.getCanonicalLocales,以及數字和 BigInt 的 toLocaleString,還有日期的 toLocaleString / toLocaleDateString / toLocaleTimeString,它們接受相同的 locales 和 options。其宣告是 TypeScript 自帶的(ES2023 程式庫),因此不在 globals.d.ts 中。
(1234567.891).toLocaleString(); // "1,234,567.891" (en-US)
(1234.5).toLocaleString("de-DE", { style: "currency", currency: "EUR" }); // "1.234,50 €"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000); // "1.5M"
new Intl.NumberFormat("en", { style: "percent", signDisplay: "exceptZero" }).format(0.042); // "+4%"
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }); // "09:05"
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short" }).format(date); // "Oct 16, 2026, 3:04 PM"
若沒有 locales,則使用裝置的地區設定:其區域格式(iOS 的 NSLocale.currentLocale、Android 的 Locale.getDefault()),在 nsh test 和網頁上則為 en-US。
- 數字:
decimal、percent、currency(currencyDisplaysymbol、narrowSymbol、code)和unit(常見單位:長度、質量、時間長度、資料大小、溫度、速度)樣式;minimumIntegerDigits、minimum/maximumFractionDigits、minimum/maximumSignificantDigits、roundingPriority、trailingZeroDisplay、useGrouping、signDisplay;standard、scientific、engineering和compact記數法(compactDisplayshort/long)。捨入為四捨五入(遠離零),並按所寫的數字進行(1.005捨入為1.01,與瀏覽器相同)。format也可精確地接受 BigInt 和十進位字串。formatToParts和resolvedOptions的運作與瀏覽器相同。 - 日期:
weekday、year、month(numeric、2-digit、short、long、narrow)、day、hour、minute、second、fractionalSecondDigits、hour12/hourCycle、timeZoneName以及dateStyle/timeStyle,採用公曆。timeZone取裝置的時區(在瀏覽器中則為瀏覽器的時區)、UTC或偏移量("+08:00");其他時區名稱會拋出RangeError(沒有時區資料庫)。 - 地區設定:地區設定資料是一張常見語言的表(英語、德語、法語、西班牙語、意大利語、葡萄牙語、荷蘭語、俄語、瑞典語、波蘭語、土耳其語、印尼語、越南語、泰語、印地語、中文、日語、韓語)及其地區變體(
en-GB、en-IN、de-CH、es-MX、pt-PT、zh-TW等),並非完整的 CLDR,因此細節可能與瀏覽器的輸出有所不同。沒有資料的地區設定會回退到所要求的下一個地區設定,然後是裝置的,最後是en-US;俄語、瑞典語、波蘭語、土耳其語、印尼語、越南語、泰語和印地語的日期沒有月份名稱(月份以數字書寫,星期則以英文書寫)。數字一律使用拉丁數字。 - 未提供:
Intl.Collator(String.prototype.localeCompare是 QuickJS 的,會忽略地區設定)、PluralRules、RelativeTimeFormat、ListFormat、DisplayNames、Segmenter和Intl.Locale;currencySign: "accounting"和currencyDisplay: "name"的格式化結果分別為standard和code。
#效能
所有這些類別都是原生的:解析 URL、文字編碼與解碼、構建 multipart 本文以及搬移串流區塊,都在 Rust 中進行,而不是在 QuickJS 中。儘管如此,從 JS 發出的每次呼叫都要跨入執行環境,因此請優先使用整個緩衝區的操作(res.json()、res.text()、encoder.encode(s)),而不要在 JS 中逐位元組迴圈。見效能。