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.
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>
);
}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ù:
| Prop | Rust | Tipo | Predefinito | Descrizione |
|---|---|---|---|---|
src | web_view(url) | string | L’URL della pagina da mostrare. | |
html | web_view_html(html, base_url), .html(html, base_url) | string | HTML inline, mostrato al posto di src. | |
baseUrl | base_url di web_view_html / .html | string | Il riferimento rispetto al quale vengono risolti i link relativi di html. | |
injectedJavaScript | .injected_java_script(..) | string | Viene eseguito dopo il caricamento di ogni pagina. | |
injectedJavaScriptBeforeContentLoaded | .injected_java_script_before_content(..) | string | Viene eseguito prima degli script propri di ogni pagina. | |
userAgent | .user_agent(..) | string | La stringa user agent del browser. | |
javaScriptEnabled | .java_script_enabled(..) | boolean | true | Le pagine eseguono script. |
scrollable | .scrollable(..) | boolean | true | L’utente può scorrere la pagina. |
Chiave ios | Rust | Tipo | Descrizione |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | Scorrimento dai bordi per tornare indietro e avanti. |
Chiave desktop | Rust | Tipo | Descrizione |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | macOS: scorrimento con due dita sul trackpad per tornare indietro e avanti; Windows: scorrimento su touchscreen; Linux: scorrimento sul touchpad. |
#Eventi
| Evento | Rust | Payload | Quando |
|---|---|---|---|
onLoadStart | .on_load_start | WebViewNavigation | Una pagina ha iniziato a caricarsi. |
onLoad | .on_load | WebViewNavigation | Una 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_change | WebViewNavigation | L’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). |
onDownload | WebViewDownload | Un 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
| Metodo | Rust | Descrizione |
|---|---|---|
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")),
)
}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>
);
}#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
onDownloadsalva i download conWKDownloaddi WebKit. - Su Android le chiamate
postMessagedella 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()).