加载中...

基于tauri+vue3.x多开窗口|Tauri创建多窗体实践

基于tauri+vue3.x多开窗口|Tauri创建多窗体实践

最近一种在捣鼓 Tauri 集成 Vue3 技术开发桌面端应用实践,tauri 实现创建多窗口,窗口之间通讯功能。

New Image

开始正文之前,先来了解下 tauri 结合 vue3.js 快速创建项目。

tauri 在 github 上star高达53K+,而且呈快速增长趋势。相比electron构建应用更具优势。

New Image

分别用 Tauri 和 Electron 打包测试一个 todo list 程序。

Electron打包体积  69  M,Tauri打包体积才只有  7.5 M。

Tauri 构建的桌面应用体积远远比 Electron 构建的小得多。因为它放弃了体积庞大的 Chromium 内核和Nodejs,tauri前端集成了 webview,后端使用 Rust。而且 Tauri 构建应用还提供了诸多初始化程序模板,比如原生 JavaScript、Vue2/3、React、Svelte.js、SvelteKit 等。

New Image

准备工作

首先您需要安装 Rust 及其他系统依赖。

  • "C++ 生成工具" 和 Windows 10 SDK。
  • Tauri 需要 WebView2 才能在 Windows 上呈现网页内容,所以您必须先安装 WebView2。
  • Rust

具体操作,请前往 https://tauri.app/zh/v1/guides/getting-started/prerequisites 来按步骤操作。

New Image

  • 创建 tauri 初始化项目

具体的前端框架模板,大家根据实际情况选择。

 npm create tauri-app 

New Image

  • 开发/构建打包

 tauri dev  tauri build 

New Image

非常简单的几步就能快速搭建 vue3+tauri 桌面端模板。接下来就能顺利的开发了。

tauri 也提供了如下几种常用创建多窗口的方法。

  • tauri.conf.json
复制代码
{
  "tauri": {
    "windows": [
      {
        "label": "external",
        "title": "Tauri App",
        "url": "https://tauri.app"
      },
      {
        "label": "local",
        "title": "Tauri",
        "url": "home.html"
      }
    ]
  }
}
复制代码
  • src-tauri/src/main.rs
复制代码
tauri::Builder::default()
  .setup(|app| {
    let docs_window = tauri::WindowBuilder::new(
      app,
      "external", /* the unique window label */
      tauri::WindowUrl::External("https://tauri.app/".parse().unwrap())
    ).build()?;
    let local_window = tauri::WindowBuilder::new(
      app,
      "local",
      tauri::WindowUrl::App("index.html".into())
    ).build()?;
    Ok(())
})
复制代码
  • 通过前端 JS 创建窗口。
复制代码
import { WebviewWindow } from ''@tauri-apps/api/window''
const webview = new WebviewWindow(''main_win'', {
  url: ''/home'',
})

webview.once(''tauri://created'', function () {
  // webview window successfully created
})
webview.once(''tauri://error'', function (e) {
  // an error happened creating the webview window
})
复制代码

具体详细的介绍,大家可以去官网查看,文档都有非常详细的讲解。

https://tauri.app/zh/v1/guides/features/multiwindow

上面介绍的方法比较适用于一些简单的窗口,对于一些复杂多开窗口,还得封装一个窗口创建器,直接通过传入参数快速生成窗体。

复制代码
createWin({
    label: ''Home'',
    title: ''主页'',
    url: ''/home'',
    width: 800,
    height: 600,
})
复制代码

新建一个 windows 文件夹,用来封装窗口及调用窗口。

New Image

复制代码
/**
 * @desc    窗口容器
 * @author: YXY  Q:282310962
 * @time    2022.10
 */

import { WebviewWindow, appWindow, getAll, getCurrent } from ''@tauri-apps/api/window''
import { relaunch, exit } from ''@tauri-apps/api/process''
import { emit, listen } from ''@tauri-apps/api/event''

import { setWin } from ''./actions''

// 系统参数配置
export const windowConfig = {
    label: null,            // 窗口唯一label
    title: '''',              // 窗口标题
    url: '''',                // 路由地址url
    width: 900,             // 窗口宽度
    height: 640,            // 窗口高度
    minWidth: null,         // 窗口最小宽度
    minHeight: null,        // 窗口最小高度
    x: null,                // 窗口相对于屏幕左侧坐标
    y: null,                // 窗口相对于屏幕顶端坐标
    center: true,           // 窗口居中显示
    resizable: true,        // 是否支持缩放
    maximized: false,       // 最大化窗口
    decorations: false,     // 窗口是否无边框及导航条
    alwaysOnTop: false,     // 置顶窗口
}

