Electron+Vue定制你的专属番茄闹钟

499 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

技术栈

Electron+Vue+Vue-Router+Less

完整项目代码

vue-electron-test

Home界面编写

user-select

客户端同网页不同,为了尽可能的模拟客户端,我们需要禁用用户光标选择。

## App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>
<style lang="less" scoped>
#app {
  user-select: none;
}
</style>

创建页面

本项目会用到两个页面,分别是 home 页面和 setting 页面。

首先,我们在views文件夹下分别创建两个文件home.vue和setting.vue。

然后需要改造一下我们electron的入口文件background.js里,创建窗口的逻辑。

// 将窗口的创建写成一个工厂函数。入参是BrowserWindowOptions & { url:string }
const windowFactory = async (options) => {
  const { url = '', ...restOptions } = options;
  console.log(url);
  const newWindow = new BrowserWindow(Object.assign({
    width: 300,
    height: 300,
    frame: false, // 无边框窗口
    alwaysOnTop: true, // 需要置顶
    resizable: false, // 不允许用户手动调整窗口大小
    webPreferences: {
      enableRemoteModule: true,
      nodeIntegration: true,
      nodeIntegrationInWorker: true, // 是否在Web工作器中启用了Node集成
      contextIsolation: false, // electron为12x版本新增此行
      devTools: true, // 是否开启 DevTools
    }
  }, restOptions))
  if (process.env.WEBPACK_DEV_SERVER_URL) {
    newWindow.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#/${url}`);
    if (!process.env.IS_TEST) newWindow.webContents.openDevTools();
  } else {
    createProtocol('app');
    newWindow.loadFile('./index.html', {
      hash: url // 参数拼接
    });
  }
  return newWindow;
}

随后的setting页面也需要这个工厂函数进行创建。

拖拽窗口

由于我们的窗口被设置成了透明且无边框,因此我们就只能自己实现窗口的拖动效果。

只要给需要设置可拖动的节点设置 -webkit-app-region: drag 就可以了。

需要注意的是,如果我们在设置了颗拖动的容器中有按钮之类的需要能触发点击事件的,需要显式的给这些按钮标记为 -webkit-app-region: no-drag

这里我写了个圆点用来进行拖拽。

<template>
  <div class="drag-circle"></div>
</template>
<style lang="less" scoped>
.drag-circle {
  position: fixed;
  top: 4px;
  right: 4px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background-color: rgb(81, 202, 83);
  -webkit-app-region: drag;
}
</style>

当前时间

写一个定时器来不断更新当前时间,为了时间准确些,我设置了500ms一次更新。

保存Home窗口webContentId

import { ipcRenderer, remote } from "electron";
import { formatDate, setStorage} from "@/utils"
setStorage("homeId", remote.getCurrentWindow().id);

之后与 Setting 页面的通信需要这个webContentId,这里需要先存在 Storage 里,方便下次使用。

Home页面暂时先到这,我们开始着手设置页。

逻辑:Home页面点击设置->创建设置页

首先配置路由

const routes = [
  {    path: '/',    name: 'Home',    component: Home  },
  {    path: '/setting',    name: 'Setting',    component: () => import(/* webpackChunkName: "setting" */ '../views/Setting.vue')}
]

## Home.vue
const openWindow = () => {
  ipcRenderer.send("create-window", {
    windowName: "setting"
  });
};

渲染进程向主进程通信。

## background.js
ipcMain.on('create-window', async (e, args) => {
  const { windowName } = args;
  switch (windowName) {
    case 'setting':
      if (setting != null) return;
      setting = await windowFactory({
        url: windowName,
        height: 240,
        width: 340,
        transparent: true,
      })
      setting.on('closed', () => {
        setting = null;
      })
      break;
  }
})

关于渲染进程与主进程之间的爱恨情仇,看这篇文章:

Vue+Electron随缘更新指南

设置页界面编写

设置页分为Header+Content+footer。

设置页Header

Header同我们原生的标题栏一样,具有对窗口进行操作的功能。这里简单模拟了下关闭和最小化操作。

<template>
  <div class="tool-bar">
    <div class="btn" @click="operation('minimize')">-</div>
    <div class="btn del" @click="operation('close')">×</div>
  </div>
</template>
<script setup>
import { ipcRenderer } from "electron";
const operation = operate => {
  ipcRenderer.send("window-operate", {
    operate
  });
};
</script>

主进程接收到消息后,对不同的type进行处理。

ipcMain.on('window-operate', (e, args) => {
  const window = BrowserWindow.fromWebContents(e.sender);
  if (window == null) return;
  const { operate } = args;
  switch (operate) {
    case 'close':
      window.hide();
      window.destroy();
      break;
    case 'minimize':
      window.minimize();
      break;
  }
});

