Electron实战——实现一个小工具

简介

这是一个开源软件包提醒工具,可以查询有哪些软件包有更新版本。

自己平时会关注Maven、NPM和GitHub上的一些开源软件包,在收藏夹里收藏了一堆,不定时的去查看,感觉有些麻烦,所以做了这个软件。

通过软件每天定时更新,就会知道是否有更新,有的话就点击版本链接去看看有啥更新。“懒人”改变世界乎。

项目地址

技术栈

  • vue 2.6
  • electron 8.2
  • element-ui 2.13
  • axios 0.19
  • cheerio 1.0
  • sqlite3 4.2
  • vuex 3.3
  • cron 1.8

开发

视图层

引入依赖

# Element插件
vue add element
  • 考虑软件功能简单,所以使用按需导入


  • 选择语言


  • 运行看下效果
yarn electron:serve

绘制页面

  • 引入需要的element组件 element.js

  • 绘制列表 Package.vue

  • 绘制新增 AddPackage.vue

数据层

爬取远程数据
  • 引入依赖
# http请求
yarn add axios
# 解析html
yarn add cheerio
  • 设置跨域访问 background.js
function createWindow () {
  win = new BrowserWindow({
    ...
    webPreferences: {
      webSecurity: false,
      ...
    }
  })
  ...
}
  • 爬取NPM包信息
const getNpmPackage = (baseUrl, packageName) => {
  return new Promise((resolve, reject) => {
    http.get(`${baseUrl}/package/${packageName}`).then(res => {
      const $ = cheerio.load(res.data)
      const packageInfo = $('#top').children().last().children('div')
      const latestVersion = packageInfo.find('h3:contains(Version)').siblings().text()
      const publishTime = packageInfo.find('h3:contains(Last publish)').siblings().children().first().attr('datetime')
      if (latestVersion) {
        resolve({ latestVersion, publishTime: util.dateFormat(publishTime) })
      } else {
        resolve({})
      }
    }).catch(err => {
      reject(err)
    })
  })
}
本地数据库
  • 引入依赖
# 存储一些全局状态
yarn add vuex
yarn add sqlite3
  • 程序启动时初始化数据库
  • App.vue
async beforeCreate() {
    await db.init().catch(() => {})
    ...
   // 更新初始化完成状态
   this.setAppInitFinished(true)
}
  • db/index.js
 async init() {
    ...
    // 同步执行初始化SQL
    await this.run(`create table t_package
                    (
                      category_code text default '' not null,
                      group_name text default '' not null,
                      name text default '' not null,
                      current_version text,
                      latest_version text,
                      publish_time text,
                      create_time text,
                      constraint t_package_pk
                      primary key (category_code, group_name, name)
                    );`).catch(() => {})
    ...
  }
  • Package.vue
watch: {
    // 初始化完成查询数据
    getAppInitFinished(val) {
      if (val) {
        this.searchCategory()
      }
    }
    ...
}
  • 新增数据 dao/package.js
  add ({ categoryCode, name, groupName, currentVersion, latestVersion, publishTime }) {
    return new Promise((resolve, reject) => {
      const sql = `insert into t_package
                   (category_code, name, group_name, current_version, latest_version, publish_time, create_time)
                   values
                   (?, ?, ?, ?, ?, ?, ?)`
      db.run(sql, [categoryCode, name, groupName || '', currentVersion, latestVersion, publishTime || '', util.dateFormat(new Date())]).then(res => {
        resolve(res)
      }).catch(err => {
        reject(err)
      })
    })
  }
  • 查询数据 dao/package.js
  getListByCategoryCode(categoryCode) {
    return new Promise((resolve, reject) => {
      const sql = `select category_code categoryCode, name, group_name groupName,
                   current_version currentVersion,
                   latest_version latestVersion,
                   publish_time publishTime
                   from t_package
                   where category_code = ?
                   order by name`
      db.all(sql, categoryCode).then(res => {
        resolve(res)
      }).catch(err => {
        reject(err)
      })
    })
  }

导入导出

导入

  • 主进程 background.js
// 弹窗选择文件返回给渲染进程
ipcMain.on(consts.IMPORT_CHANNEL, (ipcMainEvent) => {
  dialog.showOpenDialog(win, {
    title: '选择CSV文件',
    filters: [
      { name: 'Custom File Type', extensions: ['csv'] }
    ],
    properties: ['openFile']
  }).then(res => {
    ipcMainEvent.sender.send(consts.IMPORT_SELECTED_CHANNEL, res)
  }).catch(() => {})
})
  • 渲染进程 Backup.vue
    // 读取文件存入数据库
    async importData(file) {
      const data = fs.readFileSync(file)
      ...
      for (const [index, item] of packages.entries()) {
        ...
        try {
          this.setPercentage(Math.floor((index + 1) / packages.length * 100))
          await packageDao.add(item)
          successCount += 1
        } catch (e) {
          errs.push(index + 1)
          console.log(e)
        }
      }
      ...
      // 刷新页面
      this.setRefreshPackageFlag(true)
    }

