接入原生壳生命周期事件

新增 app.lifecycle HostBridge 能力与 H5 订阅入口

Expo 壳通过 React Native AppState 注入真实前后台状态

Tauri 壳通过主窗口 focus 和 blur 注入真实激活状态

更新壳能力漂移检查、测试和架构文档
This commit is contained in:
2026-06-18 02:16:47 +08:00
parent 45eec17007
commit 346368f0e7
16 changed files with 299 additions and 6 deletions

View File

@@ -134,6 +134,7 @@ const requiredBuildCommands = ['host_bridge_request'];
const requiredMainSnippets = [
'tauri_plugin_clipboard_manager::init()',
'"appearance.getColorScheme"',
'"app.lifecycle"',
'"share.open"',
'"share.setTarget"',
'"navigation.openNativePage"',
@@ -149,6 +150,8 @@ const requiredMainSnippets = [
'set_title',
'set_badge_count',
'window.theme()',
'WindowEvent::Focused',
'host_bridge_event_script',
];
for (const permission of requiredPermissions) {

View File

@@ -7,6 +7,8 @@ use std::sync::Mutex;
use tauri::Manager;
use tauri::Theme;
use tauri::Url;
use tauri::WebviewWindow;
use tauri::WindowEvent;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
@@ -81,6 +83,7 @@ fn capabilities() -> Vec<&'static str> {
vec![
"host.getRuntime",
"appearance.getColorScheme",
"app.lifecycle",
"share.open",
"share.setTarget",
"navigation.openNativePage",
@@ -403,6 +406,55 @@ fn write_export_bytes_file(path: PathBuf, bytes: Vec<u8>) -> Result<usize, Strin
Ok(byte_count)
}
fn host_bridge_event_script(event: &str, payload: Value) -> Result<String, serde_json::Error> {
let message = json!({
"bridge": HOST_BRIDGE_PROTOCOL,
"version": HOST_BRIDGE_VERSION,
"event": event,
"payload": payload,
});
let data = serde_json::to_string(&message)?;
let data_literal = serde_json::to_string(&data)?;
Ok(format!(
"window.dispatchEvent(new MessageEvent('message', {{ data: {} }})); true;",
data_literal
))
}
fn emit_desktop_lifecycle_event(
window: &WebviewWindow,
state: &'static str,
focused: bool,
native_state: &'static str,
) -> tauri::Result<()> {
let script = host_bridge_event_script(
"app.lifecycle",
json!({
"state": state,
"focused": focused,
"nativeState": native_state,
}),
)
.map_err(tauri::Error::Json)?;
window.eval(script)
}
fn register_desktop_lifecycle_events(window: &WebviewWindow) {
let lifecycle_window = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::Focused(focused) = event {
let (state, native_state) = if *focused {
("active", "focused")
} else {
("inactive", "blurred")
};
let _ = emit_desktop_lifecycle_event(&lifecycle_window, state, *focused, native_state);
}
});
}
fn payload_string<'a>(value: &'a Value, field: &str) -> Option<&'a str> {
value
.get(field)
@@ -727,7 +779,10 @@ fn main() {
.setup(|app| {
let window_config = app.config().app.windows.get(0).cloned();
if let Some(config) = window_config {
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
let window =
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
register_desktop_lifecycle_events(&window);
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
}
Ok(())
})
@@ -763,6 +818,10 @@ mod tests {
.as_array()
.unwrap()
.contains(&json!("appearance.getColorScheme")));
assert!(result["capabilities"]
.as_array()
.unwrap()
.contains(&json!("app.lifecycle")));
assert!(result["capabilities"]
.as_array()
.unwrap()
@@ -833,6 +892,25 @@ mod tests {
assert_eq!(color_scheme_from_theme(Theme::Dark), "dark");
}
#[test]
fn host_bridge_event_script_dispatches_lifecycle_message() {
let script = host_bridge_event_script(
"app.lifecycle",
json!({
"state": "active",
"focused": true,
"nativeState": "focused",
}),
)
.expect("event script");
assert!(script.contains("MessageEvent('message'"));
assert!(script.contains("GenarrativeHostBridge"));
assert!(script.contains("app.lifecycle"));
assert!(script.contains("\\\"state\\\":\\\"active\\\""));
assert!(script.contains("\\\"focused\\\":true"));
}
#[test]
fn external_url_normalization_allows_only_safe_protocols() {
assert_eq!(

View File

@@ -6,7 +6,7 @@
"build": {
"beforeDevCommand": "npm --prefix ../.. run dev:web",
"beforeBuildCommand": "npm --prefix ../.. run build:raw && npm run typecheck",
"devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,clipboard.writeText,file.exportText,file.exportImage",
"devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,clipboard.writeText,file.exportText,file.exportImage",
"frontendDist": "../../../dist"
},
"app": {
@@ -14,7 +14,7 @@
{
"create": false,
"label": "main",
"url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,clipboard.writeText,file.exportText,file.exportImage",
"url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,clipboard.writeText,file.exportText,file.exportImage",
"title": "Genarrative",
"width": 1280,
"height": 820,