JoyLau's Blog

JoyLau 的技术学习与思考

容器启动时初始化数据的方法

  1. 编写好脚本,支持 .sql;.sh;.sql.gz
  2. 容器启动时, 将脚本挂载到容器的 /docker-entrypoint-initdb.d 目录下即可

可就是这么简单的操作,我却没有成功…

注意

该方法只在初始化数据库的时候起作用,意思是,当你想把 mariadb 的数据目录 /var/lib/mysql 挂载到本地盘上,那么 该目下有文件时,放置的脚本将不会执行

Electron 自动更新的方法

  1. 使用 Electron 自己提供的 autoUpdater 模块
  2. 使用更新服务器
  3. 自己实现自动更新逻辑

为什么说经过了一系列的折腾呢, 因为前 2 中方式都没有解决我的问题,最后我是自己实现了自动更新的逻辑
没有解决我的问题是因为我需要兼顾到 mac 平台和 Windows 平台,然而 mac 平台比较麻烦,代码需要签名
我自己亲测方式一和方式二在 mac 平台上都需要代码签名, 而签名代码需要注册苹果开发者账号,需要付年费
于是这 2 条路就走不通了

最后我决定自己实现更新的逻辑

更新逻辑分析

  1. 自动触发或手动触发软件更新检查
  2. 服务器版本号大于本地版本才出现更新提示
  3. 对于更新,无非就是卸载之前的版本,安装新下载的安装包
  4. 软件的打包我选择的是 Electron Builder, 分别打成 dmg , setup.exe , app.zip
  5. 更新的时候先从服务器下载新版本
  6. 下载完成后对于安装包的安装分平台来说

Windows 下的更新

  1. Windows 下的安装包是 exe 可执行文件,安装包本身是有处理逻辑在里面的
  2. 于是我们只需要将安装包下载到临时目录,然后再软件里打开它,再退出软件,剩下的安装步骤交给用户
  3. 有一点需要注意的是,NSIS 的新安装包在安装前会自动卸载掉之前的版本,不过不会提示用户,我们可以在 nsis 脚本里加一个提示

MacOS 下的更新

  1. 相比于 Windows 下的安装包, macOS 下的 dmg 安装包就没有什么逻辑了,直接打开,然后将 app 文件拖到 Applications 目录中即可完成安装
  2. 于是有 2 中方法可选
  3. 一. 挂载 dmg, 找到挂载目录,在 mac 下是 /Volumes 目录下; 删除 /Applications 下的 app, 将 /Volumes 下的 app 拷贝到 /Applications 目录下; 再卸载 dmg; 重启应用即可,该方法可实现类似无缝更新的效果
  4. 二. 和方法一一个道理,只不过不是挂载 dmg 来查找 app, 直接解压 app.zip 压缩文件即可得到 app ,在使用相同的方式覆盖即可.

软件的版本控制

可以采取一个 json 文件来记录个版本的更新记录, 这里给个参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
{
"version": "1.1.0",
"force": false,
"time": "2019-09-14",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 修复若干 BUG,稳定性提升"
]
},
{
"version": "1.0.0",
"force": false,
"time": "2019-09-01",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 全新界面,主体功能完成"
]
}
]

代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import $ from 'jquery';
import semver from 'semver';
import request from 'request';
import progress from 'request-progress';

//global.fs = require('fs');
//global.cp = require('child_process');
const fs = window.fs;
const cp = window.cp;
const electron = window.electron;
const {app, shell} = electron.remote;

state = {
check: true,
latest: {},
// wait,download,install,error
update: 'wait',
downloadState: {}
};

// 检查更新
$.ajax({
url: appConfig.updateCheckURL,
timeout: 10000,
type: 'GET',
cache:false,
success: function (data) {
let latest = data[0];
if(semver.satisfies(latest.version, '>' + app.getVersion())){
if (latest.force) {
that.updateVersion();
}
}
},
complete: function (XMLHttpRequest, status) {
that.setState({
check: false
})
}
});


