electron 桌面端业务中的小结(坑)

2,008 阅读9分钟

[toc]

简介

在开发 electron 桌面端的时候,会遇到各种各样的坑,稍微总结下。

  • 安装electron依赖
  • 本地数据库选择
  • 自动升级
  • 网络检查(window)
  • 主进程http请求客户端
  • 下载文件
  • http请求客户端
  • IPC 通讯(渲染window 发送消息给主进程)
  • IPC 通讯(主进程发送消息给渲染window)
  • 设置开机启动项
  • 其他应用唤醒客户端
  • 全局快捷键
  • 托盘
  • 应用菜单(mac)
  • 国际化
  • TouchBar
  • 硬件加速(mac)
  • 模式(development、production)
  • 崩溃日志发送
  • 单例模式
  • 白屏
  • electron bridge
  • 集成 vue 、react
  • 代理设置
  • 打包发布

安装electron依赖

由于网络问题,导致 yarn 或者 npm 安装的时候,会报错

解决方案

sudo npm install -g cross-env 
cross-env npm_config_electron_mirror="https://npm.taobao.org/mirrors/electron/" npm_config_electron_custom_dir="9.4.0" npm install

这里的 9.4.0 就是你所想安装的electron 版本。

本地数据库选择

本地数据库有很多可选的,

indexedDB

可以同时支持web端和桌面端使用。

IndexedDB是Chromium内置的一个基于JavaScript的面向对象的数据库,在Electron应用内它存储的容量限制与用户的磁盘容量有关,是用户磁盘大小的1/3.

特点:

  • NoSQL数据库,浏览器自带,可以储存大量数据,容量为250MB以上
  • 支持事务,有版本号的概念。
  • 支持较多的字段类型

封装的库

  • localForage,支持类Storage API语法的客户端数据存储polyfill,支持回退到Storage和Web SQL
  • dexie.js,提供更友好和简单的语法便于快速的编码开发,有Typescript支持。
  • ZangoDB,提供类MongoDB的接口实现,提供了许多MangoDB的特性实现
  • JsStore,提供基于indexedDB的类SQL的语法实现。

SQLite

关系型数据库,具有关系型数据库的一切特性,事务遵循ACID属性。小巧轻便,有knex这样的库做ORM。

是node原生模块,需要重新编译,而且有坑。

npm install sqlite3 --save
npm install electron-rebuild --save

执行 electron-rebuild,重新编译electron。

Lowdb

基于Loadsh的纯JSON文件数据库,速度较慢.

不支持索引/事务/批量操作等数据库功能

Small JSON database for Node, Electron and the browser. Powered by Lodash. ⚡️

electron-store

适合简单存储

electron-json-storage-alt、electron-json-storage

Easily write and read user settings in Electron apps

localstorage/sessionstorage

自带的存储。

坑点:

LocalStorage存储容量也很小,大概不会超过10M,它是以键值对形式保存数据的,同样也没有关联查询、条件查询的机制

SessionStorage最大的问题是,每次关闭应用程序,它里面的内容会被清空,想持久化存储数据,就不用考虑它了

cookie

Cookies存储容量太小,只能存4kb的内容,而且每次与服务端交互,同域下的Cookie还会被携带到服务端,也没有关联查询、条件查询的机制

小结

一般大型项目 首选 SQLite或者 indexedDB,如果一些用户设置上面的设置 可以通过 electron-json-storage-alt 实现。

自动升级

推荐使用electron-updater 或者自己写升级模块。

electron-updater

import { autoUpdater } from 'electron-updater';

let downloading = false;


function checkForUpdates() {
    if (downloading) {
        dialog.showMessageBox({
            type: 'info',
            buttons: ['OK'],
            title: pkg.name,
            message: `Downloading...`,
            detail: `Please leave the app open, the new version is downloading. You'll receive a new dialog when downloading is finished.`
        });

        return;
    }

    autoUpdater.checkForUpdates();
}

autoUpdater.on('update-not-available', e => {
    dialog.showMessageBox({
        type: 'info',
        buttons: ['OK'],
        title: pkg.name,
        message: `${pkg.name} is up to date :)`,
        detail: `${pkg.name} ${pkg.version} is currently the newest version available, It looks like you're already rocking the latest version!`
    });

    console.log('Update not available.');
});


autoUpdater.on('update-available', e => {
    downloading = true;
    checkForUpdates();
});


