Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Rede

Apps NSH fazem requisições HTTP com fetch: em Rust, a função de nsh_core::prelude, aguardada com await em uma task, e em TSX, o global padrão. Ambos rodam sobre a pilha de rede da plataforma (NSURLSession no iOS, HttpURLConnection no Android, o do navegador na web). Assim, as requisições usam o TLS, os proxies e o HTTP/2 do sistema, e ambos passam pelas mesmas verificações de capabilities. O fetch do Rust recebe uma URL ou um HttpRequest e devolve a resposta completa (HttpResponse: status(), ok(), header(name), text(), body). O do TSX faz streaming nos dois sentidos: o corpo da resposta chega como um stream, conforme a pilha o entrega, e o corpo de uma requisição pode ser um stream enviado à medida que você o produz. O TSX também tem os complementos da Fetch API: Headers, Request, Response, AbortController, FormData, Blob, File, URL, URLSearchParams, TextEncoder / TextDecoder e Web Streams.

#Obtendo JSON

use nsh_core::prelude::*;

#[derive(Clone, PartialEq)]
struct Post {
    id: u64,
    title: String,
}

fn parse(body: &str) -> Result<Vec<Post>, String> {
    let posts: Vec<serde_json::Value> = serde_json::from_str(body).map_err(|e| e.to_string())?;
    Ok(posts
        .iter()
        .map(|p| Post {
            id: p["id"].as_u64().unwrap_or_default(),
            title: p["title"].as_str().unwrap_or_default().to_owned(),
        })
        .collect())
}

#[component]
fn App() -> impl IntoElement {
    let posts = resource(
        || Some(()),
        |(), _| async move {
            let response = fetch("https://jsonplaceholder.typicode.com/posts?_limit=5").await?;
            if !response.ok() {
                return Err(format!("HTTP {}", response.status()));
            }
            parse(&response.text())
        },
    );
    scroll().flex_1().child(
        view()
            .p_4()
            .pt(px(60.0))
            .gap_2()
            .child(
                text(move || {
                    if posts.loading() { "loading…".to_owned() } else { posts.error().unwrap_or_default() }
                })
                .text_color(rgb(0x57606a)),
            )
            .child(each(
                move || posts.get().unwrap_or_default(),
                |post| post.id,
                |post, _| text(move || post.with(|p| p.title.clone())).number_of_lines(1),
            )),
    )
}

resource mantém o valor, o erro e se um carregamento está em andamento, recarrega a partir de uma origem que muda (ou em refetch()) e cancela um carregamento que não importa mais (o componente desaparece ou um carregamento mais novo começa): no Rust, descartando a task do carregamento, o que cancela sua requisição; no TSX, por meio do signal que ele repassa. O fetch do Rust aceita URLs absolutas; o JSON é interpretado com um crate como serde_json.

Em TSX, um Response lê seu corpo uma única vez, como json(), text(), arrayBuffer(), bytes(), blob() ou formData(), ou como o stream body. response.ok é true para status 2xx; como nos navegadores, um status de erro HTTP não rejeita a promise.

#Enviando dados

use nsh_core::prelude::*;

// JSON
let body = serde_json::json!({ "title": "Groceries", "items": ["milk", "eggs"] });
let created = fetch(
    HttpRequest::post("https://api.example.com/notes", body.to_string())
        .header("Content-Type", "application/json")
        .header("Authorization", format!("Bearer {token}")),
)
.await?;

// A form, URL-encoded
fetch(
    HttpRequest::post("https://api.example.com/login", "user=ada&password=…")
        .header("Content-Type", "application/x-www-form-urlencoded"),
)
.await?;

// Other methods
fetch(HttpRequest::new("DELETE", "https://api.example.com/notes/1")).await?;

HttpRequest::new(method, url), get e post fazem uma requisição, .header(..) e .body(..) a completam. Em TSX, body aceita uma string, URLSearchParams, FormData, um Blob ou File, um ArrayBuffer ou typed array, ou um ReadableStream.

#Enviando arquivos com FormData

Em TSX, um corpo FormData é enviado como multipart/form-data com seu boundary. O NSH sempre define esse content type para um corpo FormData, mesmo que uma biblioteca defina antes um Content-Type padrão, porque sem o boundary o servidor não consegue interpretá-lo.

const form = new FormData();
form.append("title", "Sunset");
form.append("photo", new Blob([bytes], { type: "image/jpeg" }), "sunset.jpg");

const response = await fetch("https://api.example.com/photos", { method: "POST", body: form });

#Enviando uma foto escolhida pelo usuário

