تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

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.

el-web-view.tsx
el-web-view.tsxLive
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())),
        )
}

في TSX يقبل WebView واحدة من الخاصيتين src وhtml (راجع WebView API). ويتناول دليل WebView والفيديو أنماط تبادل الرسائل والإمكانات.

#الخصائص

يقبل WebView خصائص العرض (وليس له عناصر فرعية) بالإضافة إلى ما يلي:

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
srcweb_view(url)stringعنوان URL للصفحة المراد عرضها.
htmlweb_view_html(html, base_url)، .html(html, base_url)stringHTML مضمّن، يُعرض بدلاً من src.
baseUrlbase_url من web_view_html / .htmlstringالأساس الذي تُحلّ الروابط النسبية في html بالنسبة إليه.
injectedJavaScript.injected_java_script(..)stringيُنفَّذ بعد تحميل كل صفحة.
injectedJavaScriptBeforeContentLoaded.injected_java_script_before_content(..)stringيُنفَّذ قبل سكربتات كل صفحة الخاصة.
userAgent.user_agent(..)stringسلسلة وكيل المستخدم (user agent) الخاصة بالمتصفح.
javaScriptEnabled.java_script_enabled(..)booleantrueتشغّل الصفحات السكربتات.
scrollable.scrollable(..)booleantrueيمكن للمستخدم تمرير الصفحة.
المفتاح iosRustالنوعالوصف
allowsBackForwardNavigationGestures.back_forward_gestures(..)booleanاسحب من الحواف للرجوع والتقدم.
المفتاح desktopRustالنوعالوصف
allowsBackForwardNavigationGestures.back_forward_gestures(..)booleanmacOS: اسحب بإصبعين على لوحة التتبع للرجوع والتقدم؛ Windows: اسحب على شاشة اللمس؛ Linux: اسحب على لوحة اللمس.

#الأحداث

الحدثRustالبيانات المرافقةمتى يقع
onLoadStart.on_load_startWebViewNavigationبدأ تحميل صفحة.
onLoad.on_loadWebViewNavigationانتهى تحميل صفحة.
onError.on_error{ message, code? } (&str، الرسالة)فشل التحميل؛ تكون قيمة code هي "ERR_ACCESS_DENIED" لصفحة أو html لا تمنحها إمكانات التطبيق.
onNavigationStateChange.on_navigation_state_changeWebViewNavigationتغيّر عنوان URL أو العنوان أو حالة التحميل أو السجل.
onMessage.on_message{ data: string } (&str)استدعت الصفحة window.nsh.postMessage(data).
onDownloadWebViewDownloadبدأ تنزيل أو انتهى أو فشل (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")),
        )
}

#الإمكانات

عندما يصرّح التطبيق عن الإمكانات في 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()).