updateVersion(){
let that = this;
const platform = osInfo.platform();
try {
const downloadUrl = platform === 'darwin' ? this.state.latest.download.dmg : platform === 'win32' ? this.state.latest.download.winSetup : '';
if (downloadUrl === '') return;

const downloadUrlArr = downloadUrl.split("/");

const filename = downloadUrlArr[downloadUrlArr.length-1];

const savePath = osInfo.tmpdir() + '/' + filename;

const _request = request(downloadUrl);
progress(_request, {
// throttle: 2000, // Throttle the progress event to 2000ms, defaults to 1000ms
// delay: 1000, // Only start to emit after 1000ms delay, defaults to 0ms
// lengthHeader: 'x-transfer-length' // Length header to use, defaults to content-length
})
.on('progress', function (state) {
// The state is an object that looks like this:
// {
// percent: 0.5, // Overall percent (between 0 to 1)
// speed: 554732, // The download speed in bytes/sec
// size: {
// total: 90044871, // The total payload size in bytes
// transferred: 27610959 // The transferred payload size in bytes
// },
// time: {
// elapsed: 36.235, // The total elapsed seconds since the start (3 decimals)
// remaining: 81.403 // The remaining seconds to finish (3 decimals)
// }
// }
that.setState({downloadState: state})
})
.on('error', function (err) {
that.setState({
downloadState:{
error: true
}
})
})
.on('end', function () {
if (that.state.update === 'error') return;
that.setState({
update: 'install',
});

setTimeout(function () {
if (platform === 'darwin'){
const appName = pjson.build.productName;
const appVersion = app.getVersion();
console.info(appName,appVersion);
// 挂载
cp.execSync(`hdiutil attach '${savePath}' -nobrowse`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 覆盖原 app
cp.execSync(`rm -rf '/Applications/${appName}.app' && cp -R '/Volumes/${appName} ${appVersion}/${appName}.app' '/Applications/${appName}.app'`);

// 卸载挂载的 dmg
cp.execSync(`hdiutil eject '/Volumes/${appName} ${appVersion}'`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 重启
app.relaunch();
app.quit();
}

if (platform === 'win32') {
shell.openItem(savePath);
setTimeout(function () {
app.quit();
},1500)
}
},2000)
})
.pipe(fs.createWriteStream(savePath));

that.setState({update:'download'});
} catch (e) {
console.info(e);
that.setState({
update: 'error',
});
}
}

打包的资源无法包含 build 目录

1
2
3
4
5
6
"files": [
"**/*",
"build/",
"!build/static/js/*.js.map",
"!src/"
],

同时该配置也可防止源码被打包进去,

查看打包后的目录结构

"asar": false,

引入外部文件

1
2
3
4
5
6
"extraResources": [
{
"from": "./LICENSE",
"to": "./../LICENSE.txt"
}
],

定义安装包输出目录

1
2
3
"directories": {
"output": "dist"
},

Windows 环境下打出 32 位和 64 位二合一包

1
2
3
4
5
6
7
8
9
10
11
"win": {
"target": [
{
"target": "nsis",
"arch": [
"ia32",
"x64"
]
}
]
},

打出的 mac 包写入数据到 Info.plist 文件

1
2
3
4
5
6
7
8
9
10
11
12
"mac": {
"extendInfo": {
"URL types": [
{
"URL identifier": "Joy Security",
"URL Schemes": [
"joy-security"
]
}
]
}
},

NSIS 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"nsis": {
"oneClick": false, // 一键安装
"perMachine": true, // 为所有用户安装
"allowElevation": true, // 允许权限提升, 设置 false 的话需要重新允许安装程序
"allowToChangeInstallationDirectory": true, // 允许更改安装目录
"installerIcon": "./public/icons/win.ico",
"uninstallerIcon": "./public/icons/win_uninstall.ico",
"installerHeaderIcon": "./public/icons/win.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Joy Security",
"license": "./LICENSE",
"include": "./public/nsis/installer.nsh" // 包含的脚本
}

NSIS 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
!macro customHeader

!macroend

!macro preInit

!macroend

!macro customInit
# guid=7e51495b-3f4d-5235-aadd-5636863064f0
ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{7e51495b-3f4d-5235-aadd-5636863064f0}" "UninstallString"
${If} $0 != ""
MessageBox MB_ICONINFORMATION|MB_TOPMOST "检测到系统中已安装本程序,将卸载旧版本" IDOK
# ExecWait $0 $1
${EndIf}
!macroend

!macro customInstall

!macroend

!macro customInstallMode
# set $isForceMachineInstall or $isForceCurrentInstall
# to enforce one or the other modes.
#set $isForceMachineInstall
!macroend

NSIS 引入 license 文件包含中文的问题

当引入的 license 文件里有中文时, 在 Windows (中文操作系统) 平台下打包需要 GBK 编码, 在 macOS 下,GBK 编码会直接报错,需要修改为 UTF-8 编码

什么是套娃图标

可能见过一种套娃的玩具,就是一个大的套着一个小的,每一个玩具的形状颜色都一样,只是大小比例不一样,套娃图标也是这个意思

什么工具

需要这么一个工具, IconFX : 下载

IconFX

制做一组至少有 256256 (此外还有 128128 , 9696, 6464, 4848,3232,16*16)的一套图标, Windows 下格式为 icon, Mac 下格式为 icns

怎么制作

使用 PS 制作一张图片或者下载一张图片,按照下面的步骤完成所有大小的图标创建,之后保存.

图像 — 从图像创建 Windows 图标

IconFX-make

MacOS 也是同样的道理

什么是 URL Scheme 协议

个人理解为注册一种协议来实现应用间的跳转

Windows 上的实现

Windows 上是通过注册表实现的

通过在 HKCR (HKEY_CALSSES_ROOT) 添加一条注册表记录

Win Registry

其中 command 的命令即为要执行的命令,注意后面要加一个参数 "%1"

Mac 上的实现

在应用里显示包内容,使用 xcode 查看 Info.plist 找到 URL types – URL Schemes 里添加一项

Mac Info.plist

Electron 的实现

1
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [`${__dirname}`]);

这一句话即可完成 Windows 下和 macOS 下的协议注册,只不过需要应用启动后才可注册成功,就是说如果安装过后不打开的话,无法通过协议来唤醒应用,解决方式我们后面再讲

第一个参数为协议的名称, 第二个参数为执行的命令,第三个参数为所传字符串参数数组

在 Windows 环境下最后一项需要带上当前的项目路径,否则的话在开发模式下会打不开 electron 应用,打包完成后不会存在这个问题, mac 上也不会存在这个问题

Electron 上协议参数的处理

参数的处理分 2 中情况

  1. 新打开的窗口
  2. 打开的第二个实例

对于新打开的窗口:
使用 let argv = process.argv; 来获取进程参数,得到的是一个数组,如果做够一项包含我们的协议,则需要根据自己的字符串规则来进行处理

1
2
3
4
let argv = process.argv;
if (argv[argv.length - 1].indexOf(PROTOCOL + "://") > -1) {
//.....
}

对于打开的第二个实例:
windows 上监听的事件是 second-instance, mac 上监听的事件是 open-url, 2 个事件传入参数还不一样, Windows 下传入的参数是字符串数组,mac 传入的参数是字符串,都包含了协议名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 当运行第二个实例时,主动对焦
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
win.show();

let message = handleArgv(commandLine);
processSend(message);
}
});

