likes
comments
collection
share

Electron在worker里面上传文件

作者站长头像
站长
· 阅读数 30

Electronworker里面上传文件

前言

大家都知道,在electron 里面,除了壳子,业务代码其实都是放在浏览器环境的,就和我们写reactvue项目其实没多大差别,最主要的区别主要是我们可以通过electron 能力,暴露给渲染进程一些方法,渲染进程通过和主进程交互,实现一些系统层面的能力交互。这期我们主要围绕着文件上传进行。

环境

electron ^23.1.1

Node >18.0.0

electron-builder ^23.6.0

electron-updater ^4.2.0

其实关于上传功能,可以有两个选择,渲染进程上传 | 主进程上传,但是,如果通过渲染进程上传,有几个弊端,第一,无法感知文件取消上传回调,第二,渲染进程上传可能会影响渲染进程阻塞等问题,所以,我这里选择了通过主进程里面去做文件上传。

electron最新版本中,我们可以通过preload 统一暴露给渲染进程调用主线程中的一些方法,然后主线程中注册,并监听这些方法,从而和渲染进程进行交互,主线程和渲染线程交互的方法有好几种,这里就不过多赘述了,主要围绕文件上传这一流程进行。

1,选择文件

首先,我们选择文件,这里我选择通过electrondialog模块,打开一个选择文件弹窗,然后获取到选择的文件路径,然后通过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去上传这个功能,并没有实现完整的链路

  1. woker本身是一个线程,但是重复开关导致线程资源浪费,所以,我们要实现一个线程池,去根据cups数量,动态去扩展我们woker的数量;
  2. woker如果通过线程池去动态扩展,那么,woker里面的任务,其实也需要任务池,动态去给每一个woker去分发任务,这里其实就是一个任务队列,每次根据woker的运行状态去分发任务;
  3. 不知道大家有没有遇到过,在electron- build 打包后使用 worker_threads 的时候,woker文件里面三方包路径会有问题,而且,在new woker的地方传入的woker文件的path也是有问题的

等后面有空了补上,如果有错误,希望大家可以指正。

转载自:https://juejin.cn/post/7399851256226185225
评论
请登录