对于一直面对电脑的程序员,眼睛的休息是很重要的。可是咱们程序员又太过于勤勤恳恳、聚精会神、专心致志、不辞辛苦!不免会忽略了时间的流逝。javascript
因此咱们迫切的须要一个定时提醒软件,来帮助咱们管理时间!css
秉承着钻研技术的理念,此次咱们就本身来动手作一个定时提醒软件。html
本文将会从项目搭建 -> 代码实现 -> 应用打包,手把手一行行代码的带你完成这个项目。vue
看完本文你将学会什么知识呢?java
让咱们开始吧~!node
原文地址:lei4519.github.io/blog/practi…ios
首先搭建一个vue3的项目,咱们将使用随着vue3的到来一样大火的vite来搭建。git
$ yarn create vite-app remind-rest
$ cd remind-rest
$ yarn
$ yarn dev
复制代码
执行完上面的命令,打开http://localhost:3000/
就能够看到启动的vue项目了。程序员
接下来咱们将vue项目放入electron中运行es6
首先安装electron + typescript(注意设置淘宝源或者使用cnpm下载)
$ yarn add dev electron typescript
复制代码
使用npx tsc --init
初始化咱们的tsconfig.json
,vue中的ts文件会被vite进行处理,因此这里的tsconfig配置只处理咱们的electron文件便可,咱们增长include属性include: ["main/"]
。
咱们会把打包后的代码都放到dist
目录中,因此配置一下outDir
属性,将ts编译后的文件放入dist/main
目录中
修改以下
{
"compilerOptions": {
"outDir": "./dist/main",
},
"include": ["main/"]
}
复制代码
在根目录建立main
文件夹,用来存放electron主进程中的代码
在main目录中新建index.ts
const {app, BrowserWindow} = require('electron')
// Electron会在初始化完成而且准备好建立浏览器窗口时调用这个方法
app.whenReady().then(createWindow)
// 建立一个窗口
function createWindow() {
const win = new BrowserWindow()
win.loadURL('http://localhost:3000')
}
复制代码
嗯,so easy!加上注释换行才9行代码,启动一下试试看~
咱们在package.json中加一个脚本main-dev
,而后执行
"scripts": {
"dev": "vite",
"build": "vite build",
"main-dev": "electron ./main/index.ts"
}
复制代码
不出意外你应该已经能够看到启动的桌面应用了,而里面显示的正是咱们的vue项目。
至此,开发环境已经搭建完毕,接下来咱们梳理一下需求,看一下咱们要作的究竟有哪些功能。而后开始实现代码。
设置页面
倒计时提示框
锁屏页面
设置
暂停
继续
重置
退出
暂停
和重置
按钮,对倒计时进行操做关闭
按钮提早退出锁屏界面,其余全部常规操做都没法退出锁屏界面(如切换屏幕、切换软件、cmd + Q)好了,需求梳理完毕,让咱们开始快乐的codeing吧👌~
在vue项目中建立以下文件
- src
- main.js // 入口文件
- route.js // 路由配置
- App.vue
- views
- LockPage.vue // 锁屏界面
- Tips.vue // 提示气泡界面
- Setting.vue // 设置界面
复制代码
安装vue-router
yarn add vue-router@^4.0.0-alpha.4
复制代码
其中 main.js
route.js
都是vue3的新写法,和老版本没有太大区别,就不详细说明了,直接看代码吧
views文件夹中的文件咱们后面再具体实现
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './route'
const app = createApp(App)
app.use(router)
router.isReady().then(() => app.mount('#app'))
复制代码
route.js
import {createRouter, createWebHashHistory} from 'vue-router'
import LockPage from './views/LockPage.vue'
import Tips from './views/Tips.vue'
import Setting from './views/Setting.vue'
export default createRouter(
{
history: createWebHashHistory(),
routes: [
{
path: '/LockPage',
name: 'LockPage',
component: LockPage
},
{
path: '/Tips',
name: 'Tips',
component: Tips
},
{
path: '/Setting',
name: 'Setting',
component: Setting
}
]
}
)
复制代码
App.vue
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'App'
}
</script>
复制代码
- main
- index.ts // 入口
- tary.ts // 托盘模块
- browserWindow.ts // 建立渲染进程窗口
- countDown.ts // 倒计时模块
- setting.ts // 设置模块
- utils.ts // 工具代码
- store.json // 本地存储
复制代码
渲染进程的代码,每次咱们修改以后都会进行热更新。而主进程的代码却没有这样的功能(社区中未找到相关实现),这就致使在主进程的开发过程当中咱们须要频繁的手动重启终端以去查看效果,这显然是一件很不效率的事情。这里咱们经过node的api来简单实现一个主进程代码的自动重启的功能。
思路其实也很简单,就是监听到文件变动后,自动重启终端
首先咱们须要使用node来运行终端命令,这样才能去进行控制。node怎么运行终端命令呢?使用child_process中的spawn模块就能够了,不熟悉的同窗能够看一下这片文章child_process spawn模块详解
在根目录新建一个scripts
文件夹,用来存放咱们的脚本文件
而后在scripts
目录中建立createShell.js
dev.js
这两个文件
mkdir scripts
cd scripts
touch createShell.js dev.js
复制代码
在createShell.js
文件中,建立一个工厂函数,传入终端命令,返回执行此命令的终端实例,代码以下:
const { spawn } = require('child_process')
module.exports = function createShell(command) {
return spawn(command, {
shell: true
})
}
复制代码
接下来咱们实现dev.js
的内容,先来捋一下思路,当咱们执行dev.js
的时候,咱们须要执行以下命令:
启动vite,运行渲染进程的代码
启动tsc,编译主进程的代码
等到tsc编译成功,启动electron
监听到electron进程发出的重启信号,重启electron
`&&`表明串行命令,前一个执行完才会执行后一个
`&`表明并行命令,先后两个命令同时执行
复制代码
// 引入咱们刚才写的工厂函数
const createShell = require('./createShell')
// 运行vite 和 tsc
const runViteAndTsc = () => new Promise((reslove) => {
// 运行终端命令 下面会解释
createShell('npx vite & rm -rf ./dist/main && mkdir dist/main && cp -r main/store.json dist/main/store.json && tsc -w').stdout.on('data', buffer => {
// 输出子进程信息到控制台
console.log(buffer.toString())
// tsc在每次编译生成后,会输出Watching for file changes
// 这里利用Promise状态只会改变一次的特性,来保证后续的代码逻辑只执行一次
if (buffer.toString().includes('Watching for file changes')) {
reslove()
}
})
})
// 运行electron
const runElectron = () => {
// 定义环境变量,启动electron
createShell('cross-env NODE_ENV=development electron ./dist/main/index.js')
//监听到子进程的退出事件
.on('exit', (code) => {
// 约定信号100为重启命令,从新执行终端
if (code === 100) runElectron()
// 使用kill而不是exit,否则会致使子进程没法所有退出
if (code === 0) process.kill(0)
})
}
// 串起流程,执行命令
runViteAndTsc()
.then(runElectron)
复制代码
在这里解释一下上面的终端命令,咱们格式化一下
npx vite & rm -rf ./dist/main &&
mkdir dist/main &&
cp -r main/store.json dist/main/store.json &&
tsc -w
1. 运行vite,同时删除掉上一次编译产生的main目录
2. 删除目录后,从新建一个空的main目录
3. 重建的目的是为了这行的copy命令,ts不会编译非.ts文件,咱们须要手动拷贝store.json文件
4. 拷贝完成后,开始编译ts
复制代码
这里补充一下,本身来写启动命令除了实现自动刷新以外,还有下面的缘由:
electron
也能够直接运行ts文件,可是并不会编译ts,不编译的话在ts文件中就没法使用import
,不使用import
就没办法得到代码自动导入和提示功能,因此要先使用tsc
编译ts文件成为js,而后再使用electron
运行js。tsc -w
功能来监听文件变化从新编译,这就致使 ts 编译完成后并不会退出,因此没法使用 &&
串行命令执行electron
,而使用 &
并行命令可能会出现electron
运行时,ts文件可能尚未编译成功致使electron
加载js文件不存在而启动失败的问题。因此咱们须要本身写命令来进行控制。以上只完成了第一步,接下来咱们要监听文件变化并退出electron进程,退出时咱们传入code:100,来通知外部这是一次重启
先写一个辅助函数, 递归遍历指定目录下的全部文件,并执行传入的回调函数,向回调函数中传入当前文件的路径
main/utils.ts
import fs from 'fs'
type callback = (name: string) => void
export function readDeepDir(path: string, fn: callback) {
const _reader = (path: string) => {
fs.readdirSync(path).forEach((name) => {
if (!fs.statSync(path + '/' + name).isDirectory()) {
return fn(path + '/' + name)
}
return _reader(path + '/' + name)
})
}
return _reader(path)
}
复制代码
在main/index.ts
中监听当前的主进程目录,只要有文件变化,咱们就执行app.exit(100)
退出当前进程
import { readDeepDir } from './utils'
function watchFile() {
let debounce: NodeJS.Timeout
const reload = () => {
clearTimeout(debounce)
debounce = setTimeout(() => {
// 当前应用退出,外部进程接收到以后重启应用
app.exit(100)
}, 300)
}
// fs.watch并不稳定,使用watchFile进行监听
const watcher = (path: string) => {
fs.watchFile(path, reload)
}
readDeepDir(__dirname, watcher)
}
复制代码
说明一下
不使用fs.watch
是由于这个api并不稳定,会致使刷新结果不符合预期。watchFile
是监听不到新增文件的,这个解决方案实际上是借助tsc -w
的能力,当有已监听的文件去引用新增文件的时候,就会致使tsc
从新编译,而后触发自动刷新,当第二次启动electron
的时候,就会把新的文件进行监听了
electron是有app.relaunch()
api的,调用这个api就会重启应用,那咱们为何不使用这个而要本身去写呢?是由于app.relaunch
实际上是另起了一个进程来运行新的electron,当前这个进程咱们须要执行app.exit()
来退出才能够,这是在官网说明的。可是若是咱们这么作的话,app.relaunch
启动的这个进程就会脱离了咱们node scripts/dev.js
这个进程的管控,致使咱们中断node scripts/dev.js
这个进程的时候,app.relaunch
启动的这个进程还在运行的问题。
到此自动刷新就完成了,让咱们真正的来实现代码逻辑吧!
import { app } from 'electron'
import fs from 'fs'
import {inittary} from './tary'
export const isDev = process.env.NODE_ENV === 'development'
// 自动刷新
isDev && watchFile()
// 隐藏dock
app.dock.hide()
// Electron会在初始化完成而且准备好建立浏览器窗口时调用这个方法
app.whenReady().then(() => {
inittary()
})
复制代码
index.js 代码很简单,这里的inittary
咱们还没实现,在实现它以前,让咱们先把倒计时模块写好
首先定义些关于时间的常量
export const BASE_UNIT = 60
export const SECONDS = 1000
export const MINUTES = SECONDS * BASE_UNIT
export const HOURS = MINUTES * BASE_UNIT
复制代码
将倒计时模块写成一个类,方便管理
这个类有三个私有属性
class CountDown {
// 用来计算当前的时间
private time = 0
// 保存传入的时间,重置时会用到
private _time = 0
// 清除定时器,暂停时会用到
private timer?: NodeJS.Timeout
}
复制代码
接下来实现相关方法,咱们须要有设置时间、暂停时间、重置时间,启动倒计时这几个功能
setTime(ms: number) {
// 若是以前有一个定时器在运行,就中断掉
this.stop()
this.time = this._time = ms
return this
}
stop() {
this.timer && clearInterval(this.timer)
}
resetTime() {
this.time = this._time
return this
}
run() {
this.timer = setInterval(() => {
this.time -= SECONDS
}, SECONDS)
}
复制代码
easy~ 再定义一个静态方法,用于将时间戳转换为咱们的须要的时间格式
static formatTimeByMs(ms: number) {
return {
h: String((ms / HOURS) | 0).padStart(2, '0'),
m: String((ms / MINUTES) % BASE_UNIT | 0).padStart(2, '0'),
s: String((ms / SECONDS) % BASE_UNIT | 0).padStart(2, '0'),
}
}
复制代码
ok,大致功能写好了,接下来咱们须要把时间的变化发送出去
为了时间的精确性,再使用时咱们将为倒计时模块单独开一个进程,因此这里也使用进程通讯的方式来发送消息
先定义发送消息的接口
export interface SendMsg {
// 格式化后的时间
time: {
h: string
m: string
s: string
}
// 原始时间戳
ms: number
// 时间是否归零
done: boolean
}
复制代码
写一个发送消息的方法
private send(msg: SendMsg) {
process.send!(msg)
}
复制代码
而后在重置时间和启动时间时给父进程发送消息
resetTime() {
this.time = this._time
this.send({
time: CountDown.formatTimeByMs(this.time),
ms: this.time,
done: this.time <= 0
})
return this
}
run() {
this.send({
time: CountDown.formatTimeByMs(this.time),
ms: this.time,
done: this.time <= 0
})
this.timer = setInterval(() => {
let done: boolean
if (done = this.time <= 0) this.stop()
this.send({
time: CountDown.formatTimeByMs(this.time -= SECONDS),
ms: this.time,
done
})
}, SECONDS)
}
复制代码
OK,发送消息的逻辑咱们处理完成了,接下来处理一下接收消息的流程
首先定义接口,这会比较复杂,由于咱们的这些方法中,setTime是须要传入参数的,而其余的方法并不须要,若是想准确进行提示,那咱们就须要这么作
首先咱们将须要接收参数的方法名定义一个type,这里是将类型当成了变量来使用
type hasDataType = 'setTime'
复制代码
而后咱们定义不接受参数的接口,这里使用了两个技巧
这样操做以后,这里的type其实就是 resetTime | run | stop
interface ReceiveMsgNoData {
type: Exclude<keyof CountDown, hasDataType>
}
复制代码
接收参数的接口就很简单了
interface ReceiveMsgHasData {
type: hasDataType
data: number
}
复制代码
最终定义一个联合类型供外部使用,这里之因此要定义数组类型,是为了方便外部使用,以后的代码中咱们能够看到用法了
export type ReceiveMsg = ReceiveMsgNoData | ReceiveMsgHasData | Array<ReceiveMsgNoData | ReceiveMsgHasData>
复制代码
接口定义完了,来实现一下代码
const c = new CountDown()
process.on('message', (message: ReceiveMsg) => {
if (!Array.isArray(message)) {
message = [message]
}
message.forEach((msg) => {
if (msg.type === 'setTime') {
c[msg.type](msg.data)
} else {
c[msg.type]()
}
})
})
复制代码
接收消息的功能也实现了,至此倒计时模块就写完了,快让咱们去tary.js中把它使用起来吧!~
一样的,tary也将使用类来实现
在代码实现以前,咱们先来捋一下逻辑
先定义要使用的私有属性
import { Tray as ElectronTary } from 'electron'
type TimeType = 'REST' | 'WORK'
class Tary {
// 初始化托盘栏,并传入托盘图标
private tray: ElectronTary = new ElectronTary(
path.resolve(__dirname, '../icon/img.png')
)
// 标示当前时间为工做时间或休息时间
private timeType: TimeType = 'WORK'
// 菜单实例
private menu: Menu | null = null
// 锁屏窗口实例
private restWindows: BrowserWindow[] | null = null
// 提示框口实例
private tipsWindow: BrowserWindow | null = null
// 倒计时模块 使用 child_process.fork 建立一个子进程
private countDown: ChildProcess = fork(path.resolve(__dirname, './countDown'))
}
复制代码
定义向子进程发送消息的方法
send(message: ReceiveMsg | ReceiveMsg[]) {
this.countDown.send(message)
}
复制代码
设置菜单项,这里其实就是调用electron的api,详细的能够看官方文档。
当用户点击暂停、继续、重置时,给倒计时模块发送消息。偏好设置的功能咱们后面再实现
private setContextMenu() {
this.menu = Menu.buildFromTemplate([
{
label: '偏好设置',
accelerator: 'CmdOrCtrl+,',
click: () => {},
},
{
type: 'separator',
},
{
id: 'play',
label: '继续',
accelerator: 'CmdOrCtrl+p',
visible: false,
click: (menuItem) => {
this.send({
type: 'run'
})
// 暂停和继续 只显示其中一个
menuItem.menu.getMenuItemById('pause').visible = true
menuItem.visible = false
},
},
{
id: 'pause',
label: '暂停',
accelerator: 'CmdOrCtrl+s',
visible: true,
click: (menuItem) => {
this.send({
type: 'stop'
})
// 暂停和继续 只显示其中一个
menuItem.menu.getMenuItemById('play').visible = true
menuItem.visible = false
},
},
{
label: '重置',
accelerator: 'CmdOrCtrl+r',
click: (menuItem) => {
menuItem.menu.getMenuItemById('play').visible = false
menuItem.menu.getMenuItemById('pause').visible = true
this.startWorkTime()
},
},
{
type: 'separator',
},
{ label: '退出', role: 'quit' },
])
this.tray.setContextMenu(this.menu)
}
复制代码
监听倒计时模块消息
handleTimeChange() {
this.countDown.on('message', (data: SendMsg) => {
if (this.timeType === 'WORK') {
this.handleWorkTimeChange(data)
} else {
this.handleRestTimeChange(data)
}
})
}
复制代码
开始工做时间倒计时
private startWorkTime() {
this.send([
{
type: 'setTime',
data: workTime,
},
{
type: 'run',
},
])
}
复制代码
实例化时调用上面的方法
constructor() {
this.setContextMenu()
this.handleTimeChange()
this.startWorkTime()
}
复制代码
上面代码执行完成后,倒计时就启动了,接下来就要处理时间变化的逻辑了
先来处理工做时间的变化
handleWorkTimeChange({ time: {h, m, s}, ms, done }: SendMsg) {
this.tary.setTitle(`${h}:${m}:${s}`) // 1
if (ms <= tipsTime) {
this.handleTipsTime(s, done) // 2
} else if (this.tipsWindow) {
this.closeTipsWindow() // 3
}
if (done) {
this.toggleRest() // 4
}
}
复制代码
export const TIPS_MESSAGE = 'TIPS_MESSAGE'
handleTipsTime(s: string, done: boolean) {
if (!this.tipsWindow) { // 初始化
ipcMain.on(TIPS_MESSAGE, this.handleTipsMsg)
this.tipsWindow = createTipsWindow(this.tary.getBounds(), s)
} else { // 发送消息
this.tipsWindow.webContents.send(TIPS_MESSAGE, {
s,
done
})
}
}
复制代码
监听提示框渲染进程的消息
interface TipsMsgData {
type: 'CLOSE' | 'RESET' | 'STOP'
}
handleTipsMsg = (event: IpcMainEvent, {type}: TipsMsgData) => {
if (type === 'CLOSE') {
this.closeTipsWindow()
} else if (type === 'RESET') {
this.closeTipsWindow()
this.send({
type: 'resetTime'
})
} else if (type === 'STOP'){
this.closeTipsWindow()
this.send({
type: 'stop'
})
this.menu.getMenuItemById('play').visible = true
this.menu.getMenuItemById('pause').visible = false
}
}
closeTipsWindow() {
if (this.tipsWindow) {
ipcMain.removeListener(TIPS_MESSAGE, this.handleTipsMsg)
this.tipsWindow.close()
this.tipsWindow = null
}
}
复制代码
在browserWindow.ts中添加以下代码
const resolveUrl = (address: string) => `http://localhost:3000/#${address}`
export function createTipsWindow(rect: Rectangle, s: string): BrowserWindow {
const win = new BrowserWindow({
x: rect.x, // 窗口x坐标
y: rect.y, // 窗口y坐标
width: 300, // 窗口宽度
height: 80, // 窗口高度
alwaysOnTop: true, // 一直显示在最上面
frame: false, // 无边框窗口
resizable: false, // 不能够resize
transparent: true, // 窗口透明
webPreferences: {
webSecurity: false, // 忽略web安全协议
devTools: false, // 不开启 DevTools
nodeIntegration: true // 将node注入到渲染进程
}
})
// 加载Tips页面,传入消息通讯的事件名称和时间
win.loadURL(resolveUrl(`/Tips?type=${TIPS_MESSAGE}&s=${s}`))
return win
}
复制代码
页面结构很简单,提示用户还有几秒开始休息,而后提供暂停和关闭的按钮
<template>
<div class="wrap">
<div class="title">还剩{{time}}s开始休息~</div>
<div class="progress"></div>
<div class="btns">
<button @click="stop">暂停</button>
<button @click="reset">重置</button>
</div>
</div>
</template>
复制代码
主要看一下逻辑代码
<script>
import {ref} from 'vue'
import {useRoute} from 'vue-router'
const { ipcRenderer } = require('electron')
export default {
setup() {
// 取到当前页面的query参数
const {query} = useRoute()
// 使用传入的s做为时间
const time = ref(query.s)
// 向主进程发送消息
const close = () => {
ipcRenderer.send(query.type, {type: 'CLOSE'})
}
const stop = () => {
ipcRenderer.send(query.type, {type: 'STOP'})
}
const reset = () => {
ipcRenderer.send(query.type, {type: 'RESET'})
}
// 监听时间变化,修改时间
ipcRenderer.on(query.type, (ipc, {s, done}) => {
time.value = s
if (done) close()
})
return {
time,
stop,
reset
}
}
}
</script>
复制代码
为了节省篇幅,样式代码就不贴上来了,各位能够自行发挥,或者看下面的完整代码
到此,气泡提示的代码已经被咱们完成了。接下来咱们继续处理工做时间结束时,切换至休息时间的逻辑
handleWorkTimeChange({ time: {h, m, s}, ms, done }: SendMsg) {
// ...
if (done) {
this.toggleRest()
}
}
toggleRest() {
this.timeType = 'REST'
this.closeTipsWindow()
ipcMain.on(REST_MESSAGE, this.handleRestMsg)
this.restWindows = createRestWindow()
}
复制代码
interface RestMsgData {
type: 'CLOSE' | 'READY'
data?: any
}
handleRestMsg = (event: IpcMainEvent, data: RestMsgData) => {
if (data.type === 'READY') {
this.startRestTime()
} else if (data.type === 'CLOSE') {
this.toggleWork()
}
}
startRestTime = () => {
this.send([
{
type: 'setTime',
data: restTime
},
{
type: 'run'
}
])
}
toggleWork() {
this.timeType = 'WORK'
ipcMain.removeListener(REST_MESSAGE, this.handleRestMsg)
this.restWindows?.forEach(win => {
win.close()
})
this.restWindows = null
this.startWorkTime()
}
复制代码
代码很简单,当渲染进程初始化成功后(vue create时机)会向咱们发送READY事件,此时咱们开始休息事件的倒计时。
当渲染进程的倒计时结束或者点击了关闭按钮时,会触发关闭事件,此时咱们将切换回工做时间
再说一下切换回工做时间的逻辑
喝口水接着来!建立休息时间的窗口(锁屏界面)
main/browserWindow.ts
export function createRestWindow(): BrowserWindow[] {
return screen.getAllDisplays().map((display, i) => {
// 建立浏览器窗口
const win = new BrowserWindow({
x: display.bounds.x + 50,
y: display.bounds.y + 50,
fullscreen: true, // 全屏
alwaysOnTop: true, // 窗口是否应始终位于其余窗口的顶部
closable: false, // 窗口是否可关闭
kiosk: true, // kiosk模式
vibrancy: 'fullscreen-ui', // 动画效果
webPreferences: {
devTools: false,
webSecurity: false,
nodeIntegration: true
}
})
// 而且为你的应用加载index.html
win.loadURL(resolveUrl(`/LockPage?type=${REST_MESSAGE}${i === 0 ? '&isMainScreen=1' : ''}&password=${password}`))
return win
})
}
复制代码
这个有几点须要特殊处理,由于咱们但愿出现锁屏界面时,用户就不能够进行别的操做了。
这里咱们须要启用kiosk模式来达到效果
windows中的kiosk模式介绍以下(取自百度):
什么是Windows自助终端模式? Windows Kiosk模式只是Windows操做系统(OS)的一项功能,它将系统的可用性或访问权限仅限于某些应用程序。意思是,当咱们在Windows上打开Kiosk模式时,它只容许一个应用程序运行,就像机场上的kiosk系统那样设置为仅运行Web浏览器,某些应用程序如PNR状态检查一个。 Kiosk模式的好处是,它容许企业仅在办公室,餐馆等运行特定的销售点(POS)应用程序,以阻止客户使用机器上的任何其余应用程序,除了他们已分配的应用程序。它不只能够在windows 10上使用,并且还能够在Windows XP,Windows Vista,Windows 7和Windows 8.1中启用。
简单点说就是让你的电脑只运行当前这个应用程序,阻止你使用别的应用程序。
主要的配置以下
fullscreen: true, // 窗口全屏
alwaysOnTop: true, // 窗口一直显示在最上面
closable: false, // 窗口不可关闭
kiosk: true, // 窗口为kiosk模式
复制代码
那代码中的screen.getAllDisplays()
是干什么用的呢?这是为了防止外接显示器(程序员大多数都会外接的),若是咱们只建立一个窗口,那只能让当前屏幕没法操做,而别的显示器仍是能够正常工做的。因此咱们使用这个api来获取到全部的显示器,而后为每个显示器都建立一个窗口。
同时咱们只让第一个窗口中出现提示信息和关闭按钮。因此咱们给渲染进程传入一个主屏幕的标志。
vue渲染进程代码 views/LockPage.vue
<template>
<div v-if="isMainScreen" class="wrap">
<div class="time">{{time}}</div>
<div class="btn" @click="close">X</div>
</div>
</template>
<script>
export default {
setup() {
const {query} = useRoute()
const time = ref('')
const close = () => {
ipcRenderer.send(query.type, {type: 'CLOSE'})
}
const isMainScreen = ref(!!query.isMainScreen)
if (isMainScreen) {
ipcRenderer.send(query.type, {type: 'READY'})
ipcRenderer.on(query.type, (ipc, {time: {h, m, s}, done}) => {
time.value = `${h}:${m}:${s}`
if (done) close()
})
}
return {
isMainScreen,
time,
close
}
}
}
</script>
复制代码
逻辑很简单,若是是主屏幕,那初始化的时候咱们就发送一个ready事件,而后监听时间变化。若是时间结束就发送关闭的事件。
至此,就只剩设置相关的逻辑没有写
import fs from 'fs'
import path from 'path'
const storePath = path.resolve(__dirname, './store.json')
function get() {
const store = fs.readFileSync(storePath, 'utf-8')
return JSON.parse(store)
}
export let {restTime, tipsTime, workTime} = get()
export function setTime(rest: number, work: number, tips: number) {
restTime = rest
tipsTime = tips
workTime = work
fs.writeFileSync(storePath, JSON.stringify({restTime, tipsTime, workTime}, null, 2))
}
复制代码
逻辑:从本地文件中获取工做、休息、提示时间,当设置新的时间时再改写本地文件
完善托盘栏菜单项的代码
interface SettingMsgData {
rest: number
work: number
tips: number
}
Menu.buildFromTemplate([
{
label: '偏好设置',
accelerator: 'CmdOrCtrl+,',
click: () => {
const win = createSettingWindows(restTime, tipsTime, workTime)
const handleSettingMsg = (event: IpcMainEvent, {rest, work, tips}: SettingMsgData) => {
setTime(rest, work, tips)
win.close()
}
win.on('close', () => {
ipcMain.removeListener(SETTING_MESSAGE, handleSettingMsg)
})
ipcMain.on(SETTING_MESSAGE, handleSettingMsg)
},
}
])
复制代码
当咱们点击设置菜单项时
而设置窗口发消息的时机就是当用户点击保存的时候,此时会把设置以后的工做时间、休息时间、提示时间传过来。咱们设置到本地便可
下面咱们看一下建立窗口和渲染进程的逻辑
main/browserWindow.ts
export function createSettingWindows(restTime: number, tipsTime: number, workTime: number) {
const win = new BrowserWindow({
maximizable: false,
minimizable: false,
resizable: false,
webPreferences: {
webSecurity: false,
nodeIntegration: true // 将node注入到渲染进程
}
})
win.loadURL(resolveUrl(`/Setting?type=${SETTING_MESSAGE}&rest=${restTime}&tips=${tipsTime}&work=${workTime}`))
return win
}
复制代码
vue: views/setting.vue
export default {
setup() {
const {query} = useRoute()
const rest = ref(+query.rest / MINUTES)
const work = ref(+query.work / MINUTES)
const tips = ref(+query.tips / SECONDS)
const save = () => {
ipcRenderer.send(query.type, {
rest: rest.value * MINUTES,
work: work.value * MINUTES,
tips: tips.value * SECONDS
})
}
const reset = () => {
rest.value = +query.rest / MINUTES
work.value = +query.work / MINUTES
tips.value = +query.tips / SECONDS
}
return {
rest,
work,
tips,
save,
reset
}
}
}
复制代码
好了,至此咱们的代码已经彻底实现了。
可是如今还有一个点须要解决,那就是电脑休眠时,咱们应该让计时功能暂停。
咱们在main/index.ts
中修改以下代码
app.whenReady().then(() => {
const tray = initTray()
// 系统挂起
powerMonitor.on('suspend', () => {
tray.send({
type: 'stop'
})
})
// 系统恢复
powerMonitor.on('resume', () => {
tray.send({
type: 'run'
})
})
})
复制代码
好了,就是监听两个事件的事~ 都是些api,就很少说了。
接下来咱们打包一下electorn,让咱们的代码能够在电脑上安装。
项目打包主流的方式有两种:electron-builder
和electron-packager
electron-builder
会把项目打成安装包,就是咱们平时安装软件的那种形式。
electron-packager
会把项目打包成可执行文件,你能够理解为上面👆的安装包安装以后的软件目录。
下面咱们分别介绍一下这两种的打包步骤(这里只打包了mac版本,win版本可自行查阅官网,差异不大)
electron-builder
打包安装
cnpm i electron-builder --save-dev
复制代码
package.json
新增build
项
"build": {
// 软件的惟一id
"appId": "rest.time.lay4519",
// 软件的名称
"productName": "Lay",
// 要打包的文件
"files": [
"node_modules/",
"dist/",
"package.json"
],
// 打包成mac 安装包
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
// 设置打包目录
"directories": {
"output": "release"
}
}
复制代码
增长脚本
"scripts": {
// ...
"buildMac": "cp -r icon dist/icon && npx electron-builder --mac --arm64"
}
复制代码
electron-packager
打包增长脚本
"scripts": {
// ...
"packageMac": "rm -rf ./dist && npx vite build && tsc && cp -r icon dist/icon & cp main/store.json dist/main/store.json && electron-packager . --overwrite"
}
复制代码
这个大概解释一下
好了,打包已经完成了。可是你觉得到此就结束了吗?
点开vite打包后的index.html,你会发现script标签上有一个type="module"
,这意味着vite默认打包后,仍是使用了es6的模块机制,这个机制依赖了http,因此咱们没法使用file
协议来加载文件。
也就是说,这个html咱们双击打开是没法运行的,因此你在electron里直接loadFile也是没法运行的。
怎么解决呢?也许vite能够配置CMD、AMD的模块机制,可是我也懒得再去翻阅文档了。反正是用的electron,咱们直接在本地起一个http服务就是
main/browserWindow.ts
const productPort = 0
const resolveUrl = (address: string) => `http://localhost:${isDev ? 3000 : productPort}/#${address}`
if (!isDev) {
// 检测端口是否被占用
const portIsOccupied = (port: number): Promise<number> => {
return new Promise(r => {
const validate = (p: number) => {
const server: http.Server = http
.createServer()
.listen(p)
.on('listening', () => {
server.close()
r(p)
})
.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
server.close()
validate(p += 1)
}
})
}
validate(port)
})
}
// 执行
portIsOccupied(8981)
.then((p) => {
productPort = p
http.createServer((req, res) => {
if (req.url === '/') {
// content-type: application/javascript
return fs.readFile(path.resolve(__dirname, '..', 'renderer/index.html'), (err, data) => {
if (err) return
res.setHeader('content-type', 'text/html; charset=utf-8')
res.end(data)
})
} else {
return fs.readFile(path.resolve(__dirname, '..', 'renderer' + req.url), (err, data) => {
if (err) return
if (req.url!.endsWith('.js')) {
res.setHeader('content-type', 'application/javascript')
} else if (req.url!.endsWith('.css')) {
res.setHeader('content-type', 'text/css')
}
// 缓存7天
res.setHeader('cache-control', 'max-age=604800')
res.end(data)
})
}
})
.listen(p)
})
}
复制代码
好啦,这下咱们就真正的把代码完成了~
完整代码点此,以为文章还能够的欢迎star、following。
若是有什么问题欢迎在评论区提出讨论。感谢观看🙏