设置页Content

任务需要两个属性,分别是任务名称和任务的时间。对应两个input标签。

这里的逻辑和我们平时开发表单列表一样,不再赘述。

设置页Footer

点const homeId = +getHomeId();击保存时,我们需要将当前页面的数据传输给Home页面,此时需要再次进行通信。

const homeId = +getHomeId(); // 获取Home窗口的webContentId
const save = () => {
  if (checkInvaild()) {
    return alert("请完善信息!");
  }
  ipcRenderer.sendTo(homeId, "update-todolist", JSON.stringify(todoList.value));
  currentWindow.destroy();
};

还是老方法,Home页面接收到数据后进行处理。

ipcRenderer.on("update-todolist", (e, args) => {
  const t = JSON.parse(args);
  todoList.value = t.map(item => ({
    ...item,
    ...{
      isRunning: false,
      endTime: ""
    }
  }));
  taskMap.clear();
});

这边需要对传来的todoList进行处理,添加状态及结束时间。

当点击开始任务时,我们需要给todoList一个标识,表示当前任务的结束时候和运行状态。

const taskMap = new Map(); // 存为一个map,方便后面操作
const startTask = todo => {
  todo.isRunning = true;
  todo.endTime = Date.now() + todo.time * 60 * 1000; // 计算结束时间戳
  taskMap.set(todo.id, todo);
};
const closeTask = todo => {
  todo.isRunning = false;
  delete todo.endTime;
  taskMap.delete(todo.id);
};

还记得刚刚的定时器吗?我们需要在定时器里对任务的完成情况进行判断:

setInterval(() => {
  const date = new Date();
  currentTime.value = formatDate(date);
  for (let [k, v] of taskMap) {
    v.restTime = (v.endTime - date.getTime()) / 1000;
    if (Math.ceil(v.restTime) == 0) { // 完成
      delete v.endTime;
      v.isRunning = false; // 重置状态      
    remote // 使用remote模块调用Notification发送通知
        .Notification({
          title: v.name,
          body: `任务 ${v.name} 于${formatDate(date)}完成,再接再厉~!`,
          icon: require("../assets/watermelon.jpg")
        })
        .show();
      taskMap.delete(k);
    }
}}, 500);

完成任务后对状态进行重置,发送通知。

这里还需要注意的是,当我们主窗口已有任务,再次点击设置时,是需要将当前的todoList传递给Setting窗口进行初始化的。

打包

打包这个项目使用过的是 electron-builder 进行打包的,在打包前,我们需要进行一些准备。

yarn add electron-builder

我们可以看到 package.json 会多出几个脚本命令。

"scripts": {
    "electron:build": "vue-cli-service electron:build",
    "electron:serve": "vue-cli-service electron:serve",
    "postinstall": "electron-builder install-app-deps",
    "postuninstall": "electron-builder install-app-deps",
}

图标

electron打包需要配置相应的ico、png |  jpg等图标,使用 electron-icon-builder 根据我们提供的一个png或者jpg文件,自动生成所有尺寸的icon文件。

npm install electron-icon-builder

同时对应的 package.json 文件:

 "scripts": {
   ...
   +++     "build-icon": "electron-icon-builder --input=./src/assets/watermelon.jpg --output=./src/assets --flatten"
}

打包前执行 npm run build-icon 命令生成图标文件。

electron-builder的配置

electron-builder 需要对 vue.config.js 做一些配置,具体请看文档,这里贴一些我的配置:

electronBuilder: {
    nodeIntegration: true,
    builderOptions: {
        'productName': 'catwatermelon',
        'copyright': 'catwatermelon',
        'nsis': {
            'oneClick': false,
            'allowElevation': true,
            'allowToChangeInstallationDirectory': true,
            'installerIcon': 'src/assets/icon.ico',
            'uninstallerIcon': 'src/assets/icon.ico',
            'installerHeaderIcon': 'src/assets/icon.ico',
            'createDesktopShortcut': true,
            'createStartMenuShortcut': true,
            'shortcutName': 'catwatermelon'
        },
        'win': {
            'icon': 'src/assets/logo.ico',
            'target': [
                {
                    'target': 'nsis',
                    'arch': [
                        // 'x64',
                        'ia32'
                    ]
                }
            ]
        },
        'mac': { // mac
            'identity': null,
            'icon': 'src/assets/logo.icns'
        },
    }}

正式打包

执行 npm run electron:build,等待打包完成,打开.exe文件进行安装,就完成啦。