猫猫也会的electron+react+vite实现一个简约的todo-待办事项

1,027 阅读3分钟

写在前面

为什么要写个待办事项呢??? 之前有用过语雀的小记,功能很多,但很多都用不上,刚好正在学react,于是就想做一个简约的待办事项

效果图

todosimple.gif

呜呜呜~~~

这篇文章写的有点烂,但这里只能是写一些开发中遇到的问题,只能怪小编的文笔太差了,这里附上github地址,点击这里

正文

用vite创建项目

yarn create vite todoSimple --template react

然后就可以开始写项目了,写的时候一开始可以在浏览器看效果,写的差不多了就可以介入electron

接入electron

先安装electron

yarn add electron -D

这里有个坑,为什么要把electron放到devDependencies呢,因为打包我用的是electron-builder,打包的时候会提示报错,提示说electron不应该放到dependencies里面

下载完之后,我们来配置一下package.json

{
  "name": "todosimple",
  "version": "1.1.0",
  "main": "./render/electron.js",  // 这个是electron的入口文件
  "scripts": {
    "dev": "vite --host", 
    "build": "vite build",
    "build:el": "vite build && electron-builder", // 这个是electron的打包命令
    "serve": "vite preview",
    "start": "cross-env NODE_ENV=development electron .", //这个是开发环境运行electron命令
    "start:build": "cross-env NODE_ENV=production electron .",
    "edev": "yarn dev | yarn start"
  },
  "dependencies": {
    "@ant-design/icons": "^4.7.0",
    "animate.css": "^4.1.1",
    "antd": "^4.16.13",
    "axios": "^0.24.0",
    "dayjs": "^1.10.7",
    "react": "^17.0.0",
    "react-dom": "^17.0.0",
    "react-redux": "^7.2.6",
    "react-router-dom": "^5.3.0",
    "redux": "^4.1.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^1.0.0",
    "cross-env": "^7.0.3",
    "electron": "^15.3.0",
    "electron-builder": "^22.13.1",
    "electron-reloader": "^1.2.1",
    "less": "^4.1.2",
    "vite": "^2.6.4"
  }
}

接着创建electron

// electron.js
const path = require('path')
const { app, BrowserWindow, Menu, Tray } = require('electron')
const getIcon = path.join(__dirname, '../render/assets/') //获取图标
const gotTheLock = app.requestSingleInstanceLock() // 监听多个窗口
const NODE_ENV = process.env.NODE_ENV

Menu.setApplicationMenu(null) // 清除标题栏
let tray = null // 托盘对象
let win = null
require('./file')

// try {  开启这个每次刷新都会自动重启app
//   require('electron-reloader')(module);
// } catch { }

// 创建浏览器窗口
const createWindow = () => {
  win = new BrowserWindow({
    width: 1000,
    height: 700,
    icon: path.join(getIcon, 'favicon.ico'),
    webPreferences: {
      devTools: true, // 是否开启调试
      nodeIntegration: true,   // 这个两个一定要写 这样才能实现主进程和渲染进程的通信
      contextIsolation: false
    }
  })

  if (NODE_ENV === 'development') {
    win.loadURL('http://localhost:3000/')
    win.webContents.toggleDevTools() //打开调试工具
  } else {
    win.loadFile('./dist/index.html')
  }

  // 关闭window时触发下列事件.
  win.on('close', e => {
    e.preventDefault()
    win.hide()
    win.setSkipTaskbar(true)
  })


}

// 设置系统托盘

const setAppTray = () => {
  // 系统托盘图标目录
  tray = new Tray(path.join(getIcon, 'favicon.ico'))

  // 图标的上下文菜单  系统托盘右键菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '退出',
      click() {
        app.exit()
      }
    }
  ])

  // 监听鼠标单击
  tray.on('click', () => {
    let showwin = !win.isVisible()
    if (showwin) {
      win.show()
      win.setSkipTaskbar(false)
      return
    }

    win.hide()
    win.setSkipTaskbar(true)


  })

  // 设置此托盘图标的悬停提示内容
  tray.setToolTip('toDoSimple-待办事项')

  // 设置此图标的上下文菜单
  tray.setContextMenu(contextMenu)
}

console.log(gotTheLock, 'gotTheLock')

// 监听初始化完成
app.whenReady().then(() => {
  if (!gotTheLock) return

  setAppTray()
  createWindow()

  //监听应用打开 (macOs)
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  }
  )
})


// 所有窗口关闭时退出应用.
app.on('window-all-closed', () => {
  // macOS中除非用户按下 `Cmd + Q` 显式退出,否则应用与菜单栏始终处于活动状态.
  if (process.platform !== 'darwin') {
    app.quit()
  }
})


然后启动yarn start就打开桌面应用了

主进程和渲染进程之间的通信

主进程:electron的入口文件,能使用node的api
渲染进程:就是我们用raect写的页面

// 创建浏览器窗口
const createWindow = () => {
  win = new BrowserWindow({
   // ...
    webPreferences: {
      nodeIntegration: true,   // 这个两个一定要写 这样才能实现主进程和渲染进程的通信
      contextIsolation: false
    }
  })

然后新建一个file.js使用ipcMain向渲染进程通信

const { ipcMain } = require('electron')
const fs = require('fs')
const path = require('path')
const baseFileUrl = (title = '') => `${path.join(__dirname, 'data', title)}`

// 封装读取文件
const readFile = file => {
  return new Promise((resolve, reject) => {
    fs.readFile(file, 'utf8', (err, data) => {
      if (err) {
        reject(err)
        return
      }
      resolve(JSON.parse(data))
    })
  })
}

// 封装写入文件
const writeFile = (file, data) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(file, data, err => {
      if (err) {
        reject(err)
        return
      }
      resolve('写入成功')
    })
  })
}

// 监听 渲染进程的事件
ipcMain.on('writeFile', async (event, arg) => {
  let { title } = arg
  let fileUrl = baseFileUrl(title) + '.json'
  
    try {
      await writeFile(fileUrl, JSON.stringify(arg))
      event.reply('onWrite', { status: true })  // 向渲染进程发送信息
    } catch (err) {
      event.reply('onWrite', { status: false, msg: err })
    }

})
// 在react里面
const { ipcRenderer } = require('electron')  // 用import的话就或报错 而且不能在浏览器里使用

    // 写入本地
    ipcRenderer.send('writeFile', params)

    // 监听本地写入成功
    ipcRenderer.once('onWrite', (event, { status, msg }) => {
      if (status) {
       console.log(status)
        return
      }
    })

这就实现通信了

打包

// package.json
{
// ....

 "build": {
    "appId": "com.toDoSimple.app",
    "productName": "toDoSimple",
    "copyright": "Copyright © 2021 呆呆兽的猫猫",
    "icon": "render/assets/favicon.ico",
    "asar": false, // 如果用到node模块的话就不能使用压缩 ,所以设为false,不然会访问不到,就会出现开发是正常的,但打包后却发现这个功能用不了
    "nsis": {  // 配置安装
      "oneClick": false,  
      "allowToChangeInstallationDirectory": true
    },
    "files": [
      "dist/**/*",
      "render/**/*"
    ],
    "directories": {
      "buildResources": "assets",
      "output": "dist_electron"
    }
  }
}