在 Vue3 + Electron 中使用预加载脚本(preload)

一、什么是预加载脚本(preload),为什么我们需要它

根据 Electron 官方提供的相关说明,我们可以将预加载脚本理解成是主进程和渲染进程间的桥梁。通常出于安全性的角度考虑,我们使用预加载脚本来安全地将 Node.js 模块或第三方库的 API 暴露至渲染进程中。

有时,我们可能为了在渲染进程中使用 Node.js 相关模块而关闭上下文隔离和为页面集成 Node.js 环境,但是这种方式官方并不推荐:

// 不推荐
const mainWindow = new BrowserWindow({
  webPreferences: {
    // 关闭上下文隔离
    contextIsolation: false,
    // 为页面集成 Node.js 环境
    nodeIntegration: true,
  }
})

mainWindow.loadURL('https://example.com')

官方推荐的使用预加载脚本(preload)暴露相关 API,以使用 Node.js 的模块功能:

// 推荐
const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(app.getAppPath(), 'preload.js')
  }
})

mainWindow.loadURL('https://example.com')

关于预加载脚本的更多介绍参见:https://www.electronjs.org/zh/docs/latest/tutorial/tutorial-preload
关于安全方面的更多介绍参见:https://www.electronjs.org/zh/docs/latest/tutorial/security

二、通过预加载脚本暴露相关 API 至渲染进程

实现目标:

  • 获取系统默认桌面路径功能
  • 向剪切板写入内容
  • 使用系统默认浏览器访问目标 url
  • 使用文件选择对话框

项目通过 electron-vite-vue 构建,详情可见:https://blog.csdn.net/qq_45897239/article/details/138490747

准备工作,在主进程 main.ts 文件中导入 preload 相关文件:

// electron/main.ts
function createWindow() {
    win = new BrowserWindow({
        width: 1200,
        height: 700,
        minWidth: 885,
        minHeight: 580,
        icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
        webPreferences: {
            // 导入 preload 相关文件
            preload: path.join(__dirname, "preload.mjs"),
        },
        // 隐藏菜单栏 按 Alt 键显示
        autoHideMenuBar: true,
    });
    // 程序启动后开启 开发者工具
    // win.webContents.openDevTools();

    // 关闭菜单栏
    // Menu.setApplicationMenu(null);

    if (VITE_DEV_SERVER_URL) {
        win?.loadURL(VITE_DEV_SERVER_URL);
    } else {
        win?.loadFile(path.join(RENDERER_DIST, "index.html"));
    }
}
1、实现获取系统默认桌面路径功能

preload.ts 文件中通过 contextBridge 对外暴露自定义 API。

// electron/preload.ts
import { ipcRenderer, contextBridge } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
    // 获取系统默认桌面路径
    getDesktopPath: async () => {
        try {
            return await ipcRenderer.invoke("get-desktop-path");
        } catch (error) {
            console.error("Failed to get desktop path:", error.message);
        }
    },
    ...
});

ipcRenderer.invoke 允许渲染进程向主进程发送事件或消息,并且接收主进程返回的数据。

可以直接在主进程中使用 ipcMain.handle() 监听渲染进程发送来的消息:

// electron/main.ts
import { app, ipcMain } from "electron";
...
app.whenReady().then(async () => {
    try {
        ...
        createWindow();
        // get-desktop-path => 获取系统桌面路径
        ipcMain.handle("get-desktop-path", () => {
            return app.getPath("desktop");
        });
    } catch (error) {
        console.error("Failed to start server:", error);
    }
});

调用 API 获取系统桌面路径:

<script setup lang="ts">
onMounted(async () => {
    // 获取系统桌面路径
    const res = await window.electronAPI.getDesktopPath();
    console.log(res);
});
</script>

注意: 可能会出现 ts 报错

属性“electronAPI”在类型“Window & typeof globalThis”上不存在。你是否指的是“Electron”?ts-plugin(2551)
electron.d.ts(12, 19): 在此处声明了 "Electron"

解决方案

vite-env.d.ts 中追加以下内容即可:

declare interface Window {
    electronAPI: any
}
2、向剪切板写入内容

定义相关 API:

// electron/preload.ts
import { ipcRenderer, contextBridge } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
    // 向剪切板写入内容
    clipboardWriteText: async (text) => {
        try {
            await ipcRenderer.invoke("write-to-clipboard", text);
        } catch (error) {
            console.error("Failed to write to clipboard:", error.message);
        }
    },,
    ...
});

主进程中监听:

// electron/main.ts
import { app, ipcMain, clipboard } from "electron";
...
app.whenReady().then(async () => {
    try {
        ...
        // write-to-clipboard => 向剪切板写入内容
        ipcMain.handle("write-to-clipboard", (event, text) => {
            clipboard.writeText(text);
        });
    } catch (error) {
        console.error("Failed to start server:", error);
    }
});