class Windows {
    constructor() {
        this.mainWin = null
    }

    // 获取窗口
    getWin(label) {
        return WebviewWindow.getByLabel(label)
    }

    // 获取全部窗口
    getAllWin() {
        return getAll()
    }

    // 创建新窗口
    async createWin(options) {
        const args = Object.assign({}, windowConfig, options)

        // 判断窗口是否存在
        const existWin = getAll().find(w => w.label == args.label)
        if(existWin) {
            if(existWin.label.indexOf(''main'') == -1) {
                await existWin?.unminimize()
                await existWin?.setFocus()
                return
            }
            await existWin?.close()
        }

        // 创建窗口对象
        let win = new WebviewWindow(args.label, args)
        
        // 是否最大化
        if(args.maximized && args.resizable) {
            win.maximize()
        }

        // 窗口创建完毕/失败
        win.once(''tauri://created'', async() => {
            console.log(''window create success!'')
            ...
        })

        win.once(''tauri://error'', async() => {
            console.log(''window create error!'')
        })
    }

    // 开启主进程监听事件
    async listen() {
        // 创建新窗体
        await listen(''win-create'', (event) => {
            console.log(event)
            this.createWin(JSON.parse(event.payload))
        })

        // 显示窗体
        await listen(''win-show'', async(event) => {
            if(appWindow.label.indexOf(''main'') == -1) return
            await appWindow.show()
            await appWindow.unminimize()
            await appWindow.setFocus()
        })

        // 隐藏窗体
        await listen(''win-hide'', async(event) => {
            if(appWindow.label.indexOf(''main'') == -1) return
            await appWindow.hide()
        })

        // 退出应用
        await listen(''win-exit'', async(event) => {
            setWin(''logout'')
            await exit()
        })

        // 重启应用
        await listen(''win-relaunch'', async(event) => {
            await relaunch()
        })

        // 主/渲染进程传参
        await listen(''win-setdata'', async(event) => {
            await emit(''win-postdata'', JSON.parse(event.payload))
        })
    }
}

export default Windows
复制代码

actions.js进行一些调用处理。

复制代码
/**
 * 处理渲染器进程到主进程的异步通信
 */

import { WebviewWindow } from ''@tauri-apps/api/window''
import { emit } from ''@tauri-apps/api/event''

/**
 * @desc 创建新窗口
 */
export async function createWin(args) {
    await emit(''win-create'', args)
}

/**
 * @desc 获取窗口
 * @param args {string}
 */
export async function getWin(label) {
    return await WebviewWindow.getByLabel(label)
}

/**
 * @desc 设置窗口
 * @param type {string} ''show''|''hide''|''close''|''min''|''max''|''max2min''|''exit''|''relaunch''
 */
export async function setWin(type) {
    await emit(''win-'' + type)
}

/**
 * @desc 登录窗口
 */
export async function loginWin() {
    await createWin({
        label: ''Login'',
        title: ''登录'',
        url: ''/login'',
        width: 320,
        height: 420,
        resizable: false,
        alwaysOnTop: true,
    })
}

// ...
复制代码

在需要调用创建窗口的.vue页面,引入actions.js文件。

 import { loginWin, createWin } from ''@/windows/actions'' 

复制代码
const createManageWin = async() => {
    createWin({
        label: ''Manage'',
        title: ''管理页面'',
        url: ''/manage'',
        width: 600,
        height: 450,
        minWidth: 300,
        minHeight: 200
    })
}

const createAboutWin = async() => {
    createWin({
        label: ''About'',
        title: ''关于页面'',
        url: ''/about'',
        width: 500,
        height: 500,
        resizable: false,
        alwaysOnTop: true
    })
}
复制代码

一些注意点

  • 创建系统托盘图标

New Image

复制代码
use tauri::{
    AppHandle, Manager, 
    CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu
};

