electron+vue从0到1实现一个桌面端日期时间倒计时软件实践(持续更新)

9,591 阅读10分钟

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

源码已开源,gitee开源地址:gitee.com/ihope_top/t…

前言

这个文章我也是边实践边写的,所以可能并不是特别的准确,大家仅供参考哈,文章主要描述electron的使用知识点,关于vue项目部分的介绍基本一笔带过,不会详细说,由于时间仓促(主要是征文快结束了),所以很多内容还来不及写和优化,大家按照文章教程操作过程如有任何问题欢迎贴在评论区一起探讨,如果有任何的优化建议也欢迎随时提出,我也会持续对文章内容进行更新和优化。虽然内容简陋,但也包含了我摸索很久找到的一些解决方案,建议有需求的朋友进行收藏

初始化

本文采用的方案为使用vue-cli-plugin-electron-builder插件直接构建

首先使用vue-cli初始化一个项目

vue create project-name

这一步就是常规的新建vue项目,就不多讲了。

然后是进入新建的vue项目里面引入electron

cd project-name
vue add vue-cli-plugin-electron-builder

image.png

之后会让我们选择版本,插件提供的最新版本为13,我们可以在安装完成后手动安装最新版本

image.png

出现这样的画面就说明我们已经安装好了,现在我们手动升级一下electron的版本

yarn add electron --save-dev

安裝完成之后就可以启动项目了

yarn run electron:serve

image.png

之后会出现这个画面,这个主要是因为vue-devtools插件需要从外网下载,如果需要的话可以科学上网一下,如果你平时开发vue就用不到vue-devtools,那直接忽略就好了,稍等数秒,就可以看到我们的项目启动成功了。

image.png

适配主题

上面看到的是vue预置的启动画面,我们先把无用的东西都给删了,就留下一个helo world。

image.png

可以看到,画面是黑的,字体也是黑的,就导致我们看不清,这是因为我电脑主题选择的是暗黑模式,应用自动显示的黑色背景,如果你不想对系统主题做适配的话,直接设置白色背景即可,这里我们选择对系统主题进行适配。

image.png

对于系统主题的获取,我们可以使用媒体查询prefers-color-scheme,具体用法如下

@media (prefers-color-scheme: dark) {
   body {
     color: #fff;
     background: #000;
   }
}

@media (prefers-color-scheme: light) {
  body {
     color: #000;
     background: #fff;
   }
}

image.png 可以看到暗黑模式下字体颜色已经变成了白色。

我们在把应用模式切换到亮

image.png

可以看到页面页自动显示了对应的颜色。

image.png

但是如果这样子的话,我们就需要在每个页面都写两份css样式,如果后期我们增加其他主题的话,就需要写多份样式,这样肯定是不行的,这时候有的同学就想到了,我们使用scss变量不就可以了,这个思路很对,但是在这里行不通,因为这个媒体查询不支持scss变量。

@media (prefers-color-scheme: dark) {
  $--color-1: #fff;
}

@media (prefers-color-scheme: light) {
  $--color-1: #000;
}

body {
  color: $--color-1;
}

image.png

可以看到,运行直接就报错了,那我们还有没有别的方法?当然有,那就是使用css变量

/* theme-dark.scss */
@mixin dark {
  --text-color-1: #fff;
  --bg-color-1: #000;
}
/* theme-light.scss */
@mixin light {
  --text-color-1: #000;
  --bg-color-1: #fff;
}
@media (prefers-color-scheme: dark) {
  @import './assets/scss/theme-dark.scss';
  :root {
    @include dark()
  }
}

@media (prefers-color-scheme: light) {
  @import './assets/scss/theme-light.scss';
  :root {
    @include light()
  }
}

body {
  color: var(--text-color-1);
  background: var(--bg-color-1);
}

这样就可以使用变量的方式控制不同的主题显示了。

开发

基础搭建好了之后,我们就来开发主要功能及界面了。

首先分析一下我们都需要几个页面,由于这个就是个小demo,我们就实现最基础的功能,所以大概就是一个设置页面,一个倒计时管理页面,一个桌面上的小部件。所以总共就3个页面。

管理页面开发

为了让懒进行到底,我们先安装一下element的ui框架,这里我们采用的是按需加载的方式,大家可以参考官网教程,这里就不重复了。

页面方面,由于时间紧张,我们也按最简单的来,开发出一个大概的功能结构,后面会进行功能扩展和优化。

image.png

本地化存储