导出

  • 主进程
ipcMain.on(consts.EXPORT_CHANNEL, (ipcMainEvent, path) => {
  // 监听下载进度
  win.webContents.session.once('will-download', (event, item) => {
    item.on('updated', (event, state) => {
      if (state === 'progressing') {
        ipcMainEvent.sender.send(consts.EXPORT_PROGRESS_CHANNEL, Math.floor(item.getReceivedBytes() / item.getTotalBytes() * 100))
      }
    })
    item.once('done', (event, state) => {
      ipcMainEvent.sender.send(consts.EXPORT_STATE_CHANNEL, state)
    })
  })
  // 下载文件
  ipcMainEvent.sender.downloadURL('file://' + path)
})
  • 渲染进程
async exportData() {
      // 查询数据
      const packageList = await packageDao.getAll()
      ...
      // 写入临时文件
      fs.writeFileSync(filePath, Buffer.from(data.join('\n')))
      // 文件传递给主进程
      this.$electron.ipcRenderer.send(consts.EXPORT_CHANNEL, filePath)
      // 获取下载进度
      this.$electron.ipcRenderer.on(consts.EXPORT_PROGRESS_CHANNEL, (event, percentage) => {
        if (percentage) {
          this.setPercentage(percentage)
        }
      })
      this.$electron.ipcRenderer.once(consts.EXPORT_STATE_CHANNEL, (event, state) => {
        if (state === consts.ELECTRON_DOWNLOAD_STATE_COMPLETED) {
          // eslint-disable-next-line no-new
          new Notification('', {
            body: '导出数据完成'
          })
        }
        // 删除临时文件
        fs.unlink(filePath, function () {})
        this.setPercentage(0)
      })
    }

任务调度,每日定时查询

  • 添加依赖
yarn add cron
yarn add moment
yarn add moment-timezone -D
  • 程序启动时,创建任务
async beforeCreate() {
    ...
    await settingDao.getOne(consts.DB_SETTING_KEY_REMIND_PERIOD).then(res => {
      if (res && res.value) {
        // 创建任务
        const vm = this
        const job = new CronJob(util.getRemindCron(res.value), function() {
          vm.$refs.main.check()
        }, null, false)
        this.setJob(job)
      }
      console.log('create-job')
    }).catch(err => {
      console.log(err)
    })
    await settingDao.getOne(consts.DB_SETTING_KEY_REMIND_ENABLED).then(res => {
      // 启用提醒
      if (res && res.value && res.value === '1') {
        this.getJob.start()
      }
      console.log('start-job')
    }).catch(err => {
      console.log(err)
    })
    this.setAppInitFinished(true)
  },

发布

制作图标

  • 制作一张Logo图片,1024*1024 底色透明 png格式,命名为icon.png放置在public文件夹下
  • 使用electron-icon-maker制作mac图标icns和windows格式图标ico
 yarn add electron-icon-maker -D
./node_modules/.bin/electron-icon-maker --input=./public/icon.png --output=./public

安装包配置

  • vue.config.js
module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        appId: 'com.electron.demo',
        // 程序名
        productName: 'ElectronDemo',
        // 安装包名
        artifactName: '${name}-${version}.${ext}',
        win: {
          icon: './public/icon.ico',
          target: 'nsis'
        },
        nsis: {
          // 是否一键安装
          oneClick: false,
          // 允许权限提升
          allowElevation: true,
          // 每个用户安装
          perMachine: true,
          // 允许用户更改安装目录
          allowToChangeInstallationDirectory: true,
          // 创建桌面快捷方式
          createDesktopShortcut: true,
          // 创建开始菜单快捷方式
          createStartMenuShortcut: true
        },
        mac: {
          icon: './public/icon.icns',
          target: 'dmg'
        }
      }
    }
  }
}
  • 构建
yarn electron:build --win --mac

问题

element-ui按需引入Message组件,弹出空消息问题

  • Vue.use(Message) 改为 Vue.component(Message.name, Message)

moment-timezone Cannot read property 'split' of undefined

"dependencies": {
    ...
    "moment": "2.24.0",
    ...
  },
"resolutions": {
    "moment": "2.24.0"
  },

安装electron-icon-maker包卡住

  • 使用npm安装,增加-d参数查看详细下载信息,查看卡在下载哪个包上
 npm install electron-icon-maker -D -d

😢Maybe下一版本

  • 远程包查询功能
  • 开机自启动
  • 自动更新
  • 优化UI

资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349