Elemente
WebView
Eine Web View zeigt eine Webseite oder von dir übergebenes HTML innerhalb der App an: WKWebView auf iOS und Mac Catalyst,
android.webkit.WebView auf Android. Da NSH mit nativen Views rendert, sitzt eine Web View wie jede andere View im Layout, in der
Reihenfolge der umgebenden Views. In Rust erzeugst du sie mit web_view(url) oder web_view_html(html, base_url), in TSX
mit <WebView> aus 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 nimmt eine Web View entweder src oder html entgegen (siehe die WebView API). Der
Leitfaden zu Web View und Video behandelt Messaging-Muster und Capabilities.
#Props
Eine Web View akzeptiert die Props einer View (sie hat keine Kinder) sowie:
| Prop | Rust | Typ | Standard | Beschreibung |
|---|---|---|---|---|
src |
web_view(url) |
string |
Die URL der anzuzeigenden Seite. | |
html |
web_view_html(html, base_url), .html(html, base_url) |
string |
Inline-HTML, wird anstelle von src angezeigt. |
|
baseUrl |
base_url von web_view_html / .html |
string |
Die Basis, gegen die relative Links in html aufgelöst werden. |
|
injectedJavaScript |
.injected_java_script(..) |
string |
Wird nach dem Laden jeder Seite ausgeführt. | |
injectedJavaScriptBeforeContentLoaded |
.injected_java_script_before_content(..) |
string |
Wird vor den eigenen Skripten jeder Seite ausgeführt. | |
userAgent |
.user_agent(..) |
string |
Der User-Agent-String des Browsers. | |
javaScriptEnabled |
.java_script_enabled(..) |
boolean |
true |
Seiten führen Skripte aus. |
scrollable |
.scrollable(..) |
boolean |
true |
Der Benutzer kann die Seite scrollen. |
ios-Schlüssel |
Rust | Typ | Beschreibung |
|---|---|---|---|
allowsBackForwardNavigationGestures |
.back_forward_gestures(..) |
boolean |
Von den Rändern wischen, um zurück und vor zu navigieren. |
desktop-Schlüssel |
Rust | Typ | Beschreibung |
|---|---|---|---|
allowsBackForwardNavigationGestures |
.back_forward_gestures(..) |
boolean |
macOS: mit zwei Fingern auf dem Trackpad wischen, um zurück und vor zu navigieren; Windows: auf einem Touchscreen wischen; Linux: auf dem Touchpad wischen. |
#Ereignisse
| Ereignis | Rust | Payload | Wann |
|---|---|---|---|
onLoadStart |
.on_load_start |
WebViewNavigation |
Eine Seite hat begonnen zu laden. |
onLoad |
.on_load |
WebViewNavigation |
Eine Seite hat das Laden abgeschlossen. |
onError |
.on_error |
{ message, code? } (&str, die Meldung) |
Das Laden ist fehlgeschlagen; code ist "ERR_ACCESS_DENIED" für eine Seite oder html für eine, die die Capabilities der App nicht gewähren. |
onNavigationStateChange |
.on_navigation_state_change |
WebViewNavigation |
Die URL, der Titel, der Ladezustand oder der Verlauf hat sich geändert. |
onMessage |
.on_message |
{ data: string } (&str) |
Die Seite hat window.nsh.postMessage(data) aufgerufen. |
onDownload |
WebViewDownload |
Ein Download wurde gestartet, abgeschlossen oder ist fehlgeschlagen (macOS). |
WebViewNavigation ist { url, title, loading, canGoBack, canGoForward }; in Rust WebNavigation mit url, title,
loading, can_go_back und can_go_forward.
WebViewDownload ist { state, id, url, filename, path, bytes?, message? }: state ist "started", "finished"
(mit dem bytes der Datei) oder "failed" (mit zugehörigem message). Mit einem onDownload-Listener wird das, was die Seite herunterlädt
(ein Anhang, ein Typ, den die Web View nicht anzeigt, ein download-Link), im Datenverzeichnis der App als
downloads/<id>/<filename> gespeichert, worauf path zeigt, und die App liest es mit fs/promises.
Unter macOS erlaubt das Hinzufügen des Listeners jeder geladenen Seite, die durch die Capability webview zugelassen ist, Downloads
automatisch zu speichern. Die App ist für ihr Speicherbudget verantwortlich und dafür, abgeschlossene Dateien aus downloads/ zu entfernen,
sobald sie sie nicht mehr benötigt. Das Entfernen der Web View bricht laufende Downloads ab und entfernt deren partielle
Verzeichnisse. Downloads werden über onDownload gemeldet; wird eine Navigation in einen Download umgewandelt, bleibt onError
tatsächlichen Navigationsfehlern vorbehalten. Aus Dateinamen werden Steuerzeichen und bidirektionale Formatierungszeichen entfernt,
und lange Namen werden auf 200 Bytes gekürzt, wobei eine passende Dateiendung erhalten bleibt.
#Ref-Methoden
| Methode | Rust | Beschreibung |
|---|---|---|
reload() |
reload() |
Lädt die Seite neu. |
goBack() / goForward() |
go_back() / go_forward() |
Navigiert im Verlauf der Seite. |
stopLoading() |
stop_loading() |
Beendet das Laden. |
injectJavaScript(script) |
inject_java_script(script) |
Führt JavaScript in der Seite aus. |
postMessage(data) |
post_message(data) |
Sendet einen String an die Seite, die ein message-Ereignis auf window erhält. |
#Messaging
Die App und die Seite tauschen Strings aus. Die Seite sendet mit window.nsh.postMessage(data); die App sendet
mit der postMessage(data) der Ref (post_message(..) in Rust). Sende JSON für strukturierte Daten.
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>
);
}#Capabilities
Wenn die App Capabilities in nsh.json deklariert, zeigt eine Web View die unter webview aufgeführten Hosts an und
navigiert dorthin. Ein src auf einem anderen Host oder html in einer App ohne webview zeigt eine
leere View; eine Top-Level-Navigation zu einem anderen Host (ein Link, den der Benutzer antippt) wird nativ abgebrochen. Jeder dieser Fälle meldet
onError mit code "ERR_ACCESS_DENIED".
{
"capabilities": {
"webview": { "hosts": ["example.com", "*.example.com"] }
}
}
#Hinweise zu Plattformen
- iOS lädt WebKit erst, wenn zum ersten Mal eine Web View erstellt wird; es kostet eine App also nur dann etwas, wenn sie eine anzeigt.
- Unter macOS speichert
onDownloadDownloads mit WebKitsWKDownload. - Unter Android treffen die
postMessage-Aufrufe der Seite in einem Hintergrund-Thread ein und werden der Reihe nach an die App weitergeleitet. - Um die Ecken einer Web View abzurunden, gib ihr einen Eckenradius und
overflow: "hidden"(.overflow_hidden()).