العناصر
WebView
يعرض WebView صفحة ويب، أو HTML تمرّره أنت، داخل التطبيق: WKWebView على iOS وMac Catalyst،
وandroid.webkit.WebView على Android. ولأن NSH يعرض الواجهة باستخدام عناصر عرض أصلية، فإن WebView يوضع في التخطيط مثل أي عنصر عرض آخر،
بالترتيب نفسه مع العناصر المحيطة به. في Rust يمكنك إنشاؤه باستخدام web_view(url) أو web_view_html(html, base_url)، وفي TSX
باستخدام <WebView> من 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>
);
}في TSX يقبل WebView واحدة من الخاصيتين src وhtml (راجع WebView API). ويتناول
دليل WebView والفيديو أنماط تبادل الرسائل والإمكانات.
#الخصائص
يقبل WebView خصائص العرض (وليس له عناصر فرعية) بالإضافة إلى ما يلي:
| الخاصية | Rust | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|---|
src | web_view(url) | string | عنوان URL للصفحة المراد عرضها. | |
html | web_view_html(html, base_url)، .html(html, base_url) | string | HTML مضمّن، يُعرض بدلاً من src. | |
baseUrl | base_url من web_view_html / .html | string | الأساس الذي تُحلّ الروابط النسبية في html بالنسبة إليه. | |
injectedJavaScript | .injected_java_script(..) | string | يُنفَّذ بعد تحميل كل صفحة. | |
injectedJavaScriptBeforeContentLoaded | .injected_java_script_before_content(..) | string | يُنفَّذ قبل سكربتات كل صفحة الخاصة. | |
userAgent | .user_agent(..) | string | سلسلة وكيل المستخدم (user agent) الخاصة بالمتصفح. | |
javaScriptEnabled | .java_script_enabled(..) | boolean | true | تشغّل الصفحات السكربتات. |
scrollable | .scrollable(..) | boolean | true | يمكن للمستخدم تمرير الصفحة. |
المفتاح ios | Rust | النوع | الوصف |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | اسحب من الحواف للرجوع والتقدم. |
المفتاح desktop | Rust | النوع | الوصف |
|---|---|---|---|
allowsBackForwardNavigationGestures | .back_forward_gestures(..) | boolean | macOS: اسحب بإصبعين على لوحة التتبع للرجوع والتقدم؛ Windows: اسحب على شاشة اللمس؛ Linux: اسحب على لوحة اللمس. |
#الأحداث
| الحدث | Rust | البيانات المرافقة | متى يقع |
|---|---|---|---|
onLoadStart | .on_load_start | WebViewNavigation | بدأ تحميل صفحة. |
onLoad | .on_load | WebViewNavigation | انتهى تحميل صفحة. |
onError | .on_error | { message, code? } (&str، الرسالة) | فشل التحميل؛ تكون قيمة code هي "ERR_ACCESS_DENIED" لصفحة أو html لا تمنحها إمكانات التطبيق. |
onNavigationStateChange | .on_navigation_state_change | WebViewNavigation | تغيّر عنوان URL أو العنوان أو حالة التحميل أو السجل. |
onMessage | .on_message | { data: string } (&str) | استدعت الصفحة window.nsh.postMessage(data). |
onDownload | WebViewDownload | بدأ تنزيل أو انتهى أو فشل (macOS). |
WebViewNavigation هو { url, title, loading, canGoBack, canGoForward }؛ وفي Rust WebNavigation مع url وtitle
وloading وcan_go_back وcan_go_forward.
WebViewDownload هو { state, id, url, filename, path, bytes?, message? }: state هو "started" أو "finished"
(مع bytes الخاص بالملف) أو "failed" (مع message الخاص به). عند وجود مستمع onDownload، فإن ما تنزّله الصفحة
(مرفق، أو نوع لا يعرضه WebView، أو رابط download) يُحفظ في دليل بيانات التطبيق بالاسم
downloads/<id>/<filename>، حيث يشير path، ويقرؤه التطبيق باستخدام fs/promises.
على macOS، تتيح إضافة المستمع لأي صفحة محمّلة تسمح بها إمكانية webview أن تحفظ التنزيلات
تلقائيًا. ويتحمّل التطبيق مسؤولية ميزانية التخزين الخاصة به وحذف الملفات المكتملة من downloads/
عند انتهائه من استخدامها. وتؤدي إزالة WebView إلى إلغاء التنزيلات الجارية وحذف
أدلتها الجزئية. وتُبلَّغ التنزيلات عبر onDownload؛ أما تحويل عملية تنقّل إلى تنزيل فيُبقي على onError
لحالات فشل التنقّل الفعلية. وتُزال من أسماء الملفات محارف التحكم ومحارف التنسيق ثنائية الاتجاه،
وتُقصَّر الأسماء الطويلة إلى 200 بايت مع الحفاظ على الامتداد إن كان يتسع.
#دوال المرجع (Ref)
| الدالة | Rust | الوصف |
|---|---|---|
reload() | reload() | تعيد تحميل الصفحة. |
goBack() / goForward() | go_back() / go_forward() | تتنقل في سجل الصفحة. |
stopLoading() | stop_loading() | توقف التحميل. |
injectJavaScript(script) | inject_java_script(script) | تنفّذ JavaScript في الصفحة. |
postMessage(data) | post_message(data) | ترسل سلسلة نصية إلى الصفحة، فتستقبل حدث message على window. |
#تبادل الرسائل
يتبادل التطبيق والصفحة سلاسل نصية. تُرسل الصفحة باستخدام window.nsh.postMessage(data)، ويُرسل التطبيق
باستخدام postMessage(data) الخاصة بالمرجع (post_message(..) في Rust). أرسل JSON للبيانات المنظَّمة.
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>
);
}#الإمكانات
عندما يصرّح التطبيق عن الإمكانات في nsh.json، فإن WebView يعرض المضيفين المدرجين ضمن webview
ويتنقّل إليهم. أما src على مضيف آخر، أو html في تطبيق بلا webview، فيُظهر
عرضًا فارغًا؛ ويُلغى أصلاً التنقّل من المستوى الأعلى إلى مضيف آخر (رابط ينقر عليه المستخدم). ويبلّغ كلٌّ منها
عن onError مع code "ERR_ACCESS_DENIED".
{
"capabilities": {
"webview": { "hosts": ["example.com", "*.example.com"] }
}
}
#ملاحظات حول المنصات
- يحمّل iOS مكتبة WebKit عند إنشاء أول WebView، لذا لا يتكلّف التطبيق شيئًا إلا عندما يعرض واحدًا.
- على macOS يحفظ
onDownloadالتنزيلات باستخدامWKDownloadالخاص بـ WebKit. - على Android تصل استدعاءات
postMessageمن الصفحة على خيط في الخلفية وتُمرَّر إلى التطبيق بالترتيب. - لتدوير زوايا WebView، امنحه نصف قطر زاوية و
overflow: "hidden"(.overflow_hidden()).