用 Electron 开发桌面效率工具
Electron 已经不算新技术,最早是 github 从 Atom 编辑器衍生出来的框架。通过编写 Javascript, HTML, CSS 能快速编译出跨系统的桌面 app。Electron 的出现使得作为前端开发工程师的我们输出范围更广。
分享最近用 Electron 做的一个基于番茄工作法的小应用,由于实现难度不大,市面上已经有非常多类似的app。我们尝试用 Electron 来实现一个。
最终效果预览:
工作法
番茄工作法的核心是将任务颗粒拆分到单位时间内(25分钟)可以完成,在这25分钟内专注在这个任务三,不允许做任何与任务无关的事,在任务任务完成之后可以短暂休息一会,再继续工作。
所以这个 app 的重点是让你创建任务,⏳ 25分钟,帮让 focus on 当前在做的任务。
站在巨人的肩膀上开发
尝试新技术的时候,不要从零开始学习如何搭建技术栈,先做出来,遇到问题再查。 Electron 社区有很多优秀的沉淀,工具,模板,组件,教程等等。
搜索 react 关键字,找到了 electron-react-boilerplate 这个样板库, 这个库已经集成了 react, redux, sass, flow, hmr webpack 等工具,同时准备好 electron-builder 打包工具,作为 electron 新手,我们优先选择开箱即用的工具,快速开启业务开发。
SVG 和 React Component
大概画了一下草图,准备进入开发阶段。考虑后面会用到 svg icon,先在 FlatIcon 上找些免费的图标,下载 SVG 文件。
通过 SVGR 在线工具导入 svg 内容生成 React Component 代码。(svgr 也有 cli 等工具)
用 SVG Component 的好处是可以在代码上更灵活地控制样式,相比 png 图标可交互性强,复用率高。
托盘和托盘弹窗
这个 app 启动的时候就隐藏在托盘菜单的一角,点击的时候显示 BrowserWindow,通过 Electron 提供的方法,可以获得托盘和托盘弹窗的 Bounds 信息,设置坐标位置。
// main.jsconst tray = new Tray(path.join(__dirname, '../static', 'tray.png'));const mainWindow = new BrowserWindow({ // ...others frame: false, resizable: true, transparent: true});const showWindow = () => { const { x, y } = getPositionFromActiveDisplay(); mainWindow.setPosition(x, y, true); mainWindow.show();};const getPositionFromActiveDisplay = () => { const trayBounds = tray.getBounds(); const windowBounds = mainWindow.getBounds(); const x = Math.round(trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2); const y = Math.round(trayBounds.y + trayBounds.height); return { x, y };};
图的三角是由前端代码绘制的,加上 frame 和 electron 背景色,应该长这样。
渲染线程和主线程
app 需要倒计时功能,告诉用户距离任务完成时间还有多久。Electron 有渲染进程和主线程,BrowserWindow 不可见的时候,渲染进程会尽量减少消耗,所以如果 Tick 在渲染进程的话,当 app 处于后台时会出现非常大的时间偏差。这里使用 Electron 提供的 ipcMain 和 ipcRenderer 做进程通信。
在主线程每秒发送 Tick 事件
// main.jsipcMain.once('store-ready', event => { const run = () => { setTimeout(() => { run(); event.sender.send('tick'); }, 1000); }; run();});复制代码
渲染进程就收事件并将 dispatch TICK action。
// app/index.jsconst store = configureStore({ tasks: electronStore.getTasks()});ipcRenderer.send('store-ready');ipcRenderer.on('tick', () => { store.dispatch({ type: TICK });});复制代码
redux store 里面判断当前执行的任务计算倒计时时间。
switch (action.type) { case TICK: return { ...state, rows: state.rows.map(task => task.id === state.currentId ? { ...task, remain: Math.max(task.remain - 1, 0) } : task ) };复制代码
数据持久存储
数据持久化有很多种方案,因为是前端浏览器,我们可以选择 localStorage, Cookie,indexDB 等等。考虑可靠性,持久化以及存储空间,还可以通过 Electron 写文件的方式,把数据写入到应用路径下。这样即使 app 被卸载了,只要数据没被清空,用户数据还在。
通过 Electron app getPath 可以获得应用存储路径
import { app } from 'electron';app.getPath('userData');复制代码
mac 下应用 app 的路径是 /Users/user/Library/Application Support/focus。更简单的方式可以直接用开源库 electron-store,以 key-value 的格式存储 json 文件。
{ "tasks": { "rows": [ { "name": "任务名称