Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

WebView

Una vista web muestra una página web, o HTML que le pasas, dentro de la app: WKWebView en iOS y Mac Catalyst, android.webkit.WebView en Android. Como NSH renderiza con vistas nativas, una vista web se coloca en el layout como cualquier otra vista, en orden con las vistas que la rodean. Rust crea una con web_view(url) o web_view_html(html, base_url), y TSX con <WebView> de 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())),
        )
}

En TSX, una vista web acepta uno de src y html (consulta la API de WebView). La guía de vista web y vídeo cubre los patrones de mensajería y las capacidades.

#Props

Una vista web acepta las props de una vista (no tiene hijos) además de:

Prop Rust Tipo Valor predeterminado Descripción
src web_view(url) string La URL de la página que se muestra.
html web_view_html(html, base_url), .html(html, base_url) string HTML en línea, que se muestra en lugar de src.
baseUrl base_url de web_view_html / .html string Base contra la que se resuelven los enlaces relativos de html.
injectedJavaScript .injected_java_script(..) string Se ejecuta después de cargar cada página.
injectedJavaScriptBeforeContentLoaded .injected_java_script_before_content(..) string Se ejecuta antes de los scripts propios de cada página.
userAgent .user_agent(..) string La cadena de user agent del navegador.
javaScriptEnabled .java_script_enabled(..) boolean true Las páginas ejecutan scripts.
scrollable .scrollable(..) boolean true El usuario puede desplazar la página.
Clave de ios Rust Tipo Descripción
allowsBackForwardNavigationGestures .back_forward_gestures(..) boolean Desliza desde los bordes para retroceder y avanzar.
Clave de desktop Rust Tipo Descripción
allowsBackForwardNavigationGestures .back_forward_gestures(..) boolean macOS: desliza con dos dedos en el trackpad para retroceder y avanzar; Windows: desliza en una pantalla táctil; Linux: desliza en el touchpad.

#Eventos

Evento Rust Carga útil Cuándo
onLoadStart .on_load_start WebViewNavigation Una página empezó a cargarse.
onLoad .on_load WebViewNavigation Una página terminó de cargarse.
onError .on_error { message, code? } (&str, el mensaje) Falló la carga; code es "ERR_ACCESS_DENIED" para una página o html que las capacidades de la app no conceden.
onNavigationStateChange .on_navigation_state_change WebViewNavigation Cambió la URL, el título, el estado de carga o el historial.
onMessage .on_message { data: string } (&str) La página llamó a window.nsh.postMessage(data).
onDownload WebViewDownload Una descarga comenzó, terminó o falló (macOS).

WebViewNavigation es { url, title, loading, canGoBack, canGoForward }; en Rust, WebNavigation con url, title, loading, can_go_back y can_go_forward.

WebViewDownload es { state, id, url, filename, path, bytes?, message? }: state es "started", "finished" (con el bytes del archivo) o "failed" (con su message). Con un listener de onDownload, lo que la página descarga (un adjunto, un tipo que la vista web no muestra, un enlace download) se guarda en el directorio de datos de la app como downloads/<id>/<filename>, adonde apunta path, y la app lo lee con fs/promises.

En macOS, al añadir el listener, cualquier página cargada que permita la capacidad webview puede guardar descargas automáticamente. La app es responsable de su presupuesto de almacenamiento y de eliminar los archivos completados de downloads/ cuando termine de usarlos. Al eliminar la vista web se cancelan las descargas en curso y se eliminan sus directorios parciales. Las descargas se notifican mediante onDownload; convertir una navegación en una descarga reserva onError para los fallos de navegación reales. A los nombres de archivo se les eliminan los caracteres de control y los controles de formato bidireccional, y los nombres largos se acortan a 200 bytes conservando una extensión que quepa.

#Métodos de ref

Método Rust Descripción
reload() reload() Recarga la página.
goBack() / goForward() go_back() / go_forward() Se mueve por el historial de la página.
stopLoading() stop_loading() Detiene la carga.
injectJavaScript(script) inject_java_script(script) Ejecuta JavaScript en la página.
postMessage(data) post_message(data) Envía una cadena a la página, que recibe un evento message en window.

#Mensajería

La app y la página intercambian cadenas. La página publica con window.nsh.postMessage(data); la app publica con el postMessage(data) del ref (post_message(..) en Rust). Envía JSON para datos estructurados.

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")),
        )
}

#Capacidades

Cuando la app declara capacidades en nsh.json, una vista web muestra y navega a los hosts indicados en webview. Un src en otro host, o html en una app sin webview, muestra una vista vacía; una navegación de nivel superior a otro host (un enlace que toca el usuario) se cancela de forma nativa. Cada caso informa de onError con code "ERR_ACCESS_DENIED".

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

#Notas por plataforma

  • iOS carga WebKit la primera vez que se crea una vista web, por lo que solo supone un coste para la app cuando muestra una.
  • En macOS, onDownload guarda las descargas con WKDownload de WebKit.
  • En Android, las llamadas postMessage de la página llegan en un hilo en segundo plano y se reenvían a la app en orden.
  • Para redondear las esquinas de una vista web, dale un radio de esquina y overflow: "hidden" (.overflow_hidden()).