本頁由機器翻譯,可能有錯誤。 閱讀英文原文

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 以元素與應用程式自己的元件建構,應用程式用到的其他一切則來自它所匯入的模組。

這些全域物件的 TypeScript 宣告會產生於 .nsh/types/nsh/globals.d.ts(參閱專案結構)。

群組全域物件Rust
Console 與計時console、performancelog crate(log::info! 等)、nsh_core::time::now()
計時器與影格setTimeout、setInterval、clearTimeout、clearInterval、queueMicrotask、requestAnimationFrame、cancelAnimationFrameset_timeout、set_interval、timer(delay).await、request_animation_frame、next_frame().await、Spawned::cancel()
Base64 與錯誤atob、btoa、DOMExceptionbase64 crate、Result
事件與中止訊號EventTarget、Event、CustomEvent、AbortController、AbortSignal訊號與閉包;Spawned::cancel() 會結束任務
URLURL、URLSearchParamsurl crate
文字編碼TextEncoder、TextDecoderstr::as_bytes、String::from_utf8
Blob 與表單資料Blob、File、FormDataHttpRequest 的 Vec<u8> 本文
Fetchfetch、Headers、Request、Responsefetch、HttpRequest、HttpResponse
數字與日期格式化Intl.NumberFormat、Intl.DateTimeFormat、toLocaleStringformat! 與 crates.io 的格式化 crate,搭配 locale()
StreamsReadableStream、WritableStream、TransformStream、reader、writer、controller、ByteLengthQueuingStrategy、CountQueuingStrategy、TextEncoderStream、TextDecoderStreamfetch 提供完整本文;background 任務在原生目標上執行長連線

原生主機也提供 Buffer、crypto、process 與 WebSocket;其模組、權限與資源限制請參閱標準模組。

#Console 與計時

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() 以及 request_animation_frame 和 next_frame() 所提供的 Instant 就是這個時鐘。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;
        }
    }
});

#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 是被中止的操作所用的拒絕原因:沒有原因的 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 的類別,但有一個差異:沒有樹狀結構,因此事件只會分派給其目標。先執行 capture 監聽器,再執行其他監聽器,與 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();

#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"

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(依 Encoding Standard 的規定,標籤 latin1、iso-8859-1、ascii 等也指向它)。其他標籤會擲出 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)會在本機直接回應,不會接觸網路;其他任何 scheme 都會被拒絕。Rust 的 fetch(request) 經過同一個堆疊並使用相同的 capabilities,接受 URL 或 HttpRequest,並以包含完整本文的 HttpResponse 解析(網路):

#[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())
}

#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" 在回應為重新導向時會拒絕。
  • FormData 本文一律會帶有 Content-Type: multipart/form-data; boundary=…,即使已設定 Content-Type 標頭也一樣。HTTP 函式庫常會設定預設的 content type,並且只在偵測到瀏覽器時才清除;若沒有 boundary,伺服器就無法解析本文。
  • credentials、cache、referrer、referrerPolicy、integrity、keepalive 與 priority 會經過驗證並保留在 Request 物件上,但不會傳給網路堆疊:快取與 cookie 的處理由平台的堆疊決定。
  • Capabilities。 當應用程式在 nsh.json 中宣告 network capabilities(或嵌入的應用程式授予的權限較少)時,對不被允許的主機的請求不會到達網路,fetch 會以 TypeError 拒絕,其 code 為 "ERR_ACCESS_DENIED",permission 為 "network",resource 為被拒絕的 URL。參閱沙箱與 capabilities。
  • 錯誤。 網路失敗會以 TypeError 拒絕,其訊息以 Network request failed: 開頭;中止則以訊號的 reason 拒絕。

關於在元件中載入資料的指南,請參閱網路。

#Streams

完整的 WHATWG Streams Standard:readable stream(預設與位元組串流、BYOB reader、tee、pipeTo / pipeThrough、ReadableStream.from、非同步迭代)、writable stream、transform stream、兩種佇列策略,以及文字編碼串流。

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 會將被切在兩個字串區塊之間的代理對(surrogate pair)保持在一起,TextDecoderStream 則會對被切在兩個位元組區塊之間的多位元組序列做同樣的處理。

一個將文字切分為行的 transform stream:

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(currencyDisplay symbol、narrowSymbol、code)與 unit(常見單位:長度、質量、時間長度、資料大小、溫度、速度)樣式;minimumIntegerDigits、minimum / maximumFractionDigits、minimum / maximumSignificantDigits、roundingPriority、trailingZeroDisplay、useGrouping、signDisplay;standard、scientific、engineering 與 compact 記法(compactDisplay short / 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 中逐位元組的迴圈。參閱效能。