这其实是一个技术贴,真的!
18 年一晃都过去 10% 了,怎么也得发个文章了。
可是一想最近也没啥干货能拿出来啊,干脆正面撸(copy)一个小项目做个入门充数。
准备工作
- 一个小项目 -- sound-machine
- Electron 文档
分析
这篇译文中讲的非常详细了,核心功能抽离出来就是:
- 有那么一堆按钮,点击发出声音。
- 有个简陋的顶部菜单,就一个设置,一个退出,所以这里有一个设置的 window。
- 全局快捷键,可以在设置界面简单配置。
- 系统托盘菜单
- ...
在译文中使用的是强大的 vanillajs 构建的项目,所以,我们用 React 试试。
环境
- node -- 我的是 8.9.3
- typescript -- 用的是 2.7.1
- create-react-app -- 用的是 1.5.1
开始撸
1. 创建项目
create-react-app sound-machine --scripts-version=react-scripts-ts
// next
yarn add --dev electron react-router-dom
create-react-app 以下简称 CRA,已经帮你把大部分的活儿都干好了。
我们继续加点东西就可以开始啦,首先我们加入路由配置,以及 code splitting。
你可以使用 react-loadable、asyncComponent等,也可参考异步加载组件。
<Router>
<Switch>
<Route exact={true} path="/" component={AsyncApp}/>
<Route exact={true} path="/set" component={AsyncSetComponent}/>
<Route component={AsyncApp}/>
</Switch>
</Router>
我们其实就两个路由就ok了,一个主页,一个设置界面的。
2. electron
我们将 main 文件放进 public 文件夹中,main 文件是 electron 的主进程文件。
其核心代码:
// ...
function createWindow () {
// 创建浏览器窗口。
win = new BrowserWindow({width: 368, height: 700})
// 然后加载应用的 index.html。
win.loadURL( index.html);
// 打开开发者工具。
win.webContents.openDevTools()
// 当 window 被关闭,这个事件会被触发。
win.on('closed', () => {
// 取消引用 window 对象,如果你的应用支持多窗口的话,
// 通常会把多个 window 对象存放在一个数组里面,
// 与此同时,你应该删除相应的元素。
win = null
})
}
// Electron 会在初始化后并准备
// 创建浏览器窗口时,调用这个函数。
// 部分 API 在 ready 事件触发后才能使用。
app.on('ready', createWindow)
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持激活。
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if (win === null) {
createWindow()
}
})
// ...
这里有一些问题需要注意:
-
如果设置了窗口
win = new BrowserWindow({width: 368, height: 700, resizable: false, frame: false})
请注意, 如果您已使整个窗口draggable, 则必须将按钮标记为 non-draggable, 否则用户将无法单击它们。
细节参见 无边框窗口。
-
为了实现 HRM,可以用
win.loadURL('http://localhost:3000/');
但是当你打包的时候,你的入口应该是
build/index.html
而且 electron 基于 file: 协议的,所以我们可以这样写
win.loadURL( isDev ? 'http://localhost:3000/' : `file://${path.join(__dirname, '../build/index.html')}`);
-
package.json 文件中需要加入:
"main": "public/main.js", // 这个是 main.js 路径 "homepage": "./", // 这个是为了build之后使用相对路径去匹配资源
好,到这一步使用 "electron ." 运行一下,看能不能跑起来。"electron ." 这个命令可以配置在 scripts 中。
3. 先写主面板
主页面很简单,就是几个按钮,点击之后可以发出声音,随便写几个按钮就好了。
然后是写一个设置按钮和退出的按钮,这里会用到 remote,可以方便的进行通信。
比如退出事件:
handleClose(){
remote.app.quit();
}
这里需要注意的一个问题就是在使用 require('electron') 的时候会报错的。
你可以在这里找到解决办法。
其实就是使用 window.require(''),原因解决办法也有解释。当然,这里用的是 ts。
所以还有可能报错说 'Window ...'怎么的,你就全局声明一下:
declare global {
interface Window {
require: any;
}
}
4. 设置全局快捷键
快捷键也是一个重要的功能。我们这里使用全局的快捷键,比如你不需要在获取屏幕焦点的时候按下 "Ctrl+Shift+1" 就会发出一个声音。
其实实现的方式很简单,我们可以在 app 的 ready 状态中增加一个 setGlobalShortcuts 的方法。
// 基于本地配置文件重写设置全局快捷键的方法
const setGlobalShortcuts = () => {
globalShort.unregisterAll(); // 先清除一次
globalShort.register('CommandOrControl+Shift+1', () => {
win.webContents.send('global-short', 0);
});
globalShort.register('CommandOrControl+Shift+2', () => {
win.webContents.send('global-short', 1);
});
// 显示作者信息
globalShort.register('CommandOrControl+Shift+3', () => {
dialog.showMessageBox({
type: 'info',
title: 'sound-machine made by 波比小金刚',
message: 'a simple demo for electron+react',
detail: 'inspired by http://get.ftqq.com/7870.get'
})
});
}
这个方法其实就是通过 globalShort 的 API 去实现全局快捷键的注册,然后通过 webContents去发送一个信号,并且传递参数。
然后在你的组件代码中去监听这个信号,做出响应的模拟点击。就是典型的发布-订阅模式。
// 监听全局键盘事件
ipcRenderer.on('global-short', (event: any, param: number) => {
let e = new MouseEvent('click');
btns[param].dispatchEvent(e);
})
5. 设置界面
通过路由可以跳转到设置界面,设置界面很简单,一个退出,一个快捷键选择。就这两个核心功能点。
sound-machine 在这个参考项目中使用的是 nconf 模块实现配置文件的读写,我直接用的 fs 模块实现了。
本地新增一个 setting.json 文件,里面就很简单的加入初始化数据:
{"data":["CommandOrControl","Alt"]}
然后在设置全局快捷键的方法中加入配置文件的读取:
// 基于本地配置文件重写设置全局快捷键的方法
const setGlobalShortcuts = () => {
globalShort.unregisterAll(); // 先清除一次
let file = JSON.parse(fs.readFileSync('./setting.json', 'utf-8'));
let pre = file.data.length === 0 ? '' : file.data.join('+')+'+';
// 获取存在本地的配置
globalShort.register(pre + '1', () => {
win.webContents.send('global-short', 0);
});
globalShort.register(pre + '2', () => {
win.webContents.send('global-short', 1);
});
// 显示作者信息
globalShort.register(pre + '3', () => {
dialog.showMessageBox({
type: 'info',
title: 'sound-machine made by 波比小金刚',
message: 'a simple demo for electron+react',
detail: 'inspired by http://get.ftqq.com/7870.get'
})
});
}
设置界面上获取文件,读出数据渲染到界面,然后每次点击的时候去更新文件。再通知一下主进程快捷键变了。
handleClick(text: string){
// 遍历获取check了的快捷键
let nodes = this.inputEl.parentNode.parentNode.childNodes, arr: string[] = [];
for(let i = 0, node; node = nodes[i++];){
node.childNodes[0].checked ? arr.push(node.childNodes[0].attributes['data-ctrl'].value) : ''
}
let shortcuts = {
data: arr
}
// 写入文件
fs.writeFileSync('./setting.json', JSON.stringify(shortcuts));
// 告诉主进程
ipcRenderer.send('shortcuts-changed');
}
主进程订阅这个消息:
// 订阅快捷键改变事件
ipcMain.on('shortcuts-changed', () => {
setGlobalShortcuts();
})
ok, 现在就实现了全局快捷键的配置。
6. 打包
其实核心功能就差不多这些,我们现在可以打包了。
打包可以使用 electron-builder, 操作很简单,只是打包的时候可能各种 timeout,所以建议 VPN 吧!
需要配置一个 build,比如:
"build": {
"appId": "com.example.${name}",
"files": [
"build/**/*",
"node_modules/**/*",
"package.json"
],
"directories": {
"buildResources": "assets"
}
}
然后根据自己的系统和需求配置不同的打包命令,上面的参数也有很多 option,可以结合官方的文档自己配置一番。
结语
首先看看效果,声音gif没有哦~
整个项目并不复杂,只是撸一发之后对 electron 有一个比较直观的认识,对部分知识点有个了解,为你深入的掌握 electron 打下基础。
所以叫做入门嘛~
代码地址
过年了,赏个 star 呀~