autoUpdater.on('error', err => {
    dialog.showMessageBox({
        type: 'error',
        buttons: ['Cancel update'],
        title: pkg.name,
        message: `Failed to update ${pkg.name} :(`,
        detail: `An error occurred in retrieving update information, Please try again later.`,
    });

    downloading = false;
    console.error(err);
});

autoUpdater.on('update-downloaded', info => {
    var { releaseNotes, releaseName } = info;
    var index = dialog.showMessageBox({
        type: 'info',
        buttons: ['Restart', 'Later'],
        title: pkg.name,
        message: `The new version has been downloaded. Please restart the application to apply the updates.`,
        detail: `${releaseName}\n\n${releaseNotes}`
    });
    downloading = false;

    if (index === 1) {
        return;
    }

    autoUpdater.quitAndInstall();
    setTimeout(() => {
        mainWindow = null;
        app.quit();
    });
});

自己写的升级模块

可以在项目启动或者页面激活状态的时候,请求服务器端最新版本号,如果存在版本过低的情况可以通知用户下载升级。

同样的是利用electron 下载文件,执行升级。

window 平台

执行执行升级exe

mac 平台

对于 asar文件

执行 child_processexec 调用 unzip -o latestPathMacZip 将 zip 文件解压成 latest.asar 文件,然后调用 renameSync 将原本的app.asar 修改成一个临时名字,将最新的latest.asar 修改成 app.asar 名字,然后最后删除掉临时名字就行了。

对于 dmg 文件

可以通过 mac 自带的hdiutil 指令。

网络检查(window)

一般的桌面端程序,都需要检查网络连接情况,友好的提示给客户。

在 window 里面可以借助network-interface,来监听网络连接情况。

www.npmjs.com/package/net…

const networkInterface = require('network-interface');
 
networkInterface.addEventListener('wlan-status-changed', (error, data) => {
  if (error) {
    throw error;
    return;
  }
  console.log('event fired: wlan-status-changed');
  console.log(data);
});

主进程 http请求客户端

对于桌面端,主进程是会有http请求的。 例如升级啥的,或者下载文件等,都是需要主进程去执行http请求。

  • electron 封装的net模块
  • urllib
  • axios
  • request、request-promise

net模块封装

net 模块是一个发送 HTTP(S) 请求的客户端API。 它类似于Node.js的HTTP 和 HTTPS 模块 ,但它使用的是Chromium原生网络库来替代Node.js的实现,提供更好的网络代理支持。 It also supports checking network status.

net 的优势

  • 系统代理配置的自动管理, 支持 wpad 协议和代理 pac 配置文件。
  • HTTPS 请求的自动隧道。
  • 支持使用basic、digest、NTLM、Kerberos 或协商身份验证方案对代理进行身份验证。
  • 支持传输监控代理: 类似于Fiddler代理,用于访问控制和监视。
const { net } = require('electron')

urllib封装

const os = require('os');
const urllib = require('urllib');
const Agent = require('agentkeepalive');
const {HttpsAgent} = require('agentkeepalive');
const {electron: electronVersion} = process.versions;


const config = {
    defaultArgs: {
        timeout: 30000,
        dataType: 'json',
        followRedirect: true
    },
    httpAgent: {
        keepAlive: true,
        freeSocketTimeout: 20000,
        maxSockets: Number.MAX_SAFE_INTEGER,
        maxFreeSockets: 256
    },
    httpsAgent: {
        keepAlive: true,
        freeSocketTimeout: 20000,
        maxSockets: Number.MAX_SAFE_INTEGER,
        maxFreeSockets: 256
    }
};

class HttpClient extends urllib.HttpClient2 {
    constructor(app) {
        const {pkg} = app.config;
        super({
            defaultArgs: config.defaultArgs,
            agent: new Agent(config.httpAgent),
            httpsAgent: new HttpsAgent(config.httpsAgent)
        });

        this.app = app;
        this.logger = app.getLogger('httpClientLogger');
        this.UA = `${pkg.name}/${pkg.version};electron/${electronVersion};${encodeURIComponent(os.hostname())};${urllib.USER_AGENT}`;
    }