这个地方为了可以让数据长久保存,我选择了Node.js嵌入式数据库Nedb,你可以把他理解成一个简化版的MongoDB。由于nedb不支持promise调用,我们选择使用nedb-promises(后简称nedb),直接安装即可

yarn add nedb-promises

由于nedb需要借助node的能力,所以我们需要开启electron的node支持

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true
    }
  }
})

使用方法也很简单,这里我们先对nedb进行一个简单的封装

// datastore.js
import Vue from 'vue'
import Datastore from 'nedb-promises'
const { app } = require('@electron/remote')

const basePath = app.getPath('userData')
// 创建一个表
const db = {
  todoItem: new Datastore({
    autoload: true,
    filename: basePath + '/nedb/todo-item'
  })
}
// 挂载到vue上
Vue.prototype.$db = db

然后在main.js进行引入

//main.js
import './plugins/datastore.js'

注意,这里还使用了electron的remote模块,需要先进行安装

yarn add @electron/remote

之后在background.js加入以下代码

require('@electron/remote/main').initialize()

image.png

require('@electron/remote/main').enable(win.webContents)

image.png

之后就可以使用了,使用也很简单

await datastore.find(...)
 .sort(...)
 .limit(...)

具体使用方法可参考官方文档github.com/bajankristo…

也可以阅读文章源代码查看更多用法

桌面悬浮窗(创建新窗口)

接下来我们需要一个在桌面常驻的日期倒计时,那么第一步,我们就需要先创建一个新窗口

创建新窗口

新窗口需要几个特点

  1. 全屏,因为我们需要让主要内容可以拖动到屏幕任意地方,并且放大缩小,然后配置项还可以展示(由于时间问题,本期暂不实现)
  2. 透明,因为我们不能挡住桌面图标
  3. 透明区域可以穿透点击,因为我们需要可以进行正常的桌面操作。
  4. 不会在下方出现缩略图标(如下)
  5. win+d时候窗口不能隐藏

image.png

具体代码如下,实现需求的关键代码都给了注释

// background.js

// 创建桌面悬浮窗口
const winURL = process.env.NODE_ENV === 'development'
  ? 'http://localhost:8080'
  : 'app://./index.html'
let desktopWindow = null

function createDesktopWindow () {
  desktopWindow = new BrowserWindow({
    width: 500,
    height: 500,
    frame: false, // 是否显示边缘框
    fullscreen: true, // 是否全屏显示
    transparent: true, // 是否透明
    resizable: false, // 是否可以改变窗口大小
    type: 'toolbar', // 设置窗口类型为工具窗口,则不会在任务栏出现缩略图
    webPreferences: {
      nodeIntegration: true,
      // 官网似乎说是默认false,但是这里必须设置contextIsolation
      contextIsolation: false
    }
  })

  desktopWindow.loadURL(winURL + '#/desktop')
  desktopWindow.setIgnoreMouseEvents(true) // 设置窗口不接收鼠标事件
  desktopWindow.on('closed', () => {
    desktopWindow = null
  })
  require('@electron/remote/main').enable(desktopWindow.webContents)
}

app.on('ready', async () => {
    /**其他代码*/
    createDesktopWindow()
}}

我们还要再vue的路由里加上对应的页面

// router.js
  {
    path: '/desktop',
    name: 'desktop',
    component: () => import('../views/DesktopView.vue')
  }

注意,在这里我们还需要把router的路由模式改为hash,否则打包后会遇到白屏的情况

const router = new VueRouter({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes
})

然后在对应的目录创建对应的vue文件就可以了。 此时可能看到窗口不是透明的,这是因为我们最开始演示适配主题的时候,给body设置了默认颜色,直接去掉就行了。

image.png

由于这篇文章主要是走流程,所以咱们这里不做的特别完善,比如拖动啊,手动固定啊之类的暂时都没有,主要是交大家一个思路,后续有时间也会对软件进行完善。

这里我们再采用一个最简单的方式去实现时间显示,就是每次启动的时候去获取最近的一个日期进行倒计时,当前倒计时结束,再去获取下一个。代码就不贴了,这里就说一下实现思路,感兴趣可以直接查看源码。后续有时间也会进行优化。

暂时的实现成果就这样

image.png

注意,目前发现一个问题,当点击在windows上使用win+d显示桌面时,倒计时也会消失,目前尚未找到完美的解决办法,后续也会继续探索可能的解决方案。也尝试了窗口置顶,但在测试过程中有时候有效,有时候无效,效果不稳定,后续也会持续进行测试

主进程与渲染进程通信