// macOS
app.on('open-url', (event, urlStr) => {
if (win) {
win.showInactive();
let message = handleArgv(urlStr);
processSend(message);
} else {
global.shareObject.message = handleArgv(urlStr);
global.shareObject.isSend = true;
}

});


function processSend(message) {
global.shareObject.message = message;
win.webContents.send('ch-1', 'send');
}

function handleArgv(argv) {
let urlObj = [];
if (Array.isArray(argv)) {
urlObj = argv[argv.length - 1].replace(PROTOCOL + "://", "").split("_");
} else {
urlObj = argv.replace(PROTOCOL + "://", "").split("_");
}
return urlObj.length >= 2 ? {sessionId: urlObj[0], url: urlObj[1], macInfo: os.networkInterfaces()} : {};
}

浏览器判断 scheme 协议是否存在

使用 setTimeout, 如果超时未打开的话则说明协议不存在

1
2
3
4
5
let downloadURL = "http://xxxx";
window.location = "joy-security://xxxxxx_xxxxxxx";
setTimeout(function() {
window.location = downloadURL;
},1000)

Electron 只启动一个实例

使用 app.requestSingleInstanceLock()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
app.quit()
} else {
app.on('ready', createWindow);

app.on('window-all-closed', () => {
app.quit();
});

app.on('activate', () => {
if (win == null) {
createWindow();
}
});
}