    async request(url, options = {}) {
        const {app} = this;
        const {host} = app.config || '';

        let request;

        options.headers = {
            "Content-Type": "application/json",
            referer: host,
            "user-agent": this.UA,
            ...options.headers
        };
        const nowDate = Date.now();
        let error;
        try {
            return request = await super.request(url, options);
        } catch (e) {
            error = e;
            error.name = 'httpError';
            error.url = url;
            throw error;
        } finally {
            // 一次请求的时间差
            const timestamp = Date.now() - nowDate;
            // logger 日志记录
            console.log(timestamp);

            if (!options.disableLogger) {
                this.logger.info([url, options.method, request && request.status, error && error.message].join("^"));
            }
        }
    }
}

module.exports = (app => {
    app.httpClient = new HttpClient(app);
})

axios

axios 是支持在node.js的。可以在electron 主进程中使用。

import axios from 'axios';

axios.defaults.baseURL = process.env.VUE_APP_BASE_URL;
<!--强制使用node模块。--> 
axios.defaults.adapter = require('axios/lib/adapters/http');

// 请求拦截  设置统一header
axios.interceptors.request.use(
    config => {
        return config;
    },
    error => {
        console.log(error);
        return Promise.reject(error);
    }
);

axios.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        // 
        console.error(error);
        return Promise.reject(error);
    }
);

export default axios;

request.js

下载文件

文件下载,需要借助electron 提供的getPath方法,一般默认的是下载目录下面

const downloadPath = electronApp.getPath('downloads');

对于node http 模块封装

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

const downloadFileAsync = (uri, dest) => {
    return new Promise((resolve, reject) => {
        let file = '';
        try {
            // 确保dest路径存在,已经存在则删除重新下载
            if (fs.existsSync(dest)) {
                fs.unlinkSync(dest);
            }
            const path = process.platform === 'win32'
                ? dest.slice(0, dest.lastIndexOf('\\'))
                : dest.slice(0, dest.lastIndexOf('/'));

            fs.mkdirSync(path, { recursive: true });
            file = fs.createWriteStream(dest);
        } catch (e) {
            reject(e.message);
        }


        http.get(uri, (res) => {
            if (res.statusCode !== 200) {
                reject(response.statusCode);
                return;
            }

            res.on('end', () => {
                console.log('download end');
            });

            // 进度、超时等

            file.on('finish', () => {
                console.log('finish write file')
                file.close(resolve);
            }).on('error', (err) => {
                fs.unlink(dest);
                reject(err.message);
            })

            res.pipe(file);
        });
    });
}

对于urllib 的封装

业务的封装

const {app: electronApp, dialog} = require('electron');
const {createWriteStream} = require('fs');
const {parse} = require('url');
const path = require('path');


const downloadFile = async (app, url) => {
    const downloadPath = electronApp.getPath('downloads');
    const {pathname} = parse(url);
    const fileName = pathname.split('/').pop();
    const localFilePath = path(downloadPath, fileName);

    const {canceled, filePath} = await dialog.showSaveDialog(app.mainWindow, {
        title: '保存附件',
        default: localFilePath
    })

    if (!canceled) {
        const savedFilePath = path.join(path.dirname(filePath), fileName);
        const writeSteam = createWriteStream(savedFilePath);

        const request = app.httpClient.request(url, {
            headers: {
                'Content-Type': null
            },
            streaming: true,
            followRedirect: true
        })

        const needShowProgress = Number(request.headers['content-length']) > 1048576;

        const downloadResponse = (type) => {
            // progress
            app.mainWindow.webContents.send('download-progress', {type});
        }

        request.res.on("data", data => {
            writeSteam.write(data);

            if (needShowProgress) {
                downloadResponse('data');
            }
        });

        request.res.on('end', () => {
            writeSteam.end();
            downloadResponse('end');
        });

        request.res.on('error', () => {
            downloadResponse('error');
        })
    }
};

客户端日志

这里涉及到了,日志的本地存储,以及日志的打包上传到服务器。

市面上现在的技术方案

  • winston
  • electron-log

winston

所使用到的插件有winston-daily-rotate-filewinston-transport用于文件切割和远程上传。

对于 winston 的封装

const path = require('path');
const winston = require('winston');
require('winston-daily-rotate-file');
const Transport = require('winston-transport');
const {app: electronApp} = require('electron');
const {format} = winston;
const logReomteUrl = 'http://test.com/electron/log';