实现上面的效果时需要考虑一个问题,就是当数据更新的时候,桌面的倒计时也要跟着更新,尝试了很多方法都无效,比如使用bus、或者定时请求nedb中最新的数据(可能存在缓存的情况)都无效,后来只能每次修改都销毁桌面窗口,然后重新打开。那么我们怎么实现这一功能呢?

首先我们需要弄明白一个概念,就是主进程和渲染进程,这一块我也理解的不太深,就简单讲一下,大家可以自行学习一下。

我们项目里有一个background.js,这个是electron的入口文件,我们可以把它就理解为主进程,而除了这个文件外的其他页面里写的方法,我们就把它当成渲染进程。我们对于窗口的操作最好都放在主进程中进行,比如我们的倒计时窗口就是在主进程创建的,那么我们如何在vue文件(渲染进程)告诉主进程我们要重启倒计时窗口呢,这时候我们就需要用到ipc进行进程间的通信,使用方法如下

// vue文件

// 引入
import { ipcRenderer as ipc } from 'electron'

// 使用
ipc.send('desktopRestar')

我们使用ipc的send方法即可触发相应的事件,然后我们需要在主进程进行事件接收

// background.js

import { ipcMain} from 'electron' // 通常页面初始化时已引入本模块,无需重复引入

// 使用
ipcMain.on('desktopRestar', (e) => {
  desktopWindow.destroy() // 销毁倒计时窗口
  createDesktopWindow() //重新调用创建窗口
})

到这里其实最简单的一个应用已经完成了,我们可以实现事件的添加、编辑与删除,并且还会在桌面展示距离最近一个事件的倒计时。

修改图标

image.png

有个细节我们一直没有修改,就是应用的图标一直是用的默认的,我们需要改成自己的。

这个地方我直接用的png就行,用ico文件反而在打包后显示不出来,还没弄清楚怎么回事

image.png

注意看,第二个红框里的__static可能会报错,忽略即可,这个是electron的全局变量。 image.png

设置任务栏图标并增加退出菜单

现在还有一个问题就是我们的应用在右下角没有图标,这样我们点击右上角的时候只是暂时关闭窗口,其实没有退出,而我们也没有办法让它再显示,所以我们需要在右下角可以重新显示窗口并且退出。

这个功能我们需要借助electron的tray模块和Menu模块,所以我们需要先进行引入

import { Tray, Menu } from 'electron'

之后就是在app准备好之后设置菜单

app.on('ready', async () => {
  
  /**省略已有代码*/

  // 设置图标及菜单
  const tray = new Tray(path.join(__static, './favicon.png')) // sets tray icon image
  const contextMenu = Menu.buildFromTemplate([ // define menu items
    {
      label: '退出',
      click: () => app.quit()
    }
  ])

  tray.on('click', () => {
    if (mainWindow) {
      mainWindow.show()
    }
  })
  tray.setContextMenu(contextMenu)
})

image.png

现在右下角图标就出现了,也可以正常退出了

打包

我们直接执行下面的打包命令即可

yarn run electron:build

打包之后项目根目录会出现dist_electron目录,里面会有一个可执行的exe文件,双击即可安装。如果是mac的话应该是mac的可执行文件,可惜我没有。

禁止多开

安装完成之后,会发现重复打开竟然会出现多个窗口,这怎么能行,我们需要禁止多开,官方也提供给了我们方法。(使用此方法,如果该软件正在运行的话,则无法重新打开,所以你如果一直打开失败的话,不妨去任务管理器看看该软件是否已经正在运行)

// background.js
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
}

更新日志

6.23 更新

新建窗口时的加载路径错误,造成打包后无法访问同一个nedb数据库

const winURL = process.env.NODE_ENV === 'development'
  ? 'http://localhost:8080'
  : path.join('file://', __dirname, '/index.html')

改为

const winURL = process.env.NODE_ENV === 'development'
  ? 'http://localhost:8080'
  : 'app://./index.html'

由于偶然会出现桌面倒计时不出现的情况,所以增加临时解决办法,在主页面增加显示倒计时按钮,后期会进行优化

尚未解决问题

打包后element图标字体丢失的问题 打包后桌面窗口不展示倒计时的问题(可能是无法访问nedb数据库,待排查)

结束语

这样一个简单的不完善的项目就做好了,当然这篇文章的目的也不是为了做一个完美的项目,只是为了把一些常用的知识点介绍给大家而已。

由于完整的项目内容确实太多,所以这里省略了很多的内容,后期也会逐渐的补全,谢谢理解。

顺便推荐一下我写的另一篇文章,使用js写一个跑酷游戏,欢迎体验 juejin.cn/post/710342…