【Tauri2】024——plugin(二)——opener

目录

前言

正文

open_url

Scope结构体

打印结果

后续操作

看看is_url_allowed函数

看看Entry

build方法

ShellExecuteExW函数

 总结


前言

前面介绍插件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 Learnhttps://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));
                    })?;
                }
            }
        }

SHOpenFolderAndSelectItems 函数 (shlobj_core.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems

总结

opener这个插件,既实现了前后端的通信,还通过open依赖实现了与操作系统的通信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值