const logger = function (options = {}) {
        return () => {
            const logDir = options.logDir || path.join(options.debug ? process.cwd() : electronApp.getPath('userData'), 'logs');

            const transportList = [
                new winston.transports.DailyRotateFile({
                    dirname: path.join(logDir, options.name),
                    filename: `${options.filename || options.name}-%DATE%.log`,
                    maxSize: '15m',
                    maxFiles: 7,
                    createSymlink: true,
                    symlinkName: `${options.symlinkName || options.name}.log`
                }),
                new class extends Transport {
                    constructor(props) {
                        super(props);
                        this.options = props;
                    }

                    log(options = {}, callback) {
                        if (process.env.DISABLE_LOG_REMOTE) {
                            return;
                        }

                        const data = {
                            type: this.options.name,
                            message: `${options.timestamp} ${options.message}`
                        };

                        // 提交服务器端的日志地址。
                        const url = logReomteUrl;

                        // request
                        app.httpClient.request(url, {
                            method: 'POST',
                            contentType: "json",
                            data: data,
                            disableLogger: true,
                        }).catch(() => {

                        });

                        callback(null, true);
                    }
                }(options)
            ];

            <!--是否同步控制台输出-->
            if (process.env.CONSOLE_LOGGER) {
                transportList.push(new winston.transports.Console);
            }

            return new winston.createLogger({
                format: format.combine(
                    format.label({label: options.name}),
                    format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
                    format.splat(),
                    format.simple(),
                    format.printf(({level, timestamp, message, label}) => {
                        const {tracert = {}, currentUser = {}} = options.currentContext || {};
                        return [timestamp, level.toUpperCase(), `[${label}]`, tracert.traceId, currentUser.id, message].join("^")
                    })
                ),
                transports: transportList
            })
        }
    };

// 使用

// electron 桌面端日志
app.electronLog = logger({
    name: "electron",
    symlinkName: 'app',
    debug: app.isDev
})
// 给web端调用的日志
app.webLogger = logger({
    name: "web",
    debug: app.isDev
});

electron-log

自行查询。

IPC 通讯(渲染window 发送消息给主进程)

借助electron 提供的 ipcMainipcRenderer 进行通讯。

ipcMain

method

  • on(channel,listener)
  • once(channel,listener)
  • removeListener(channel,listener)
  • removeAllListeners([channel])
  • handle(channel,listener)
  • handleonce(channel,listener)
  • removeHandler(channel)

demo

// 在主进程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.returnValue = 'pong'
})

ipcRenderer

method

  • on(channel,listener)
  • once(channel,listener)
  • removeListener(channel,listener)
  • removeAllListeners([channel])
  • send(channel,...args)
  • invoke(channel,...args)
  • sendSync(channel,...args)
  • postMessage(channel,message,[transfer])
  • sendTo(webContentsId,channel,...args)
  • sendToHost(channel,...args)

demo

//在渲染器进程 (网页) 中。
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')

IPC 通讯(主进程发送消息给渲染window)

主要借助 BrowserWindow.webContents.sendipcRenderer.on方法

// 主进程
app.mainWindow.webContents.send('xxx','this is message');
// 渲染window
const {ipcRenderer} = require('electron');

ipcRenderer.on('xxx',(event, message)=>{
    console.log(message) // this is message
})

设置开机启动项

主要是通过 setLoginItemSettings 实现

const {ipcMain, app: electronApp} = require('electron');
// 属性返回启动 Node.js 进程的可执行文件的绝对路径名。
const exePath = process.execPath;

module.exports = (() => {
    // 给渲染页面获取当前的状态
    ipcMain.handle('get-auto-start-status', () => electronApp.getLoginItemSettings())

    // 设置开启自启
    ipcMain.on('auto-start-open', () => {
        electronApp.setLoginItemSettings({
            openAtLogin: true,
            path: exePath,
            args: []
        })
    });

    //设置开机不自启
    ipcMain.on('auto-start-closed', () => {
        electronApp.setLoginItemSettings({
            openAtLogin: false,
            path: exePath,
            args: []
        })
    })

});

其他应用唤醒客户端

主要是借助setAsDefaultProtocolClient 实现其他应用唤起客户端。

const {app: electronApp, dialog} = require('electron');
const path = require('path');
const SCHEMA_NAME = 'xx'; // 协议前缀

if (process.defaultApp) {
    if (process.argv.length >= 2) {
        electronApp.setAsDefaultProtocolClient(SCHEMA_NAME, process.execPath, [path.resolve(process.argv[1])]);
    }
} else {
    electronApp.setAsDefaultProtocolClient(SCHEMA_NAME);
}

