Electron 已经不算新技术, 最早是 GitHub 从 Atom 编辑器衍生出来的框架. 通过编写 JavaScript, html, CSS 能快速编译出跨系统的桌面 App.Electron 的出现使得作为前端开发工程师的我们输出范围更广.
分享最近用 Electron 做的一个基于番茄工作法的小应用, 由于实现难度不大, 市面上已经有非常多类似的 App. 我们尝试用 Electron 来实现一个.
最终效果预览:
工作法
番茄工作法的核心是将任务颗粒拆分到单位时间内 (25 分钟) 可以完成, 在这 25 分钟内专注在这个任务三, 不允许做任何与任务无关的事, 在任务任务完成之后可以短暂休息一会, 再继续工作.
所以这个 App 的重点是让你创建任务, 25 分钟, 帮让 focus on 当前在做的任务.
站在巨人的肩膀上开发
尝试新技术的时候, 不要从零开始学习如何搭建技术栈, 先做出来, 遇到问题再查. Electron 社区 https://electronjs.org/community 有很多优秀的沉淀, 工具, 模板, 组件, 教程等等.
搜索 react 关键字, 找到了 这个样板库, 这个库已经集成了 react, redux, Sass, flow, hmr webpack 等工具, 同时准备好 electron-builder 打包工具, 作为 electron 新手, 我们优先选择开箱即用的工具, 快速开启业务开发.
SVG 和 React Component
大概画了一下草图, 准备进入开发阶段. 考虑后面会用到 svg icon, 先在 FlatIcon https://www.flaticon.com 上找些免费的图标, 下载 SVG 文件.
通过 SVGR https://www.smooth-code.com/open-source/svgr/ 在线工具导入 svg 内容生成 React Component 代码.(svgr 也有 cli 等工具)
用 SVG Component 的好处是可以在代码上更灵活地控制样式, 相比 PNG 图标可交互性强, 复用率高.
托盘和托盘弹窗
这个 App 启动的时候就隐藏在托盘菜单的一角, 点击的时候显示 BrowserWindow, 通过 Electron 提供的方法, 可以获得托盘和托盘弹窗的 Bounds 信息, 设置坐标位置.
- // main.JS
- const 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.JS
- ipcMain.once('store-ready', event => {
- const run = () => {
- setTimeout(() => {
- run();
- event.sender.send('tick');
- }, 1000);
- };
- run();
- });
渲染进程就收事件并将 dispatch TICK action.
- // App/index.JS
- const 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": "任务名称",
- "id": "91ac7f05-76f4-46ea-addb-f392a3a29b54",
- "created_at": 1553398427806,
- "plan": 1500,
- "remain": 0,
- "done": true
- }
- ],
- "currentId": "91ac7f05-76f4-46ea-addb-f392a3a29b54"
- }
- }
倒计时 UI
有些样式可能用 CSS 实现难度较大, 而用 svg 的方式实现起来非常简单. 比如倒计时 UI, 路径圆角和路径长度用 CSS 实现复杂度较高. 可以在 Sketch 上直接绘制处理, 导出成 svg, 直接通过 react 代码控制.
- export default function(props: Props) {
- const offset = percentage * totalLength;
- const cx =
- Math.cos(percentage * Math.PI * 2 - Math.PI * 0.5) * radius + radius;
- const cy =
- Math.sin(percentage * Math.PI * 2 - Math.PI * 0.5) * radius + radius;
- return (
- <svg>
- ...others
- <circle
- id="path-1"
- cx={cx}
- cy={cy}
- r="32"
- fill="white"
- style={{ transition: '1s linear' }}
- />
- <path
- ...others
- strokeLinecap="round"
- strokeDasharray={totalLength}
- strokeDashoffset={offset}
- style={{ transition: '1s linear' }}
- />
- </svg>
- );
- }
临界状态判断
App 在任务时间结束时需要有 Notification, 由于的 Tick 设计, 判断任务是否完成可以放在 redux middleware 上.
- // middlewares/tasks
- export default ({ getState }) => next => action => {
- if (typeof action === 'object' && action.type === 'TICK') {
- const beforeCount = getTimeEndTaksCount(getState);
- next(action);
- const afterCount = getTimeEndTaksCount(getState);
- if (beforeCount !== afterCount) {
- new Notification('Focus, 任务完成了吗?');
- }
- } else {
- next(action);
- }
- };
经过一个 Tick action 之后, 判断任务完成数是否有变化, 并使用 HTML5 Notification 通知用户.
Travis CI
功能开发完毕之后, 使用 electron-builder 进行打包发布, 构建之后推到 GitHub release 下, 用户可以直接在这下载到最新的包.
同样的, boilerplate 已经准备好 .travis.YAML 文件, 唯一需要我们操作的是在 https://github.com/settings/tokens/new 上生成 token, 在 https://www.travis-ci.org/ 构建之前配置 Environment Variables,GH_TOKEN
tirgger build, 成功之后就能看到构建成功过的包, 下载使用
总结
使用 Electron, 前端开发者可以使用自己的武器构建跨系统的桌面端应用, 而且不用学习其他技术, 缺点是一个小小的功能打包完的体积是 70M.
这个 App 从有想法到最终实现比预期的简单, 感兴趣的同学也可以自己 DIY 些小玩意儿. 完整的代码在 GitHub 上 https://github.com/HelKyle/focus , 欢迎体验, 同时也欢迎 star~
来源: https://juejin.im/post/5c966596e51d454d42032ce9