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

文章目录

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

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

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

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

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

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

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 相关文件:

js 复制代码
// 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。

js 复制代码
// 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() 监听渲染进程发送来的消息:

js 复制代码
// 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 获取系统桌面路径:

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

注意: 可能会出现 ts 报错

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

解决方案

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

js 复制代码
declare interface Window {
    electronAPI: any
}
2、向剪切板写入内容

定义相关 API:

js 复制代码
// 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);
        }
    },,
    ...
});

主进程中监听:

js 复制代码
// 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);
    }
});

使用:

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

定义相关 API:

js 复制代码
// 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);
        }
    },
    ...
});

主进程中监听:

js 复制代码
// 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);
    }
});

使用:

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

定义相关 API:

js 复制代码
// 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 主进程文件中。

js 复制代码
// 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");
        }
    });
}

在主进程中导入:

js 复制代码
// 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);
    }
});

使用:

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

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

效果展示:

三、参考资料

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐4 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄5 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser7 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui