Éléments
WebView
Une vue web affiche une page web, ou du HTML que vous lui transmettez, dans l’application : WKWebView sur iOS et Mac Catalyst,
android.webkit.WebView sur Android. Comme NSH effectue son rendu avec des vues natives, une vue web s’insère dans la mise en page comme n’importe quelle autre
vue, dans l’ordre par rapport aux vues voisines. En Rust, on en crée une avec web_view(url) ou web_view_html(html, base_url), en TSX
avec <WebView> depuis 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, une vue web accepte soit src, soit html (voir l’API WebView). Le
guide des vues web et vidéo présente les modèles de messagerie et les capacités.
#Props
Une vue web accepte les props d’une vue (elle n’a pas d’enfants), plus :
| Prop | Rust | Type | Défaut | Description |
|---|---|---|---|---|
src | web_view(url) | string | L’URL de la page à afficher. | |
html | web_view_html(html, base_url), .html(html, base_url) | string | HTML en ligne, affiché à la place de src. | |
baseUrl | base_url de web_view_html / .html | string | Base de résolution des liens relatifs de html. | |
injectedJavaScript | .injected_java_script(..) | string | S’exécute après le chargement de chaque page. | |
injectedJavaScriptBeforeContentLoaded | .injected_java_script_before_content(..) | string | S’exécute avant les scripts propres à chaque page. | |
userAgent | .user_agent(..) | string | La chaîne user agent du navigateur. | |
javaScriptEnabled | .java_script_enabled(..) | boolean | true | Les pages exécutent des scripts. |
scrollable | .scrollable(..) | boolean | true | L’utilisateur peut faire défiler la page. |
Clé ios | Rust | Type | Description |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | Balayer depuis les bords pour revenir en arrière et avancer. |
Clé desktop | Rust | Type | Description |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | macOS : balayer avec deux doigts sur le trackpad pour revenir en arrière et avancer ; Windows : balayer sur un écran tactile ; Linux : balayer sur le pavé tactile. |
#Événements
| Événement | Rust | Payload | Quand |
|---|---|---|---|
onLoadStart | .on_load_start | WebViewNavigation | Une page a commencé à se charger. |
onLoad | .on_load | WebViewNavigation | Une page a fini de se charger. |
onError | .on_error | { message, code? } (&str, le message) | Échec du chargement ; code vaut "ERR_ACCESS_DENIED" pour une page ou html que les capacités de l’application n’accordent pas. |
onNavigationStateChange | .on_navigation_state_change | WebViewNavigation | L’URL, le titre, l’état de chargement ou l’historique a changé. |
onMessage | .on_message | { data: string } (&str) | La page a appelé window.nsh.postMessage(data). |
onDownload | WebViewDownload | Un téléchargement a démarré, s’est terminé ou a échoué (macOS). |
WebViewNavigation est { url, title, loading, canGoBack, canGoForward } ; en Rust, WebNavigation avec url, title,
loading, can_go_back et can_go_forward.
WebViewDownload est { state, id, url, filename, path, bytes?, message? } : state vaut "started", "finished"
(avec le bytes du fichier) ou "failed" (avec son message). Avec un écouteur onDownload, ce que la page télécharge
(une pièce jointe, un type que la vue web n’affiche pas, un lien download) est enregistré dans le répertoire de données de l’application sous
downloads/<id>/<filename>, là où pointe path, et l’application le lit avec fs/promises.
Sur macOS, l’ajout de l’écouteur permet à toute page chargée autorisée par la capacité webview d’enregistrer automatiquement des téléchargements.
L’application est responsable de son budget de stockage et de la suppression des fichiers terminés de downloads/
lorsqu’elle a fini de les utiliser. La suppression de la vue web annule les téléchargements en cours et supprime leurs répertoires
partiels. Les téléchargements sont signalés via onDownload ; la conversion d’une navigation en téléchargement réserve onError
aux véritables échecs de navigation. Les caractères de contrôle et les contrôles de mise en forme bidirectionnelle sont retirés des noms de fichiers,
et les noms longs sont raccourcis à 200 octets tout en conservant une extension qui tient dans cette limite.
#Méthodes de ref
| Méthode | Rust | Description |
|---|---|---|
reload() | reload() | Recharge la page. |
goBack() / goForward() | go_back() / go_forward() | Se déplace dans l’historique de la page. |
stopLoading() | stop_loading() | Arrête le chargement. |
injectJavaScript(script) | inject_java_script(script) | Exécute du JavaScript dans la page. |
postMessage(data) | post_message(data) | Envoie une chaîne à la page, qui reçoit un événement message sur window. |
#Messagerie
L’application et la page échangent des chaînes. La page envoie avec window.nsh.postMessage(data) ; l’application envoie
avec postMessage(data) de la ref (post_message(..) en Rust). Envoyez du JSON pour des données structurées.
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>
);
}#Capacités
Lorsque l’application déclare des capacités dans nsh.json, une vue web affiche les hôtes listés sous webview et y
navigue. Un src sur un autre hôte, ou html dans une application sans webview, affiche une
vue vide ; une navigation de premier niveau vers un autre hôte (un lien sur lequel l’utilisateur appuie) est annulée de façon native. Chacun signale
onError avec code "ERR_ACCESS_DENIED".
{
"capabilities": {
"webview": { "hosts": ["example.com", "*.example.com"] }
}
}
#Notes par plateforme
- iOS charge WebKit la première fois qu’une vue web est créée ; cela ne coûte donc quelque chose à une application que lorsqu’elle en affiche une.
- Sur macOS,
onDownloadenregistre les téléchargements avecWKDownloadde WebKit. - Sur Android, les appels
postMessagede la page arrivent sur un thread d’arrière-plan et sont transmis à l’application dans l’ordre. - Pour arrondir les coins d’une vue web, donnez-lui un rayon de coin et
overflow: "hidden"(.overflow_hidden()).