Electron 不显示菜单栏

经过实测
Menu.setApplicationMenu(null);
在 Windows 环境下没有菜单栏, 在 MAC 系统上开发模式下有菜单栏

正确的解决方式是
Menu.setApplicationMenu(Menu.buildFromTemplate([]));

注册快捷键

electron 自带的注册快捷键的功能函数是 globalShortcut, 这个是全局的快捷键,就是说焦点不在当前程序上也能触发快捷键
我这里使用的是一个第三方的组件 electron-localshortcut

1
2
3
4
5
6
7
electronLocalshortcut.register(win, 'F12', function () {
win.webContents.isDevToolsOpened() ? win.webContents.closeDevTools() : win.webContents.openDevTools();
});

electronLocalshortcut.register(win, 'F5', function () {
win.reload();
});

主线程和渲染线程之间的通信

这里使用的是 ipcMain 和 ipcRenderer
渲染进程使用ipcRenderer.send发送异步消息,然后使用on事件监控主进程的返回值。主进程使用on事件监听消息,使用event.sender.send返回数据

App.js:

1
2
3
4
5
const {ipcRenderer} = require('electron')
ipcRenderer.send('asynchronous-message', 'ping')
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})

main.js

1
2
3
4
5
const {ipcMain} = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong')
});

渲染进程使用ipcRenderer.sendSync发送同步消息。主进程使用on事件监控消息,使用event.returnValue返回数据给渲染进程。返回值在渲染进程中,就直接体现为ipcRenderer.sendSync的函数返回值

主线程如何给渲染线程发送消息

上面的示例没有说主线程如何对小渲染线程发送消息,应该这样做:

1
win.webContents.send('ch-1', 'send');

渲染进程和渲染进程如何互发消息

  1. 渲染进程的页面自己处理
  2. 通过主线程进行中间转换

渲染线程如何使用 electron 的功能

渲染窗口添加配置:

1
2
3
4
webPreferences: {
nodeIntegration: true, // 开启 node 功能
preload: path.join(__dirname, './public/renderer.js')
}

添加 renderer.js

1
global.electron = require('electron')

渲染进程的页面使用:

1
2
const electron = window.electron;
electron.xxxx

主线程和渲染进程如何共享对象

不需要引入任何包,直接在主线程使用 global

1
2
3
4
// 共享对象
global.shareObject = {
osInfo: os
};

渲染进程获取信息: let osInfo = electron.remote.getGlobal(‘shareObject’).osInfo;

主线程修改对象: global.shareObject.osInfo = message;

渲染线程修改对象: electron.remote.getGlobal(‘shareObject’).osInfo = null;

区分开发模式还是生产模式

建议使用 app.isPackaged

通过协议打开第二个实例的情况下触发的事件

Windows 环境下:

1
2
3
4
5
6
7
8
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 当运行第二个实例时,主动对焦
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
win.show();
}
});

Mac 环境下:

1
2
3
4
5
6
// macOS
app.on('open-url', (event, urlStr) => {
if (win) {
win.showInactive();
}
});

开发环境和生成环境加载不同的页面

1
2
3
4
5
 if (app.isPackaged) {
win.loadURL(`file://${__dirname}/build/index.html`);
} else {
win.loadURL('http://localhost:3000');
}

添加多页面配置

  1. npm run eject
  2. 修改 webpack.config.js

