Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

WebView

Una web view mostra una pagina web, oppure l’HTML che le passi, all’interno dell’app: WKWebView su iOS e Mac Catalyst, android.webkit.WebView su Android. Poiché NSH esegue il rendering con view native, una web view si inserisce nel layout come qualsiasi altra view, nell’ordine rispetto alle view che la circondano. Rust ne crea una con web_view(url) o web_view_html(html, base_url), TSX con <WebView> da nsh.

el-web-view.tsx
el-web-view.tsxLive
import { mount, signal, Text, View, WebView, type WebViewNavigation, type WebViewRef } from "nsh";
import { platform, windowSize } from "nsh";
import { button, buttonText, ui } from "./palette";

// The page: a button that posts to the app, and a line that shows what the
// app posts back.
const page = (c: ReturnType<typeof ui>) => `<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Inline page</title>
<style>
  body { font: 15px system-ui, sans-serif; margin: 0; padding: 16px; background: ${c.page}; color: ${c.text}; }
  button { font: inherit; font-weight: 600; padding: 10px 14px; border-radius: 10px; border: 1px solid ${c.border}; background: ${c.accent}; color: ${c.page}; }
  .muted { color: ${c.muted}; }
  #got { margin-top: 12px; }
  #box { margin-top: 12px; height: 48px; border-radius: 10px; background: ${c.border}; transition: background 0.3s; }
</style>
<p class="muted">This is HTML in a web view.</p>
<button id="post">postMessage to the app</button>
<p id="got" class="muted">Nothing from the app yet.</p>
<div id="box"></div>
<script>
  let taps = 0;
  document.getElementById("post").onclick = () =>
    window.nsh.postMessage("tap " + ++taps + " from the page");
  window.addEventListener("message", (e) => { document.getElementById("got").textContent = "App says: " + e.data; });
</script>`;

function Button(props: { label: string; onPress: () => void }) {
  return (
    <View
      onPress={props.onPress}
      {...button()}
    >
      <Text style={buttonText()}>{props.label}</Text>
    </View>
  );
}

const colors = () => [ui().accent, ui().info, ui().violet, ui().down, ui().warn];

function App() {
  const messages = signal<string[]>([]);
  const nav = signal<WebViewNavigation | null>(null);
  const sent = signal(0);
  let painted = 0;
  let web!: WebViewRef;
  const wide = () => windowSize().width >= 700;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>web-view</Text>
      <View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 16 }}>
        <WebView
          ref={web}
          html={page(ui())}
          // Runs after each load: the page reports its title.
          injectedJavaScript={`window.nsh.postMessage("loaded: " + document.title)`}
          onLoad={(e) => nav.set(e)}
          onMessage={(e) => messages.set([e.data, ...messages()].slice(0, 4))}
          style={{ flex: 1, minHeight: 240, borderRadius: 14, overflow: "hidden", borderWidth: 1, borderColor: ui().border }}
        />
        <View style={{ gap: 12, width: wide() ? 320 : undefined }}>
          <Text style={{ color: ui().muted }}>
            {platform.desktop ? "Click" : "Tap"} the button in the page to post to the app; these buttons call the ref's methods.
          </Text>
          <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
            <Button
              label="postMessage"
              onPress={() => {
                sent.update((n) => n + 1);
                web.postMessage(`hello #${sent()}`);
              }}
            />
            <Button
              label="injectJavaScript"
              onPress={() => {
                const color = colors()[painted++ % colors().length];
                web.injectJavaScript(`document.getElementById("box").style.background = "${color}"`);
              }}
            />
            <Button label="reload" onPress={() => web.reload()} />
          </View>
          <View style={{ ...ui().card, borderRadius: 12, padding: 12, gap: 4 }}>
            <Text style={{ color: ui().muted, fontSize: 12 }}>onLoad</Text>
            <Text style={{ numberOfLines: 1, color: ui().text, fontFamily: "monospace", fontSize: 13 }}>
              {nav() ? `title "${nav()!.title}"` : "…"}
            </Text>
            <Text style={{ color: ui().muted, fontSize: 12, marginTop: 6 }}>onMessage, newest first</Text>
            <Each of={messages()}>
              {(m) => <Text style={{ color: ui().text, fontFamily: "monospace", fontSize: 13 }}>{m()}</Text>}
            </Each>
          </View>
        </View>
      </View>
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn browser() -> impl IntoElement {
    let title = signal(SharedString::from("Loading…"));
    view()
        .flex_1()
        .child(text(title).number_of_lines(1))
        .child(
            web_view("https://example.com")
                .flex_1()
                .on_navigation_state_change(move |e: &WebNavigation| title.set(e.title.clone().into())),
        )
}

In TSX una web view accetta uno tra src e html (vedi la WebView API). La guida a web view e video tratta i pattern di messaggistica e le capability.

#Props

Una web view accetta le prop di una view (non ha figli) più:

