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.
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())),
)
}import { signal, Text, View, WebView, type WebViewNavigation } from "nsh";
function Browser() {
const nav = signal<WebViewNavigation | null>(null);
return (
<View style={{ flex: 1 }}>
<Text style={{ numberOfLines: 1 }}>{nav()?.title ?? "Loading…"}</Text>
<WebView
src="https://example.com"
style={{ flex: 1 }}
onNavigationStateChange={(e) => nav.set(e)}
/>
</View>
);
}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")),
)
}import { signal, Text, View, WebView, type WebViewRef } from "nsh";
const page = `<!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>`;
function Bridge() {
const message = signal("");
let web!: WebViewRef;
return (
<View style={{ gap: 12 }}>
<WebView ref={web} html={page} style={{ height: 200 }} onMessage={(e) => message.set(e.data)} />
<Text onPress={() => web.postMessage("hi from the app")}>Post to the page · got: {message()}</Text>
</View>
);
}#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,
onDownloadguarda las descargas conWKDownloadde WebKit. - En Android, las llamadas
postMessagede 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()).