entry 修改:
这里我加了一个 update.html 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
entry: {
index: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
update: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + '/update.js',
].filter(Boolean),
},

output 修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name]bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},

注意修改其中的 filename

HtmlWebpackPlugin 修改:
新增一个 HtmlWebpackPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
chunks: ["index"]
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
chunks: ["update"],
filename: "update.html"
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),

在 public 目录里添加 update.html, 内容照抄 index.html 文件即可;
在 src 目录下添加 update.js 文件:

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Update from './page/Update';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<Update />, document.getElementById('root'));
serviceWorker.register();

之后, http://localhost:3000/update.html 即可访问; 如果想加个路径,直接修改 HtmlWebpackPlugin 里的 filename, 例如 filename: "index/update.html"
就可以 使用 http://localhost:3000/index/update.html 来访问

引入 src 目录以外的文件报错

例如需要引入 public 目录下的图片,就会报错,此时,注释掉

1
// new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),

这一行,重启即可.

步骤

创建 create-react-app-antd 项目

  1. git clone https://github.com/ant-design/create-react-app-antd
  2. npm install
  3. 将 webpack 所有内建的配置暴露出来, npm run eject, 如果发现错误,看下 package.json 里 eject 的脚本是不是为 react-scripts eject
  4. 修改 config-overrides.js
1
2
3
module.exports = function override(config, env) {
return config;
};
  1. 修改 webpack.config.js 里的 module.rules.oneOf 支持 css 和 less, 添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    {
    test: /\.(css|less)$/,
    use: [
    require.resolve('style-loader'),
    {
    loader: require.resolve('css-loader'),
    options: {
    importLoaders: 1,
    },
    },
    {
    loader: require.resolve('postcss-loader'),
    options: {
    // Necessary for external CSS imports to work
    // https://github.com/facebookincubator/create-react-app/issues/2677
    ident: 'postcss',
    plugins: () => [
    require('postcss-flexbugs-fixes'),
    autoprefixer({
    browsers: [
    '>1%',
    'last 4 versions',
    'Firefox ESR',
    'not ie < 9', // React doesn't support IE8 anyway
    ],
    flexbox: 'no-2009',
    }),
    ],
    },
    },
    {
    loader: require.resolve('less-loader'),
    options: { javascriptEnabled: true }
    },
    ],
    }
  2. 修改 start.js 注释掉下面代码关闭项目启动自动打开浏览器

1
// openBrowser(urls.localUrlForBrowser);
  1. package.json 添加 "homepage": "." ,防止打包后的静态文件 index.html 引入 css 和 js 的路径错误

  2. App.less 修改为 @import '~antd/dist/antd.less';

添加 electron

  1. package.json 添加 "main": "main.js", 和 electron 依赖
1
2
3
4
5
6
{
"main": "main.js",
"devDependencies": {
"electron": "^6.0.7"
}
}
  1. 创建 main.js,添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const {app, BrowserWindow, Menu} = require('electron');

let win;

let windowConfig = {
width: 800,
height: 600,
title: "Joy Security",
};

let menuTemplate = [{
label: 'Joy Security',
submenu: [{
label: '退出',
role: 'quit'
}, {
label: `关于 ${windowConfig.title}`,
role: 'about'
}]
}];


app.on('ready', createWindow);

app.on('window-all-closed', () => {
app.quit();
});

app.on('activate', () => {
if (win == null) {
createWindow();
}
});


function createWindow() {
// 隐藏菜单栏,兼容 MAC
Menu.setApplicationMenu(Menu.buildFromTemplate([]));

win = new BrowserWindow(windowConfig);

win.loadURL('http://localhost:3000');

win.on('close', () => {
//回收BrowserWindow对象
win = null;
});

win.on('resize', () => {
// win.reload();
});

}

  1. package.json 更改脚本
1
2
3
4
5
6
7
{
"scripts": {
"react-start": "node scripts/start.js",
"eletron-start": "electron .",
"react-build": "node scripts/build.js",
}
}
  1. 启动时先 react-start 再 eletron-start 即可看到效果
0%