目录
前言
前面介绍插件plugin时,使用了opener插件,插件在前端是通过invoke函数通信,这篇就来看看opener的后端的通信函数是怎么写的和一些其他比较东西
正文
open_url
代码如下
#[tauri::command]
pub async fn open_url<R: Runtime>(
app: AppHandle<R>,
command_scope: CommandScope<crate::scope::Entry>,
global_scope: GlobalScope<crate::scope::Entry>,
url: String,
with: Option<String>,
) -> crate::Result<()> {
let scope = Scope::new(
&app,
command_scope
.allows()
.iter()
.chain(global_scope.allows())
.collect(),
command_scope
.denies()
.iter()
.chain(global_scope.denies())
.collect(),
);
if scope.is_url_allowed(&url, with.as_deref()) {
app.opener().open_url(url, with)
} else {
Err(Error::ForbiddenUrl { url, with })
}
}
首先,这是一个异步函数,返回Result
你不能简单地在异步函数的签名中包含借用的参数。一些常见的类型示例包括 &str 和 State<'_, Data>,解决方案
选项 1:转换类型,例如将
&str
转换为类似的非借用类型,选项 2:将返回类型包装在 Result 中
——tauri官网
有五个参数,三个参数AppHandle,CommandScope,GlobalScope都是从Tauri后端获得的
都实现trait CommandArg,能在通信函数中直接获得
impl<'de, R: Runtime> CommandArg<'de, R> for AppHandle<R>
impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T>
impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope<T>
url是String类型,with是Option<String>类型
Scope结构体
看看Scope结构体
#[derive(Debug)]
pub struct Scope<'a, R: Runtime, M: Manager<R>> {
allowed: Vec<&'a Arc<Entry>>,
denied: Vec<&'a Arc<Entry>>,
manager: &'a M,
_marker: PhantomData<R>,
}
实现了trait Debug,就可以打印了
allowed,denied,很明显,用于权限管理的
manager,显而易见是Manager这个比较重要的结构体
_marker,是PhantomData ,PhantomData 是 Rust 标准库中的一个特殊类型,用于在编译时提供类型信息,但不会在运行时占用内存,不重要
打印结果
在open_url通信函数中添加打印
println!("{:#?}",scope);
重新编译,运行,打印了许多东西,看看几个有趣的东西
plugins: Mutex {
data: PluginStore {
plugins: [
"opener",
"path",
"event",
"window",
"webview",
"app",
"resources",
"image",
"menu",
"tray",
"__TAURI_CHANNEL__",
],
},
poisoned: false,
..
},
原来有这些插件,笔者以前还不知道。
以后写plugin:<plugin_name>|<function_name> 就知道插件的名字了。
看看allowed和denied
allowed: [
Url {
url: Pattern {
original: "mailto:*",
...
},
app: Default,
},
Url {
url: Pattern {
original: "tel:*",
...
},
app: Default,
},
Url {
url: Pattern {
original: "http://*",
...
},
app: Default,
},
Url {
url: Pattern {
original: "https://*",
...
},
app: Default,
},
],
denied: [],
允许mailto:*、tel:*、http://*、"https://*这些。
没有denied
后续操作
if scope.is_url_allowed(&url, with.as_deref()) {
app.opener().open_url(url, with)
} else {
Err(Error::ForbiddenUrl { url, with })
}
满足url和with,就使用open_url方法了,成功就返回Result<()>
pub fn open_url(&self, url: impl Into<String>,
with: Option<impl Into<String>>)
-> Result<()>
失败返回报错信息
Err(Error::ForbiddenUrl { url, with })
即
#[error("Not allowed to open url {}{}",
.url,
.with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())]
ForbiddenUrl { url: String, with: Option<String> },
看看is_url_allowed函数
pub fn is_url_allowed(&self, url: &str, with: Option<&str>) -> bool {
let denied = self.denied.iter().any(|e| e.matches_url(url, with));
if denied {
false
} else {
self.allowed.iter().any(|e| e.matches_url(url, with))
}
}
先使用denied对url进行遍历匹配,如果有url在denied中返回false
没有,则在匹配allowed,成功返回true。
看看Entry
#[derive(Debug)]
pub enum Entry {
Url {
url: glob::Pattern,
app: Application,
},
Path {
path: Option<PathBuf>,
app: Application,
},
}
是个enum,有两个字段Url和Path,两个变体。
对于另外两个通信函数open_path和reveal_item_in_dir,是类似的,不必细说。
build方法
看看插件中的build方法,先看init方法
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::default().build()
}
Builder是结构体,是The opener plugin Builder
pub struct Builder {
open_js_links_on_click: bool,
}
调用default方法,然后调用build,build代码如下
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
let mut builder = tauri::plugin::Builder::new("opener")
.setup(|app, _api| {
#[cfg(target_os = "android")]
let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?;
#[cfg(target_os = "ios")]
let handle = _api.register_ios_plugin(init_plugin_opener)?;
app.manage(Opener {
#[cfg(not(mobile))]
_marker: std::marker::PhantomData::<fn() -> R>,
#[cfg(mobile)]
mobile_plugin_handle: handle,
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::open_url,
commands::open_path,
commands::reveal_item_in_dir
]);
if self.open_js_links_on_click {
builder = builder.js_init_script(include_str!("init-iife.js").to_string());
}
builder.build()
}
使用的是tauri::plugin::Builder,看看定义
pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<String>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_window_ready: Box<OnWindowReady<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
on_drop: Option<Box<OnDrop<R>>>,
uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
}
有name、setup、js_init_script、invoke_handler等的。
首先调用new方法,初始化,把插件的名字传进去——opener
然后使用setup函数,处理在移动端的插件,注册Opener状态。
接着注册通信函数
再接着初始化一个js文件
最后Builder结构体的build方法
大体上可以认为,注册了一个State,注册了三个通信函数,初始化了一个js脚本
js文件的内容就不展示了
看看tauri::plugin::Builder中的on_event
on_event: Box<OnEvent<R>>,
Box中放OnEvent
type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
OnEvent首先是个别名,
其次是动态 trait 对象类型
是个可变的、可跨线程发送的闭包或函数
定义的挺复杂
看看RunEvent
#[derive(Debug)]
#[non_exhaustive]
pub enum RunEvent {
Exit,
ExitRequested {
code: Option<i32>,
api: ExitRequestApi,
},
WindowEvent {
label: String,
event: WindowEvent,
},
WebviewEvent {
label: String,
event: WebviewEvent,
},
Ready,
Resumed,
MainEventsCleared,
#[cfg(any(target_os = "macos", target_os = "ios"))]
Opened {
urls: Vec<url::Url>,
},
#[cfg(desktop)]
MenuEvent(crate::menu::MenuEvent),
#[cfg(all(desktop, feature = "tray-icon"))]
TrayIconEvent(crate::tray::TrayIconEvent),
#[cfg(target_os = "macos")]
Reopen {
has_visible_windows: bool,
},
}
全是事件,既有window,又有webview等的,看来可以获取所有事件。
ShellExecuteExW函数
无论是open_url还是open_path,都是需要调用某个二进制文件,比如msedge.exe。
笔者是Windows系统,自然是调用Windows API
那么需要调用Windows API的关键函数如下
pub(crate) fn open<P: AsRef<OsStr>, S: AsRef<str>>(path: P, with: Option<S>) -> crate::Result<()> {
match with {
Some(program) => ::open::with_detached(path, program.as_ref()),
None => ::open::that_detached(path),
}
.map_err(Into::into)
}
可以发现插件opener其中一个依赖如下
[dependencies.open]
version = "5"
features = ["shellexecute-on-windows"]
::open::with_detached就是open依赖中的函数了
一直往里面走,可以发现下面这个函数
#[cfg(feature = "shellexecute-on-windows")]
pub fn with_detached<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> std::io::Result<()> {
let app = wide(app.into());
let path = wide(path);
let mut info = ffi::SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<ffi::SHELLEXECUTEINFOW>() as _,
nShow: ffi::SW_SHOWNORMAL,
lpFile: app.as_ptr(),
lpParameters: path.as_ptr(),
..unsafe { std::mem::zeroed() }
};
unsafe { ShellExecuteExW(&mut info) }
}
fn wide<T: AsRef<OsStr>>(input: T) -> Vec<u16>
SHELLEXECUTEINFOW是一个结构体
作为ShellExecuteExW函数的的参数传进去。
SHELLEXECUTEINFOW (shellapi.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfowShellExecuteExW function (shellapi.h) - Win32 apps | Microsoft Learn
https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecuteexw在Windows操作系统中,这个ShellExecuteExW可以说是open_url和open_path能够成功实现最关键的部分。
对于reveal_item_in_dir函数,也是如此,大差不差。
先尝试使用SHOpenFolderAndSelectItems显示,判断是否成功,如果失败,再使用ShellExecuteExW
关键代码如下
unsafe {
if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) {
if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
let is_dir = file.is_dir();
let mut info = SHELLEXECUTEINFOW {
......
};
ShellExecuteExW(&mut info).inspect_err(|_| {
ILFree(Some(dir_item));
ILFree(Some(file_item));
})?;
}
}
}
总结
opener这个插件,既实现了前后端的通信,还通过open依赖实现了与操作系统的通信。