Picker entrega as fotos escolhidas pelo usuário como arquivos cujos bytes permanecem nativos, reduzidos e em um formato aceito pelos servidores; o Rust envia seus bytes, e o TSX adiciona o File a um FormData:

use nsh_core::geometry::DevicePixels;
use nsh_core::picker::PickRequest;
use nsh_core::prelude::*;

let mut request = PickRequest::new(false, 1);
request.max_dimension = Some(DevicePixels(2048));
let picked = pick_images(request);
spawn(async move {
    if let Some(photo) = picked.await.into_iter().next() {
        let upload = HttpRequest::post("https://api.example.com/photos", photo.data.to_vec())
            .header("Content-Type", photo.mime.as_str());
        let _ = fetch(upload).await;
    }
});

#Streaming

O fetch do TSX faz streaming de respostas e de corpos de requisição; o do Rust entrega o corpo inteiro junto com a resposta.

#Lendo uma resposta conforme ela chega

response.body é um ReadableStream<Uint8Array> preenchido à medida que a rede entrega os bytes. Leia-o pedaço por pedaço para mostrar o progresso ou para processar linhas de server-sent events assim que elas chegam:

const response = await fetch("https://api.example.com/large.csv");
const total = Number(response.headers.get("Content-Length") ?? 0);
let received = 0;

for await (const chunk of response.body!) {
  received += chunk.length;
  progress.set(total ? received / total : 0);
}

Streams de texto são decodificados com TextDecoderStream, e um TransformStream os divide em linhas:

const response = await fetch("https://api.example.com/events", { headers: { Accept: "text/event-stream" } });
let rest = "";
const lines = response
  .body!.pipeThrough(new TextDecoderStream())
  .pipeThrough(
    new TransformStream<string, string>({
      transform(chunk, controller) {
        const parts = (rest + chunk).split("\n");
        rest = parts.pop() ?? "";
        for (const line of parts) controller.enqueue(line);
      },
    }),
  );

for await (const line of lines) {
  if (line.startsWith("data: ")) messages.update((m) => [...m, line.slice(6)]);
}

O back pressure funciona de ponta a ponta: enquanto mais de 1 MiB de uma resposta aguarda sem ser lido, a requisição é pausada no nível da rede até que seu código a alcance.

#Fazendo streaming do corpo de uma requisição

Um corpo ReadableStream é enviado pedaço por pedaço, conforme a pilha de rede o solicita (chunked transfer). Passe duplex: "half", como exige o padrão Fetch:

const encoder = new TextEncoder();
const body = new ReadableStream<Uint8Array>({
  async start(controller) {
    for (const line of logLines) controller.enqueue(encoder.encode(line + "\n"));
    controller.close();
  },
});

await fetch("https://api.example.com/logs", { method: "POST", body, duplex: "half" });

Um corpo em streaming não pode ser enviado duas vezes, portanto um redirecionamento 307 ou 308 de uma requisição em streaming falha.

#Cancelamento e timeouts

Em Rust, uma requisição roda em uma task e é cancelada junto com ela: quando o componente da task desaparece, ou com cancel() na task que spawn devolve. Em TSX, passe um AbortSignal para cancelar uma requisição; a promise é rejeitada com o motivo (reason) do signal (por padrão, um AbortError DOMException), e a leitura de um corpo em andamento é interrompida.

let task = spawn(async move {
    if let Ok(response) = fetch(format!("https://api.example.com/search?q={query}")).await {
        results.set(response.text());
    }
});

// Typing again: cancel the previous search.
task.cancel();

Em TSX, AbortSignal.timeout(ms) cria um signal que aborta após um atraso, e AbortSignal.any([...]) combina signals:

const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });

Um componente que carrega dados deve cancelar quando desaparece: uma task do Rust faz isso sozinha, e o TSX aborta em cleanup:

use nsh_core::prelude::*;

fn weather(city: String) -> impl IntoElement {
    let data = signal(String::new());
    // The task, and its request, end when the component goes away.
    spawn(async move {
        if let Ok(response) = fetch(format!("https://api.example.com/weather?city={city}")).await {
            data.set(response.text());
        }
    });
    text(move || data.get())
}

#Erros

Uma requisição que não produz uma resposta HTTP falha: o fetch do Rust devolve um Err com a mensagem, e o do TSX é rejeitado com um TypeError cuja mensagem começa com Network request failed:. Isso acontece sem conexão, com falha de DNS ou de TLS, com um esquema de URL não suportado ou com uma requisição que as capabilities do seu app não permitem. Verifique ok() / status() (TSX: response.ok, response.status) para erros HTTP.

