2

Electron怎么启动并行的子任务

有些场景下比如要处理一大堆文件内容的查找,字符串的替换,文件的修改等一系列耗时操作的时候,如果放在主进程执行那必然会导致渲染进程的阻塞,界面会出现卡死的现象,影响用户体验。那怎么能并行地去处理这些事情呢,可以通过node的多进程去实现。如果你在思考把这些任务放在新的渲染进程中去做,那么每次启动一个任务都要打开一个隐藏的窗口,还要考虑去怎么关闭,代价有点高。这里用一个清除缓存的例子介绍下具体步骤,功能很简单就是每次应用启动的时候清理一些指定的文件。

Node多进程

先简单介绍一下,node的多进程是通过child_process和cluster模块实现。child _process模块中创建子进程有这样几种方式,forkspawnexecexecFile。spawn可以指定子进程执行的文件路径,也可以传递参数,但是API使用起来要麻烦一点,fork与spawn功能类似,在它上面又做了一次封装,所以在传递参数上面要更简洁一些,exec和execFile两个差不多,都可以直接执行命令或者传入可执行文件的路径。cluster是属于集群的API,可以更方便地处理主进程和其他子进程之间的关系,就是更容易地处理负载问题,因为Node的多进程都是属于一个Master进程管理多个Worker进程的形式。

Electron中使用child_process模块

Electron中使用多进程有个坑,它不能在子进程中使用非Node标准模块的其他模块,比如第三方模块或者Electron中的模块,当你有这样的代码时就会出现错误,require('lodash')或者require('electron')。这是因为子进程中会有一个预设的环境变量,ELECTRON_RUN_AS_NODE=true,这样的话就会认为在Node的环境下执行,所以第三方模块和Electron是找不到的,而且这个值你是改不了的,可以参考这个issue,当你有这样的需求的时候就要考虑用一些其他的方式了。

生成任务脚本

Electron在应用程序打包后会在一个asar文件中,里面的文件目录是不能直接去引用的,所以如果启动的任务是一个文件的执行路径,那么这个文件需要放在一个可以直接读取的路径上,那就需要在启动或者其他的某个时间去把工程中的脚本拷贝到磁盘某个可以正常访问的路径上。
可以采取这样的作法,把已经写好的脚本放在工程的static目录下,这样打包后就会原封不动地放在<应用目录>/dist/electron/static目录下,然后把对应的文件拷贝到指定路径,例如我这里直接拷贝到了应用目录的父级目录,这里说的应用目录就是你的asar文件所在目录。

//copyUtils

const path = require('path');
const fs = require('fs');
import {app, dialog} from 'electron';

const FILE_NAME_LIST = ['clean.js'];

function copyFile(fileName, callback) {
    let fromPath = path.resolve(app.getAppPath(), 'dist', 'electron', 'static') + path.sep;
    let targetPath = path.resolve(app.getAppPath(), '../') + path.sep;
    let fromFileName = fromPath + fileName;
    let targetFileName = targetPath + fileName;
    if (!fs.existsSync(targetFileName)) {
        fs.readFile(fromFileName, (readErr, data) => {
            if (!readErr) {
                fs.writeFile(targetFileName, data, writeErr => {
                    if (writeErr) {
                        dialog.showErrorBox('WriteErr', writeErr.message);
                    } else {
                        callback(targetFileName);
                    }
                })
            } else {
                dialog.showErrorBox('ReadErr', readErr.message);
            }
        })
    } else {
        callback(targetFileName);
    }
}

export default {
    initScripts(callback) {
        if (process.env.NODE_ENV === 'production') {
            let allCount = DLL_NAME_LIST.length;
            let currentCount = 0;
            for (let item of DLL_NAME_LIST) {
                copyFile(item, name => {
                    ++currentCount;
                    if (currentCount === allCount) {
                        callback(true);
                    }
                });
            }
        }
    }
}

这个clean.js就是清理缓存的脚本。

// clean.js

process.on('message', folder => {
    // 接收一个目录然后去查找匹配的文件并删除,具体的就不贴了,无关紧要
    delete(folder);
    
    // 执行完自动退出
    process.exit(0)
});

console.log('clean task has created...');

当文件不存在的时候才会拷贝脚本,假如脚本存在但是已经被修改了,这时候是不知道的,那执行的时候就会出错,更好的做法是再校验一下文件的md5,如果文件损坏依然执行拷贝操作。

const crypto = require('crypto');
const fs = require('fs');

let hash = crypto.createHash('md5');
let buffer = fs.readFileSync('脚本目录');
hash.update(buffer);
let md5 = fsHash.digest('hex');
// 比较当前md5与预设的md5是否一致

部署脚本

可以选择在应用启动的时候执行文件的拷贝

// deployUtils

import {app} from 'electron'
import copyUtils from './copyUtils';
const path = require('path');
const cp = require('child_process');

const runtimeFolder = path.resolve(app.getAppPath(), '..') + path.sep;
const cleanProcessName = 'clean.js';

function startTask() {
    let cleanProcess = cp.fork(runtimeFolder + cleanProcessName);
    cleanProcess.send('清理的目录');
    // 有更多的任务可以一直继续fork追加
}

/**
 * 把所有.js结尾的文件都拷贝到指定目录
 * @param {String} srcDir 源文件目录 
 * @param {function} next 回调函数 
 */
function copyFile(srcDir, next) {
    fs.readdir(srcDir, (err, files) => {
        if (err) {
            console.log(err)
        } else {
            let index = 0;
            let targetCount = 0;
            for (let item of files) {
                if (item.endsWith('.js')) {
                    targetCount++;
                    let distFilePath = runtimeFolder + item;
                    // 同名文件会被覆盖
                    fs.copyFile(srcDir + path.sep + item, distFilePath, err => {
                        if (err) {
                            console.log(err)
                        } else {
                            index++;
                            if (index == targetCount) {
                                next();
                            }
                        }
                    })
                }
            }
        }
    })
}

export default {
    deploy() {
        if (process.env.NODE_ENV === 'production') {
            copyUtils.initScripts(() => {
                startTask();
            })
        } else {
            let fromDir = path.resolve(__dirname, '..', '..', 'static') + path.sep;
            new Promise(resolve => {
                copyFile(fromDir, resolve);
            }).then(() => {
                startTask();
            })
        }
    }
}

可以在创建窗口的时候启动

// src/main/index.js

import deployTask from './deployTask'

...

function createWindow() {
    deployTask.deploy();
}

我来自伯纳乌
298 声望9 粉丝

喜欢钓鱼、爱踢球的程序猿一枚