本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

WebView

Web view 會在 app 內顯示網頁或你傳入的 HTML:在 iOS 與 Mac Catalyst 上使用 WKWebView,在 Android 上使用 android.webkit.WebView。由於 NSH 以原生 view 進行渲染,web view 在版面配置中與其他 view 並無差異,會依順序排列在周圍的 view 之間。在 Rust 中以 web_view(url) 或 web_view_html(html, base_url) 建立,在 TSX 中則使用 nsh 的 <WebView>。

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 中,web view 接受 src 與 html 其中之一(請參閱 WebView API)。web view 與影片指南涵蓋了訊息傳遞模式與 capabilities。

#Props

Web view 接受 view 的 props(它沒有子元素),另外還有:

PropRust型別預設值說明
srcweb_view(url)string要顯示的頁面 URL。
htmlweb_view_html(html, base_url)、.html(html, base_url)string內嵌 HTML,會取代 src 顯示。
baseUrlbase_url(包含 web_view_html / .html)stringhtml 中相對連結的解析基準。
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使用者是否可以捲動頁面。
ios 鍵Rust型別說明
allowsBackForwardNavigationGestures.back_forward_gestures(..)boolean從邊緣滑動以返回與前進。
desktop 鍵Rust型別說明
allowsBackForwardNavigationGestures.back_forward_gestures(..)booleanmacOS:在觸控板上以雙指滑動以返回與前進;Windows:在觸控螢幕上滑動;Linux:在觸控板上滑動。

#事件

事件RustPayload時機
onLoadStart.on_load_startWebViewNavigation頁面開始載入。
onLoad.on_loadWebViewNavigation頁面載入完成。
onError.on_error{ message, code? }(&str,即訊息)載入失敗;對於頁面,code 為 "ERR_ACCESS_DENIED",對於 app 的 capabilities 未授予的內容,則為 html。
onNavigationStateChange.on_navigation_state_changeWebViewNavigationURL、標題、載入狀態或歷史記錄已變更。
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 監聽器後,頁面所下載的內容(附件、web view 不顯示的類型,或 download 連結)會儲存在 app 的資料目錄中,路徑為 downloads/<id>/<filename>,即 path 所指向的位置,app 可透過 fs/promises 讀取。

在 macOS 上,加入該監聽器後,任何經 webview capability 允許而載入的頁面都能自動儲存下載內容。app 須自行負責其儲存空間預算,並在使用完畢後從 downloads/ 移除已完成的檔案。移除 web view 會取消進行中的下載,並移除其未完成的暫存目錄。下載透過 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)向頁面傳送字串,頁面會在 window 上收到 message 事件。

#訊息傳遞

app 與頁面之間以字串互相交換資料。頁面使用 window.nsh.postMessage(data) 傳送;app 則使用 ref 的 postMessage(data)(Rust 中為 post_message(..))傳送。若要傳遞結構化資料,請傳送 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")),
        )
}

#Capabilities

當 app 在 nsh.json 中宣告 capabilities 時,web view 只會顯示並導覽至 webview 下列出的主機。對其他主機發出的 src,或在沒有 webview 的 app 中使用 html,都會顯示空白的 view;對其他主機的頂層導覽(例如使用者點選的連結)會在原生層被取消。以上每種情況都會回報 onError,並附帶 code "ERR_ACCESS_DENIED"。

{
  "capabilities": {
    "webview": { "hosts": ["example.com", "*.example.com"] }
  }
}

#平台注意事項

  • iOS 會在第一次建立 web view 時才載入 WebKit,因此 app 只有在顯示 web view 時才會付出相關成本。
  • 在 macOS 上,onDownload 使用 WebKit 的 WKDownload 儲存下載內容。
  • 在 Android 上,頁面的 postMessage 呼叫會在背景執行緒上送達,並依序轉送至 app。
  • 若要讓 web view 的邊角變圓,請為其設定圓角半徑與 overflow: "hidden"(.overflow_hidden())。