match fetch(url).await {
    Ok(response) if response.ok() => items.set(response.text()),
    Ok(response) => error.set(format!("Server answered {}", response.status())),
    Err(message) => error.set(message), // no response, or not granted
}

#A partir de componentes Rust

Componentes Rust, funções #[module] e membros #[class] chamam fetch a partir de uma task: spawn em um componente, resource para um valor que a UI exibe, ou um async fn exportado para o TSX, que o TSX aguarda como uma Promise. Uma requisição é uma URL ou um HttpRequest (HttpRequest::get(url), HttpRequest::post(url, body), .header(name, value)), e sua resposta chega inteira. A task é dona da requisição: descartá-la, ou o componente que a iniciou desaparecer, cancela a requisição. Ela usa a mesma pilha de rede e as mesmas verificações de capabilities do TSX, em todas as plataformas e no navegador.

#Capabilities: quais hosts um app pode acessar

Um app pode declarar em nsh.json com quais hosts ele se comunica. Uma vez que "capabilities" esteja presente, tudo o que não estiver listado é negado:

{
  "capabilities": {
    "network": {
      "hosts": ["api.example.com", "*.cdn.example.com"],
      "http": [
        { "host": "quotes.example.com", "methods": ["GET"], "pathPrefix": "/v1/" }
      ]
    }
  }
}
Chave Descrição
network.hosts Hosts acessíveis com qualquer método: "*" (qualquer host), "*.example.com" (seus subdomínios, não o próprio example.com) ou um nome de host.
network.http Permissões mais granulares: um host, opcionalmente limitado a methods, schemes, um port, paths exato e pathPrefixes.

As verificações são aplicadas no runtime do NSH, fora do código do seu app, antes que uma requisição chegue à rede:

  • Um fetch recusado falha (em TSX com um TypeError, como toda requisição que falha) com uma mensagem que nomeia a chave a adicionar, como Network request failed: fetch GET https://evil.io/ is not granted: add `evil.io` to capabilities.network in nsh.json. Em TSX, o code do erro é "ERR_ACCESS_DENIED", seu permission é "network" e seu resource é a URL recusada (a da requisição ou a de um redirecionamento), de modo que o app distingue uma recusa de uma conexão perdida:

    try {
      await fetch(url);
    } catch (error) {
      if (error.code === "ERR_ACCESS_DENIED") console.warn(`not granted: ${error.resource}`);
      else throw error;
    }
  • Todo redirecionamento é verificado novamente. O NSH segue os redirecionamentos por conta própria (até 20) em vez de deixar a plataforma fazê-lo, de modo que um host permitido não pode redirecionar seu app para um que não seja.

  • As mesmas permissões de network valem para imagens remotas (também em canvases e Image.getSize / prefetch) e para fontes de vídeo. URIs asset: do seu próprio bundle são sempre permitidas.

  • O host é obtido após o último @, de modo que https://ok.com@evil.io conta como evil.io.

Um app sem "capabilities" pode acessar qualquer host: um app standalone pertence ao próprio desenvolvedor. Apps embutidos em um app host ou distribuídos como pacotes assinados devem declará-las; o app host pode conceder menos do que um app declara, e uma requisição precisa ser permitida por ambos. O desenvolvimento com nsh dev / nsh run aplica as mesmas capabilities. Veja Sandbox e capabilities.

#Diferenças em relação aos navegadores

O fetch do TSX segue o padrão Fetch, com as diferenças que fazem sentido para um app e não para uma página web:

  • Sem CORS. Um app se comunica com qualquer host que suas capabilities permitam; não há requisições preflight nem respostas opacas.
  • Sem headers proibidos. Um app pode definir Cookie, User-Agent, Origin e outros.
  • redirect: "manual" devolve a própria resposta 3xx (com seu header Location) em vez de um redirecionamento opaco. redirect: "error" rejeita em um redirecionamento.
  • URLs data: e blob: são respondidas localmente. URL.createObjectURL(blob) cria uma URL blob: que fetch pode ler até que você a revogue.
  • URLs asset:/ são os arquivos que o app distribui, lidos dos arquivos do próprio app sem precisar de capability (Lendo um arquivo em tempo de execução).
  • Opções exclusivas de navegador (mode, credentials, cache, referrer, integrity, keepalive, priority) são aceitas por compatibilidade com bibliotecas.

Bibliotecas construídas sobre fetch (e sobre AbortController, FormData e streams) geralmente funcionam sem alterações. Não há XMLHttpRequest; os hosts nativos fornecem o global WebSocket e o módulo websocket, verificados contra as mesmas permissões de fetch.

#Relacionados