Electron在worker里面上传文件
Electron
在worker
里面上传文件
前言
大家都知道,在
electron
里面,除了壳子,业务代码其实都是放在浏览器环境的,就和我们写react
和vue
项目其实没多大差别,最主要的区别主要是我们可以通过electron
能力,暴露给渲染进程一些方法,渲染进程通过和主进程交互,实现一些系统层面的能力交互。这期我们主要围绕着文件上传进行。
环境
electron
^23.1.1
Node
>18.0.0
electron-builder
^23.6.0
electron-updater
^4.2.0其实关于上传功能,可以有两个选择,渲染进程上传 | 主进程上传,但是,如果通过渲染进程上传,有几个弊端,第一,无法感知文件取消上传回调,第二,渲染进程上传可能会影响渲染进程阻塞等问题,所以,我这里选择了通过主进程里面去做文件上传。
在electron
最新版本中,我们可以通过preload
统一暴露给渲染进程调用主线程中的一些方法,然后主线程中注册,并监听这些方法,从而和渲染进程进行交互,主线程和渲染线程交互的方法有好几种,这里就不过多赘述了,主要围绕文件上传这一流程进行。
1,选择文件
首先,我们选择文件,这里我选择通过electron
的dialog
模块,打开一个选择文件弹窗,然后获取到选择的文件路径,然后通过ipcRenderer.invoke
调用主进程的方法,将文件路径传递给主进程,然后主进程通过fs
模块读取文件,然后通过http
请求上传到服务器。
// 主线程注册监听 选择文件 方法
ipcMain.handle("openDialog", async (_, option) => {
return new Promise((resolve, reject) => {
dialog
.showOpenDialog(mainWindow, Options)
.then((result) => {
if (result.canceled) {
reject("已取消");
} else {
console.log("result", result);
//当前为单选所有
const filePath = result.filePaths[0];
const filePathName = path.extname(filePath);
const fileType = mime.lookup(filePath);
const fileName = path.basename(filePath);
const stats = fsv.statSync(filePath);
const size = coverSize(stats.size);
const fileinfo = {
fileName,
fileType,
size: stats.size,
filePath,
fileStatus: "start",
fileSize: size,
...result,
};
resolve(fileinfo);
}
})
.catch((err) => {
console.error("openDilog==>", err);
reject(err);
});
});
});
// 渲染进程调用主进程方法选择文件
const fileAddress = await window.electronAPI?.ipcInvoke<FileInfo>("openDialog", { properties: ["openFile"], ...(opt || {}) });
2,通知主进程上传文件
选择文件后,可以在渲染进程进行展示文件信息,上传要交给主进程,主进程通过fs模块读取文件,然后通过http请求上传到服务器。首先我们第一步我们要注册上传事件,(当然,注册事件可以只注册一个事件,通过传入不同参数去匹配对应的逻辑处理,我这里就单独注册了,看需求考量)
//主进程
ipcMain.handle("uploadFile", async (_, fileInfo) => {
upload(fileInfo)
})
通过给主进程发送消息,主进程上传文件即可
//渲染进程(这里需要传入所需要的参数即可)
window.electronAPI?.ipcInvoke("uploadFile", {
fileAddress: fileInfo.filePath,
folder,
id: insertId,
options: {
fileObsUrl,
downloadKey,
conversationID,
insertClientMsgID,
folder,
fileInfo,
insertId,
},
});
到这里,其实交互流程就已经算完结了,但是,还有两个功能没有处理,第一个就是上传进程以及上传完成后要通知渲染进程去修改状态;第二个也是最重要的,实现上传的逻辑部分
3,上传文件
上传文件,这里我们用的三方obs
,相信大家都有自己的上传方式,但是有一个问题相信大家都知道,如果是一个聊天或者多任务列表,这种方式其实不能只有一个上传,会有很多的上传任务,那么,我们要考虑多个任务的情况,去并发这些任务,其实就是多线程上传,在浏览器端,也是是woker
,同理,在主线程中,我们可以使用 worker_threads
去进行多线程,这是node > 12版本以后新增的功能,不是三方模块实现的,属于node
原生自带功能。和 browser woker
使用方法一致
// worker.js, 这里大致列一下,有些断点续传的功能或者其他业务处理,错误处理可以自己加
const { workerData, parentPort } = require("worker_threads");
const path = require("node:path");
const ObsClient = require("esdk-obs-nodejs");
function nodeUploadFiles(option, parentPort, onResumeCallback) {
const { fileAddress, folder, id } = option;
const fileName = path.basename(fileAddress);
const fileType = path.extname(fileAddress);
let prePercent = 0;
nodeObsClient.putObject(
{
Bucket: PCIMFOLDER,
Key: `${ENV}/${folder}${fileName}`,
SourceFile: fileAddress,
ProgressCallback: (transferredAmount, totalAmount, totalSeconds) => {
const p = (transferredAmount * 100) / totalAmount;
const percent = Math.round(p);
if (prePercent !== percent) {
parentPort.postMessage({ type: "percent", value: { percent, status: "starting", ...workerData }, id });
prePercent = percent;
}
},
ResumeCallback: (resumeHook) => {
hook = resumeHook;
},
},
(err, data) => {
if (err) {
// 处理上传报错逻辑
parentPort?.postMessage({ type: "percent", value: { status: "errorFile", percent: 0, ...workerData }, id: workerData?.id, error: err });
reject(err);
} else {
// 上传完成后的逻辑
console.info(`upload=>complete`);
const newData = {
url: `${obsUrl}${ENV}/${folder}${fileName}`,
code: data.CommonMsg.Status,
Message: data.CommonMsg.Message,
...data,
};
parentPort.postMessage({ type: "percent", value: { percent: 100, status: "completed", ...workerData }, id });
}
}
);
}
handleUpload(workerData);
// upload 方法
import { Worker } from "node:worker_threads";
const runWorkers = (workerDatas) => {
return new Promise(() => {
const workerPath1 = path.resolve(__dirname, "./worker.js");
const worker = new Worker(workerPath, { workerData: { ...workerDatas, resourcesPath } });
const taskHandle = (worker, option) => {
worker?.postMessage(option);
};
const onExitCallback = () => {
global.globalEmitter.off(workerDatas.id, taskHandle);
};
// 监听来自woker的消息推送
worker.on("message", onMessageCallback);
// 错误,这里如果有错误,要抛出去,给渲染进程,单独处理
worker.on("error", onErrorCallback);
// 如果woker异常退出,要移除监听,并关闭 woker
worker.on("exit", onExitCallback);
});
};
到这里,最简单版的一个文件上传功能就完成了,但是,还有两个问题,一个是断点续传,一个是上传失败后,需要重新上传,这里就自己实现一下,方法大致就是记录一下断点的位置,然后下次上传的时候,从断点处开始上传,这里就不贴代码了,调研一下obs
的文档应该都可以找到
最后
其实写到这里,只是引出了通过worker
去上传这个功能,并没有实现完整的链路
woker
本身是一个线程,但是重复开关导致线程资源浪费,所以,我们要实现一个线程池,去根据cups
数量,动态去扩展我们woker
的数量;woker
如果通过线程池去动态扩展,那么,woker
里面的任务,其实也需要任务池,动态去给每一个woker
去分发任务,这里其实就是一个任务队列,每次根据woker的运行状态去分发任务;- 不知道大家有没有遇到过,在
electron- build
打包后使用worker_threads
的时候,woker
文件里面三方包路径会有问题,而且,在new woker
的地方传入的woker
文件的path
也是有问题的
等后面有空了补上,如果有错误,希望大家可以指正。
转载自:https://juejin.cn/post/7399851256226185225