electronApp.on('open-url', ((event, url) => {
    dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`);
}));

全局快捷键

借助 globalShortcut 方法实现


const {globalShortcut} = require("electron");

module.exports = (app => {
    // 注册全局快捷键
    globalShortcut.register("CommandOrControl+Option+Y", () => {
        app.mainWindow.show()
    })
});

托盘

需要考虑window 系统和macOS 系统,对于macOS 还需要考虑是不是暗黑模式。

对于mac 还有

const macLightIcon = path.join(__dirname, "../../../dist-assets/tray/tray@2x.png");
const macDarkIcon = path.join(__dirname, "../../../dist-assets/tray/tray-active@2x.png");
const winLightIcon = path.join(__dirname, "../../../dist-assets/tray/tray-windows.png");

mac 应用菜单

对于mac 端的左上角也有一个应用的菜单,主要是通过 Menu.setApplicationMenu(Menu.buildFromTemplate([])) 实现的

对于 tabs 数组

const tabs = [
{
    label: 'Application',
    submenu:[
        {
            label:'xxxx',
            accelerator: "CommandOrControl+,",
            click:()=>{
                // 
            }
        },
        {type: "separator"}, // 一根线
        {
             label:'xxx2',
            accelerator: "CommandOrControl+Q",
            click:()=>{
                // 
            }
        }
    ]
}
]

国际化

默认提供的是中文,然后通过配置文件,来获取到其他语言的翻译

// en-US
module.exports = {
    "测试": "Test",
    "关于xx": "About xx",
    "退出": "Quit",
    "调试": "Debug",
    "窗口": "Window",
    "托盘":'Tray'
};

然后在app 方法里面绑定一个方法

const en = require('../locales/en-US');

// 匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]。
const interpolate_reg = /\{(\w*)\}/g;

// replace data
const replaceData = (key, lang) => {
    return key.replace(interpolate_reg, value => {
        const tempKey = value.slice(1, value.length - 1);
        return lang[tempKey] ? lang[tempKey] : key;
    })
};


module.exports = (app => {
    // 绑定 t 方法。
    app.t = ((title, lang = {}) => {
        // 绑定到app 上面的语言。
        if (app.locale.startsWith('zh')) {
            return replaceData(title, lang);
        }
        const enLang = en[title];
        return enLang ? replaceData(enLang, lang) : title;
    })
});

然后在调用的时候。

app.t('关于xx') //这样就会根据不同的语言情况。获取到不同翻译了。

然后在启动项目的时候,渲染线程,拉取桌面端的语言,然后根据语言更新window 端的语言。

// 在主线程里面

 ipcMain.handle('get-app-config', async () => {
    const locale = await app.getLocale();

    return {
        timestamp: (new Date).getTime(),
        locale: locale,
        ...app.config
    }
});
// 在渲染window 里面 
  import {isElectron, getAppConfig, addEventListener, EVENTS} from "./utils/electron";

async created() {
  if (isElectron) {
    const appConfig = await getAppConfig();
    console.log(appConfig);
    // 
    this.$store.commit('UPDATE_ELECTRON_CONFIG', appConfig);
  }
},

TouchBar

主要借助 TouchBarTouchBarButton 接口注册进去

每个 TouchBarButton 参数有 label,backgroundColor, click

最后通过 win.setTouchBar(TouchBar) 注册进去。

硬件加速(mac)

mac版本的的electron 可能会出现花屏现状,解决方案就是关闭硬件加速。

通过 disableHardwareAcceleration实现。

模式(development、production)

通过注入 process.env.XXX 这里 XXX 是通过 cross-env 组件注入进去的。

在启动electron的时候

{
    "scripts":{
        "dev":"cross-env NODE_ENV=development electron ."
    }
}

这样就可以在代码里面通过 process.env.NODE_ENV 获取了。

this.isDev = "development" === process.env.NODE_ENV;

崩溃日志发送

const {crashReporter} = require('electron');

crashReporter.start({
    productName: "test",
    companyName: "test",
    submitURL: "https://www.test.com",
    autoSubmit: true,
    uploadToServer: true,
    ignoreSystemCrashHandler: true
});

单例模式

通过requestSingleInstanceLock方法,来实现 单例模式

const singleInstanceLock = electronApp.requestSingleInstanceLock();

if (singleInstanceLock) {
    //
    electronApp.on('second-instance', () => {
        //  查看打开的是否是 login window 还是 main window
        app.loginWindow &&
        !app.loginWindow.isDestroyed() ?
            (app.loginWindow.isVisible() || app.loginWindow.show(),
            app.loginWindow.isMinimized() && app.loginWindow.restore(),
                app.loginWindow.focus()) :
            app.mainWindow &&
            (app.mainWindow.isVisible() || app.mainWindow.show(),
            app.mainWindow.isMinimized() && app.mainWindow.restore(),
                app.mainWindow.focus())
    });

    // 监听  ready 事件
    electronApp.on('ready', async () => {
        <!--app 的初始化操作-->
        await app.init();
        <!--唤起登录,自动登录,选择登录。啥的-->
        app.launchLogin();
    });


} else {
    electronApp.quit();
}

白屏

在项目启动的时候,需要先用 loading.html 页面加载,防止出现白屏现象。

依赖 BrowserViewBrowserWindow

新建完 BrowserWindow 之后,利用BrowserView 加载loading页面,监听BrowserWindow.webContens的dom-ready 事件。移除掉BrowserView

const {BrowserView, BrowserWindow} = require('electron');

const browserWindow = new BrowserWindow({
    width: 500,
    height: 600,
    // 其他参数
});

const loadingBrowserView = new BrowserView();

browserWindow.setBrowserView(loadingBrowserView);

loadingBrowserView.setBounds({
    x: 0,
    y: 0,
    width: 400,
    height: 600
});

loadingBrowserView.webContents.loadURL('loading.html');

browserWindow.webContents.on('will-navigate', () => {
    browserWindow.setBrowserView(loadingBrowserView);
});
    
    
browserWindow.webContents.on('dom-ready', async (event) => {
    browserWindow.removeBrowserView(loadingBrowserView);
});

electron bridge

通过在 new BrowserWindow的时候,传递参数 webPreferences.preload 这样就可以设置electron-bridge了。

在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入node的全局引用标志

通过这种方式,就可以实现兼容web端桌面端的应用了。

BrowserWindow http请求拦截

通过监听webContents.session.webRequest.onBeforeRequestwebContents.session.webRequest.onBeforeSendHeaders 就可以了。

 // 需要拦截的请求。
const webRequestFilter = {
    urls: ["*://test.aaa.com/*", "*://*.ccc.com/*"]
};

browserWindow.webContents.session.webRequest.onBeforeRequest(webRequestFilter, (details, callback) => {
    // 监听 before request

    // 是否存在下载
    if (details.url.includes('desktop-download')) {
        downloadFile(app, details.url);
    }
    // 原始请求被阻止发送或完成,而不是重定向到给定的URL
    callback({redirectURL: details.redirectURL});
});

browserWindow.webContents.session.webRequest.onBeforeSendHeaders(webRequestFilter, (details, callback) => {
    // 绑定header 头部。
    if (details.requestHeaders.Cookie) {
        const {ctoken = ''} = cookieParse(details.requestHeaders.Cookie);
        if (ctoken) {
            details.requestHeaders['x-csrf-token'] = ctoken;
        }
    }
    // When provided, request will be made with these headers.
    callback({requestHeaders: details.requestHeaders});
});

集成 vue 、react

vue

主要通过 electron-vue 传送门: github

react

主要是通过 electron-react-boilerplate 传送门:github

无frame

只需要在创建 BrowserWindow 的时候,参数 frame 为false 就可以了。

const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ width: 800, height: 600, frame: false })
win.show()

window

就需要自定义实现右上角最大、最小、关闭

<header class="drag-area">
    <div class="header-actions">
      <div @click="handleMinWindow">
        <svg t="1586443316286" className="icon-min" viewBox="0 0 1024 1024" version="1.1">
          <defs>
            <style type="text/css"></style>
          </defs>
          <path
            d="M65.23884 456.152041 958.760137 456.152041l0 111.695918L65.23884 567.847959 65.23884 456.152041z"
            p-id="1094"></path>
        </svg>
      </div>
      <div @click="handleUnMaxWindow" v-if="isWinMax">
        <svg t="1586445181598" className="icon" viewBox="0 0 1157 1024" version="1.1">
          <defs>
            <style type="text/css"></style>
          </defs>
          <path
            d="M1086.033752 753.710082 878.220684 753.710082 878.220684 951.774989 878.220684 1021.784509 878.220684 1023.113804 808.211164 1023.113804 808.211164 1021.784509 70.895716 1021.784509 70.895716 1023.113804 0.886196 1023.113804 0.886196 1021.784509 0.886196 951.774989 0.886196 339.413241 0.886196 269.403721 70.895716 269.403721 269.403721 269.403721 269.403721 0.886196 274.277802 0.886196 339.413241 0.886196 1086.033752 0.886196 1151.612289 0.886196 1156.043271 0.886196 1156.043271 683.700563 1156.043271 753.710082 1086.033752 753.710082ZM70.895716 951.774989 808.211164 951.774989 808.211164 753.710082 808.211164 683.700563 808.211164 339.413241 70.895716 339.413241 70.895716 951.774989ZM1086.033752 70.895716 339.413241 70.895716 339.413241 269.403721 808.211164 269.403721 878.220684 269.403721 878.220684 339.413241 878.220684 683.700563 1086.033752 683.700563 1086.033752 70.895716Z"
            p-id="2415"></path>
        </svg>
      </div>
      <div @click="handleMaxWindow" v-else>
        <svg t="1586443335243" className="icon-max" viewBox="0 0 1024 1024" version="1.1">
          <defs>
            <style type="text/css"></style>
          </defs>
          <path
            d="M128.576377 895.420553 128.576377 128.578424l766.846222 0 0 766.842129L128.576377 895.420553zM799.567461 224.434585 224.432539 224.434585l0 575.134923 575.134923 0L799.567461 224.434585z"
            p-id="1340"></path>
        </svg>
      </div>

      <div @click="handleCloseWindow">
        <svg t="1586443316286" className="icon-close" viewBox="0 0 1024 1024"
             version="1.1">
          <path
            d="M895.423623 224.432539 607.855138 512l286.901289 297.699216 0.666172 85.723384-95.856162 0L512 607.856162 224.432539 895.423623l-95.856162 0 0-95.856162 287.567461-287.567461L128.576377 224.432539l0-95.856162 95.856162 0 287.567461 287.567461 287.567461-287.567461 95.856162 0L895.423623 224.432539z"
            p-id="1217"></path>
        </svg>
      </div>
    </div>
  </header>  
.drag-area {
  -webkit-app-region: drag;
  -webkit-user-select: none;
  user-select: none;
  z-index: 500;
  width: 100vw;
}

mac

顶部需要设置一条可以拖拽的区域

.drag-area {
  -webkit-app-region: drag;
  -webkit-user-select: none;
  user-select: none;
  z-index: 500;
  width: 100vw;

  background-color: transparent;
  height: 18px;
  position: fixed;
}

设置DOM拖拽联通窗口拖拽

.drag-area {
  -webkit-app-region: drag; // 支持窗口拖拽
  -webkit-user-select: none;
  user-select: none;
  z-index: 500;
}

代理设置

待定。。。

打包发布

主要的打包方案 如下:

  • electron-builder
  • electron-packager

electron-builder

通过在 package.json 里面配置

{
"scripts":{
    "build":"electron-builder"
}

"build":{
    "productName":"productName",
    "appId":"appId",
    "directories":{
        "output": "output"
    },
    "files":[
    ],
    "nsis":{
        
    },
    "dmg":{
        
    },
    "mac":{
        
    },
    "win":{
        
    },
    "linux":{
        
    }
}
}

electron-packager

打包参数:

electron-packager <sourcedir> <appname> <platform> <architecture> <electron version> <optional options>
  • sourcedir:项目所在路径
  • appname:应用名称
  • platform:确定了你要构建哪个平台的应用(Windows、Mac 还是 Linux)
  • architecture:决定了使用 x86 还是 x64 还是两个架构都用
  • electron version:electron 的版本
  • optional options:可选选项

优缺点

1、支持平台有:Windows (32/64 bit)、OS X (also known as macOS)、Linux (x86/x86_64); 2、进行应用更新时,使用electron内置的autoUpdate进行更新 3、支持CLI和JS API两种使用方式;

项目地址

为此,我将业务系统里面用到的各个客户端需要的功能,剥离了出来,新建了一个template 方便新业务系统的开发。

编码不易,欢迎star

github.com/bosscheng/e…

github