// 托盘菜单
pub fn menu() -> SystemTray {
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    let show = CustomMenuItem::new("show".to_string(), "Show");
    let hide = CustomMenuItem::new("hide".to_string(), "Hide");
    let change_ico = CustomMenuItem::new("change_ico".to_string(), "Change Icon");
    let tray_menu = SystemTrayMenu::new()
        .add_submenu(SystemTraySubmenu::new(
            "Language", // 语言菜单
            SystemTrayMenu::new()
                .add_item(CustomMenuItem::new("lang_english".to_string(), "English"))
                .add_item(CustomMenuItem::new("lang_zh_CN".to_string(), "简体中文"))
                .add_item(CustomMenuItem::new("lang_zh_HK".to_string(), "繁体中文")),
        ))
        .add_native_item(SystemTrayMenuItem::Separator) // 分割线
        .add_item(change_ico)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(hide)
        .add_item(show)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(quit);

    SystemTray::new().with_menu(tray_menu)
}

// 托盘事件
pub fn handler(app: &AppHandle, event: SystemTrayEvent) {
    match event {
        SystemTrayEvent::LeftClick {
            position: _,
            size: _,
            ..
        } => {
            println!("点击左键");
        }
        SystemTrayEvent::RightClick {
            position: _,
            size: _,
            ..
        } => {
            println!("点击右键");
        }
        SystemTrayEvent::DoubleClick {
            position: _,
            size: _,
            ..
        } => {
            println!("双击");
        }
        SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
            "change_ico" => { // 更新托盘图标
                app.tray_handle()
                    .set_icon(tauri::Icon::Raw(
                        include_bytes!("../icons/new.png").to_vec()
                    ))
                    .unwrap();
            }
            lang if lang.contains("lang_") => { // 选择语言,匹配 id 前缀包含 `lang_` 的事件
                Lang::new(
                    app,
                    id, // 点击菜单的 id
                    vec![
                        Lang {
                            name: "English",
                            id: "lang_english",
                        },
                        Lang {
                            name: "繁体中文",
                            id: "lang_zh_HK",
                        },
                        Lang {
                            name: "简体中文",
                            id: "lang_zh_CN",
                        },
                    ],
                );
            }
            "hide" => {
                // let window = app.get_window("main").unwrap();
                // window.show().unwrap();
                println!("点击隐藏");
            }
            "show" => {
                println!("点击显示");
            }
            "quit" => {
                println!("点击退出");
                std::process::exit(0);
            }
            _ => {}
        },
        _ => {}
    }
}

struct Lang<''a> {
    name: &''a str,
    id: &''a str,
}

impl Lang<''static> {
    fn new(app: &AppHandle, id: String, langs: Vec<Lang>) {
        // 获取点击的菜单项
        langs.iter().for_each(|lang| {
            let handle = app.tray_handle().get_item(lang.id);
            if lang.id.to_string() == id.as_str() {
                // 设置菜单名称
                handle.set_title(format!("  {}", lang.name)).unwrap();
                // 还可以使用 `set_selected`、`set_enabled` 和 `set_native_image`(仅限 macOS)
                handle.set_selected(true).unwrap();
            } else {
                handle.set_title(lang.name).unwrap();
                handle.set_selected(false).unwrap();
            }
        });
    }
}
复制代码

创建托盘图标,默认图标文件在src-tauri/icons目录下。如果想使用自定义的.ico图标,可通过tauri.cong.json文件配置。

"systemTray": {
    "iconPath": "icons/tray.ico",
    "iconAsTemplate": true,
    "menuOnLeftClick": false
}

如果setIcon报错,则需要在 src-tauri/src/Cargo.toml 中配置 icon-icoicon-png

New Image

  • tauri 配置自定义拖拽区域。

当创建窗口的时候配置了 decorations: false  则会不显示窗口边框及顶部导航栏。

此时在需要拖动元素上加一个  data-tauri-drag-region 属性,即可实现自定义区域拖动窗口功能。这个功能有些类似 electron 中自定义拖拽 -webkit-app-region: drag

New Image

不过点击窗口右键,会出现系统菜单。这样显得应用不够原生,可以简单的通过禁用右键菜单来屏蔽功能。

New Image

export function disableWinMenu() {
    document.addEventListener(''contextmenu'', e => e.preventDefault())
}
disableWinMenu()

好了,基于 tauri+vue3 构建多窗口桌面应用就分享到这里。希望对大家有丢丢帮助哈~~ 😙

最后附上一个 vue3+electron 仿macOs桌面UI系统

https://www.cnblogs.com/xiaoyan2017/p/14926338.html

New Image