[toc]
背景
接着上一篇文章
在开发 electron 桌面端的时候,会遇到各种各样的坑,稍微总结下。
- 杀毒软件破坏检查
- 防止debug调试
- 客户端崩溃报告
- 提升客户端启动速度
- 性能监测分析
- 延迟加载模块
- 滚动条样式统一
- browserWindow 错误监听
- browserWindow a 标签,打开默认浏览器
- electron-中无法使用-jquery、requirejs、meteor、angularjs。
- electron-bridge
- 代理设置
- 系统版本比较(mac)
- 多窗口管理
- 类似vscode无缝升级安装
杀毒软件破坏检查
对于内部的一些核心的文件,可以通过白名单机制,来查看文件是否存在,如果不存在则报告软件被破坏,直接退出。
const getBinaryFileCheckList = ()=>{
const dir = [];
// 比如 network-interface 这个包是必须的。
const network = require.resolve("network-interface/package"),
dir.push(network);
return dir;
}
const binaryFileCheckList = getBinaryFileCheckList();
<!--检查-->
for (let e = 0; e < binaryFileCheckList.length; e++) {
const n = binaryFileCheckList[e];
if (!fs.existsSync(n)) {
dialog.showErrorBox("启动失败", "应用文件损坏,可能是杀毒软件导致,请重新下载安装");
// 直接 exit
electronApp.exit(1);
break
}
}
防止debug调试
需要检查 argv 的参数上是否存在 chrome 调试的关键词 例如inspect
或者debugging
等。
const runWithDebug = process.argv.find(e => e.includes("--inspect") || e.includes("--inspect-brk") || e.includes("--remote-debugging-port"));
if(runWithDebug){
// 直接退出。
electronApp.quit()
}
客户端崩溃报告
可以借助第三方插件来辅助客户端崩溃报告
@sentry/electron
中文文档
提升客户端启动速度
提升客户端的启动速度有好几个方面去着手。
使用 V8 缓存数据
electorn 使用 V8 引擎运行 js,V8 运行 js 时,需要先进行解析和编译,再执行代码。其中,解析和编译过程消耗时间多,经常导致性能瓶颈。而 V8 缓存功能,可以将编译后的字节码缓存起来,省去下一次解析、编译的时间。
使用v8-compile-cache
缓存编译插件的代码
v8-compile-cache 的使用非常简单,在需要缓存的代码中,添加一行代码即可:
require('v8-compile-cache')
v8-compile-cache 默认缓存到临时文件夹 <os.tmpdir()>/v8-compile-cache-<V8_VERSION>
下,电脑重启后,该文件会被清除掉。
如果希望缓存永久化,可以通过环境变量 process.env.V8_COMPILE_CACHE_CACHE_DIR 来指定缓存文件夹,避免电脑重启后删除。另外,如果希望项目的不同版本对应的缓存不同,可以在文件夹名中加入代码版本号(或其他唯一标识),以此保证缓存和项目版本完全对应。当然,这也意味着项目的多个版本有多份缓存。为了不占用过多磁盘空间,在程序退出时,我们需要删除其他版本的缓存。
性能监测分析
主进程,可以用 v8-inspect-profiler 进行性能监测。生成的 .cpuprofile 文件,可以用 devtools 上的 Javascript Profiler 进行分析。如果用 fork 等方法启动了子进程,也可以用相同的方法监测,只需要设置不同的监测端口。
v8-inspect-profiler
设置启动命令,添加参数 --inspect=${port},设置主进程的 v8 调试端口。
// package.json
{
"name": "test",
"version": "1.0.0",
"main": "main.js",
"devDependencies": {
"electron": "9.2.1"
},
"scripts": {
"start": "electron . --inspect=5222"
},
"dependencies": {
"v8-inspect-profiler": "^0.0.20"
}
}
延迟加载模块
项目的一些依赖模块,是在特定功能触发时才需要使用。所以,没有必要在应用启动时立刻加载,可以在方法调用时再加载。
优化前
// 导入模块
const xxx = require('xxx');
export function share() {
...
// 执行依赖的方法
xxx()
}
优化后
export function share() {
// 导入模块
const xxx = require('xxx');
...
// 执行依赖的方法
xxx()
}
滚动条样式统一
对于window
和macOs
系统里面,默认的滚动轴的宽度是不一样的,先获取滚动轴宽度
function getScrollbarWidth() {
const div = document.createElement('div');
div.style.visibility = 'hidden';
div.style.width = '100px';
document.body.appendChild(div);
const offsetWidth = div.offsetWidth;
div.style.overflow = 'scroll';
const childDiv = document.createElement('div');
childDiv.style.width = '100%';
div.appendChild(childDiv);
const childOffsetWidth = childDiv.offsetWidth;
div.parentNode.removeChild(div);
return offsetWidth - childOffsetWidth;
}
然后根据 getScrollbarWidth
做不同设置。
例如:
<!-- 滚动条上的滚动滑块 -->
::-webkit-scrollbar-thumb{
background-color: rgba(180, 180, 180, 0.2);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover{
background-color: rgba(180, 180, 180, 0.5);
}
<!-- 滚动条轨道 -->
::-webkit-scrollbar-track {
border-radius: 8px;
}
<!-- 整个滚动条 -->
::-webkit-scrollbar{
width: 8px;
height: 8px;
}
document.onreadystatechange=(()=>{
if("interactive" === document.readyState){
// 处理逻辑
}
})
browserWindow 错误监听
主要是监听 unhandledrejection
和 error
事件
error
window.addEventListener('error',(error)=>{
const message = {
message: error.message,
source: error.source,
lineno: error.lineno,
colno: error.colno,
stack: error.error && error.error.stack,
href: window.location.href
};
// 通过 ipcRender 发送到 主进程 进行日志记录。
ipcRenderer.send("weblog", n)
},false)
unhandledrejection
window.addEventListener('unhandledrejection',(error)=>{
if(!error.reason){
return;
}
const message = {
message: error.reason.message,
stack: error.reason.stack,
href: window.location.href
}
<!-- 通过 ipcRender 发送到 主进程 进行日志记录。-->
ipcRenderer.send("weblog", n)
},false)
browserWindow a 标签,打开默认浏览器
业务上面,一般会在 browserWindow 页面上面,会存在a标签,这个时候,如果在electron容器里面,就需要做拦截,并通过默认浏览器打开。
document.addEventListener('click',(event)=>{
const target = event.target;
if(target.nodeName === 'A'){
if(event.defaultPrevented){
return;
}
if(location.hostname){
event.preventDefault();
}
if(target.href){
shell.openExternal(target.href);
}
}
},false);
暴露一个全局的 打开浏览器的方法
window.openExternalLink = ((r)=>{
shell.openExternal(r)
});
electron-中无法使用-jquery、requirejs、meteor、angularjs。
因为 Electron 在运行环境中引入了 Node.js,所以在 DOM 中有一些额外的变量,比如 module、exports 和 require。 这导致 了许多库不能正常运行,因为它们也需要将同名的变量加入运行环境中。
两种方式,
- 一种就是通过配置
webPreferences.nodeIntegration
为false
,通过禁用node.js - 通过在electron-bridge.js 里面 最头部
delete window.require;
,delete window.exports;
,delete window.module;
方式
// 在主进程中.
const { BrowserWindow } = require('electron')
const win = new BrowserWindow(format@@
webPreferences: {
nodeIntegration: false
}
})
win.show()
<head>
<script>
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
</script>
<script type="text/javascript" src="jquery.js"></script>
</head>
electron-bridge
通过bridge 向 browserWindow
注入 electron 额外的 api
const {ipcRenderer: ipcRenderer, shell: shell, remote: remote, clipboard: clipboard} = require("electron"),
<!-- process 里面参数-->
const processStaticValues = _.pick(process, ["arch", "argv", "argv0", "execArgv", "execPath", "helperExecPath", "platform", "type", "version", "versions"]);
module.exports = (() => ({
ipcRenderer: ipcRenderer, // ipc renderer
shell: shell, // shell
remote: remote, //
clipboard: clipboard,
process: {
...processStaticValues,
hang: () => {
process.hang()
},
crash: () => {
process.crash()
},
cwd: () => {
process.cwd()
}
}
}));
代理设置
对于代理设置,一般有两种模式:
- PAC
- HTTP
PAC
直接输入地址 Protocol://IP:Port
HTTP
对于HTTP 模式下,有
- HTTP
- SOCKS4
- SOCKS5
输入 Protocol://IP:Port
系统版本比较(mac)
推荐使用semver
来。
多窗口管理
推荐electron-windows
,支持动态创建窗口。
类似vscode无缝升级安装
大体思路:先挂载 dmg, 找到挂载目录,在 mac 下是 /Volumes 目录下; 删除 /Applications 下的 app, 将 /Volumes 下的 app 拷贝到 /Applications 目录下; 再卸载 dmg; 重启应用即可,该方法可实现类似无缝更新的效果。
主要借助于 hdiutil
实现的、
主要分为六个步骤:
- which hdiutil
- hdiutil eject [/Volumes/appDisplayName latestVersion]
- hdiutil attach [latest Dmg Path]
- mv [local App Path] [temp dir]
- cp -R [latest app path] [local app path]
- hdiutil eject [/Volumes/appDisplayName latestVersion]
which hdiutil
查看 hdiutil
可执行文件 是否存在。
hdiutil eject [/Volumes/appDisplayName latestVersion]
卸载 [/Volumes/appDisplayName latestVersion] 下面的文件 。
hdiutil attach [latest Dmg Path]
安装 dmg 文件
mv [local App Path] [temp dir]
将 旧的本地app目录 移动到 tempDir 目录中。
cp -R [latest app path] [local app path]
将 latest app path 文件下的所有文件,都复制到原本的app 目录下面。
hdiutil eject [/Volumes/appDisplayName latestVersion]
再次 卸载 [/Volumes/appDisplayName latestVersion] 下面的文件
每一步下来,如果都成功了,则成功了。
实例代码。
const path = require("path");
const os = require('os');
const {waitUntil, spawnAsync} = require('../../utils');
const {existsSync} = require('original-fs');
const getMacOSAppPath = () => {
const sep = path.sep;
const execPathList = process.execPath.split(sep);
const index = execPathList.findIndex(t => 'Applications' === t);
return execPathList.slice(0, index + 2).join(sep);
};
module.exports = (async (app) => {
const {appDisplayName} = app.config;
const {latestVersion, latestDmgPath} = app.updateInfo;
//
const macOsAppPath = getMacOSAppPath();
// temp dir
const tempDir = path.join(os.tmpdir(), String((new Date).getTime()));
const appDisplayNameVolumesDir = path.join('/Volumes', `${appDisplayName} ${latestVersion}`);
//
const latestAppPath = path.join(appDisplayNameVolumesDir, `${appDisplayName}.app`);
// step 1 which hdiutil
// /usr/bin/hdiutil
try {
const hdiutilResult = await spawnAsync('which', ['hdiutil']);
if (!hdiutilResult.includes('/bin/hdiutil')) {
throw new Error('hdiutil not found');
}
} catch (e) {
app.logger.warn(e);
return {
success: false,
type: 'dmg-install-failed'
}
}
// step 2 hdiutil eject appDisplayNameVolumesDir
try {
await spawnAsync("hdiutil", ["eject", appDisplayNameVolumesDir])
} catch (e) {
e.customMessage = '[InstallMacOSDmgError] step2 volume exists';
app.logger.warn(e);
} finally {
const result = await waitUntil(() => !existsSync(latestAppPath), {
ms: 300,
retryTime: 5
});
if (!result) {
app.logger.warn('[InstallMacOSDmgError] step2 volume exists');
return {
success: false
}
}
}
//step 3 hdiutil attach latestDmgPath
try {
await spawnAsync('hdiutil', ['attach', latestDmgPath])
} catch (e) {
e.customMessage = '[InstallMacOSDmgError] step3 hdiutil attach error';
app.logger.warn(e);
} finally {
const result = await waitUntil(() => !existsSync(latestAppPath), {
ms: 300,
retryTime: 5
});
if (!result) {
app.logger.warn('[InstallMacOSDmgError] step3 hdiutil attach fail');
return {
success: false
}
}
}
// step 4 mv
try {
await spawnAsync('mv', [macOsAppPath, tempDir]);
} catch (e) {
e.customMessage = '[InstallMacOSDmgError] step4 mv to tmp path error';
app.logger.warn(e);
} finally {
const result = await waitUntil(() => !existsSync(tempDir), {
ms: 300,
retryTime: 5
});
if (!result) {
app.logger.warn('[InstallMacOSDmgError] step4 cp to tmp path fail');
return {
success: false,
type: "dmg-install-failed"
}
}
}
// step 5
try {
await spawnAsync('cp', ['-R', latestAppPath, macOsAppPath])
} catch (e) {
e.customMessage = '[InstallMacOSDmgError] step5 cp to app error';
app.logger.warn(e);
} finally {
const result = await waitUntil(() => !existsSync(macOsAppPath), {
ms: 300,
retryTime: 5
});
if (!result) {
app.logger.warn('[InstallMacOSDmgError] step5 cp to app fail');
await spawnAsync('mv', [tempDir, macOsAppPath]);
return {
success: false,
type: "dmg-install-failed"
}
}
}
// step 6
try {
await spawnAsync('hdiutil', ['eject', appDisplayNameVolumesDir])
} catch (e) {
e.customMessage = '[InstallMacOSDmgError] step6 hdiutil eject fail';
app.logger.warn(e);
}
return {
success: true
}
});
项目地址
为此,我将业务系统里面用到的各个客户端需要的功能,剥离了出来,新建了一个 template 方便新业务系统的开发。
编码不易,欢迎star