使用:

<script setup lang="ts">
// 向剪切板写入内容
const copyLink = (text) => {
    window.electronAPI.clipboardWriteText(text);
};
</script>
3、使用系统默认浏览器访问目标 url

定义相关 API:

// electron/preload.ts
import { ipcRenderer, contextBridge } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
    // 使用系统默认浏览器访问目标 url
    openBrowserByUrl: async (url) => {
        try {
            await ipcRenderer.invoke("open-browser-by-url", url);
        } catch (error) {
            console.error("Failed to open browser:", error.message);
        }
    },
    ...
});

主进程中监听:

// electron/main.ts
import { app, ipcMain, shell } from "electron";
...
app.whenReady().then(async () => {
    try {
        ...
        // open-browser-by-url => 使用系统默认浏览器访问目标 url
        ipcMain.handle("open-browser-by-url", async (event, url) => {
            await shell.openExternal(url);
        });
    } catch (error) {
        console.error("Failed to start server:", error);
    }
});

使用:

<script setup lang="ts">
// 使用系统默认浏览器访问目标 url
const goToLink = (url) => {
    window.electronAPI.openBrowserByUrl(url);
};
</script>
4、使用文件选择对话框

定义相关 API:

// electron/preload.ts
import { ipcRenderer, contextBridge } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
    // 打开文件保存对话框 返回文件保存路径
    openFileSaveDialog: async (path) => {
        try {
            return await ipcRenderer.invoke("open-save-dialog",path);
        } catch (error) {
            console.error("Failed to open save dialog:", error.message);
        }
    },
    ...
});

此处需要将文件对话框设置为顶层窗口,否则用户关闭应用程序后,该窗口依然存在。由于将对话框设置为顶层对话框需要win实例,所以使用函数方式导出初始化。设置为顶层对话框后,防止对话框被多次打开和未关闭对话框时的其他窗口操作。

可以在 electron 文件夹下创建一个 ipcHandlers.ts 文件,该文件内放置主进程需要监听的渲染进程发送来的消息,后续再导入 main.ts 主进程文件中。

// electron/ipcHandlers.ts
import { app, ipcMain, dialog, BrowserWindow } from "electron";

// 可以在此文件内放置需要监听的来自渲染进程的消息
// get-desktop-path => 获取系统桌面路径
ipcMain.handle("get-desktop-path", () => {
    ...
});

export function initIpcHandlerDialog(win: BrowserWindow) {
    // open-save-dialog => 打开文件保存对话框
    ipcMain.handle("open-save-dialog", async (event, path) => {
        try {
            // 打开保存文件对话框
            const result = await dialog.showOpenDialog(win, {
                // 对话框标题
                title: "选择文件保存目录",
                // 确认按钮
                buttonLabel: "选择目录",
                // 默认文件路径
                defaultPath: app.getPath("desktop"),
                // 只允许选择文件夹
                properties: ["openDirectory"],
                // 文件过滤器,定义可以选择哪些类型的文件
                filters: [{ name: "All Files", extensions: ["*"] }]
            });
            if (result) {
                if (result.canceled) {
                    console.log("用户取消了保存操作");
                    // 如果用户取消了保存操作,则返回之前的保存路径,如若为空,则使用默认值
                    if (!path) {
                        path = app.getPath("desktop");
                    }
                    return path;
                } else {
                    const filePath = result.filePaths[0];
                    console.log("用户选择了保存路径:", filePath);
                    return filePath;
                }
            }
        } catch (error) {
            console.log("文件对话框打开失败:" + error);
            // 获取桌面默认路径并返回
            return app.getPath("desktop");
        }
    });
}

在主进程中导入:

// electron/main.ts
import { app, BrowserWindow, ipcMain } from "electron";
...
// 导入相关功能模块
import "./ipcHandlers.ts";
import { initIpcHandlerDialog } from "./ipcHandlers.ts";

...

app.whenReady().then(async () => {
    try {
        ...
        createWindow();
        // 初始化对话框,将弹出对话框设置为顶层状态
        initIpcHandlerDialog(win);
    } catch (error) {
        console.error("Failed to start server:", error);
    }
});

使用:

<script setup lang="ts">
// 打开文件保存对话框
const openSaveDialog = async () => {
    const defaultPath = 'D:/Desktop'
    const path = await window.electronAPI.openFileSaveDialog(defaultPath);
};
</script>

该方法传递了一个 defaultPath 参数,用于处理当用户点击了取消文件选择对话框时,使用的默认文件保存路径。

效果展示:

效果展示

三、参考资料

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值