不知道起什么标题,就叫新年快乐吧~

206 阅读3分钟

这其实是一个技术贴,真的!

18 年一晃都过去 10% 了,怎么也得发个文章了。

可是一想最近也没啥干货能拿出来啊,干脆正面撸(copy)一个小项目做个入门充数。

准备工作

分析

这篇译文中讲的非常详细了,核心功能抽离出来就是:

  • 有那么一堆按钮,点击发出声音。
  • 有个简陋的顶部菜单,就一个设置,一个退出,所以这里有一个设置的 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-loadableasyncComponent等,也可参考异步加载组件

<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()
  }
})
// ...

这里有一些问题需要注意:

  1. 如果设置了窗口

    win = new BrowserWindow({width: 368, height: 700, resizable: false, frame: false})
    

    请注意, 如果您已使整个窗口draggable, 则必须将按钮标记为 non-draggable, 否则用户将无法单击它们。

    细节参见 无边框窗口

  2. 为了实现 HRM,可以用

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

    但是当你打包的时候,你的入口应该是

    build/index.html

    而且 electron 基于 file: 协议的,所以我们可以这样写

    win.loadURL( isDev ? 'http://localhost:3000/' : `file://${path.join(__dirname, '../build/index.html')}`);
    
  3. 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 呀~