PropRustTipoPredefinitoDescrizione
srcweb_view(url)stringL’URL della pagina da mostrare.
htmlweb_view_html(html, base_url), .html(html, base_url)stringHTML inline, mostrato al posto di src.
baseUrlbase_url di web_view_html / .htmlstringIl riferimento rispetto al quale vengono risolti i link relativi di html.
injectedJavaScript.injected_java_script(..)stringViene eseguito dopo il caricamento di ogni pagina.
injectedJavaScriptBeforeContentLoaded.injected_java_script_before_content(..)stringViene eseguito prima degli script propri di ogni pagina.
userAgent.user_agent(..)stringLa stringa user agent del browser.
javaScriptEnabled.java_script_enabled(..)booleantrueLe pagine eseguono script.
scrollable.scrollable(..)booleantrueL’utente può scorrere la pagina.
Chiave iosRustTipoDescrizione
allowsBackForwardNavigationGestures.back_forward_gestures(..)booleanScorrimento dai bordi per tornare indietro e avanti.
Chiave desktopRustTipoDescrizione
allowsBackForwardNavigationGestures.back_forward_gestures(..)booleanmacOS: scorrimento con due dita sul trackpad per tornare indietro e avanti; Windows: scorrimento su touchscreen; Linux: scorrimento sul touchpad.

#Eventi

EventoRustPayloadQuando
onLoadStart.on_load_startWebViewNavigationUna pagina ha iniziato a caricarsi.
onLoad.on_loadWebViewNavigationUna pagina ha terminato il caricamento.
onError.on_error{ message, code? } (&str, il messaggio)Il caricamento non è riuscito; code è "ERR_ACCESS_DENIED" per una pagina o html che le capability dell’app non concedono.
onNavigationStateChange.on_navigation_state_changeWebViewNavigationL’URL, il titolo, lo stato di caricamento o la cronologia sono cambiati.
onMessage.on_message{ data: string } (&str)La pagina ha chiamato window.nsh.postMessage(data).
onDownloadWebViewDownloadUn download è iniziato, terminato o fallito (macOS).

WebViewNavigation è { url, title, loading, canGoBack, canGoForward }; in Rust WebNavigation con url, title, loading, can_go_back e can_go_forward.

WebViewDownload è { state, id, url, filename, path, bytes?, message? }: state è "started", "finished" (con il bytes del file) oppure "failed" (con il relativo message). Con un listener onDownload, ciò che la pagina scarica (un allegato, un tipo che la web view non mostra, un link download) viene salvato nella directory dei dati dell’app come downloads/<id>/<filename>, dove punta path, e l’app lo legge con fs/promises.

Su macOS, l’aggiunta del listener consente a qualsiasi pagina caricata e permessa dalla capability webview di salvare i download automaticamente. L’app è responsabile del proprio budget di archiviazione e della rimozione dei file completati da downloads/ quando ha finito di usarli. La rimozione della web view annulla i download in corso e rimuove le relative directory parziali. I download vengono segnalati tramite onDownload; la conversione di una navigazione in download mantiene onError per i veri errori di navigazione. Dai nomi dei file vengono rimossi i caratteri di controllo e i controlli di formattazione bidirezionale, e i nomi lunghi vengono abbreviati a 200 byte preservando un’estensione che rientri nel limite.

#Metodi del ref

MetodoRustDescrizione
reload()reload()Ricarica la pagina.
goBack() / goForward()go_back() / go_forward()Si sposta nella cronologia della pagina.
stopLoading()stop_loading()Interrompe il caricamento.
injectJavaScript(script)inject_java_script(script)Esegue JavaScript nella pagina.
postMessage(data)post_message(data)Invia una stringa alla pagina, che riceve un evento message su window.

#Messaggistica

L’app e la pagina si scambiano stringhe. La pagina invia con window.nsh.postMessage(data); l’app invia con il postMessage(data) del ref (post_message(..) in Rust). Invia JSON per i dati strutturati.

use nsh_core::prelude::*;

const PAGE: &str = r#"<!doctype html>
<meta name="viewport" content="width=device-width">
<button onclick="window.nsh.postMessage('hello from the page')">Post to the app</button>
<p id="got"></p>
<script>
  window.addEventListener("message", (e) => { document.getElementById("got").textContent = e.data; });
</script>"#;

fn bridge() -> impl IntoElement {
    let message = signal(String::new());
    let web = element_ref();
    view()
        .gap_3()
        .child(
            web_view_html(PAGE.to_string(), None)
                .element_ref(&web)
                .h(px(200.0))
                .on_message(move |data: &str| message.set(data.to_string())),
        )
        .child(
            text(move || format!("Post to the page · got: {}", message.get()))
                .on_press(move || web.post_message("hi from the app")),
        )
}

#Capability

Quando l’app dichiara le capability in nsh.json, una web view mostra e naviga verso gli host elencati in webview. Un src su un altro host, oppure html in un’app senza webview, mostra una view vuota; una navigazione di primo livello verso un altro host (un link toccato dall’utente) viene annullata a livello nativo. Ciascuno segnala onError con code "ERR_ACCESS_DENIED".

{
  "capabilities": {
    "webview": { "hosts": ["example.com", "*.example.com"] }
  }
}

#Note sulle piattaforme

  • iOS carica WebKit la prima volta che viene creata una web view, quindi ha un costo per l’app solo quando ne mostra una.
  • Su macOS onDownload salva i download con WKDownload di WebKit.
  • Su Android le chiamate postMessage della pagina arrivano su un thread in background e vengono inoltrate all’app in ordine.
  • Per arrotondare gli angoli di una web view, assegnale un raggio degli angoli e overflow: "hidden" (.overflow_hidden()).