父母都是作出纳相关的工做,但愿我能给他们作个简单的进销存,在上班的时候使用。开发一个不须要花钱买服务器,不须要依赖网络(更新除外),单机版的程序,对于前端出身的我来讲,那么electron或nwjs是最好的选择。
electron官网对electron与nwjs的比较
这里我选择了electron,由于很熟悉vue,就使用国人集成的electron-vue进行快速开发。本地数据库采用轻量嵌入型数据库sqlite3,不二之选。UI组件为iview。
物品管理css
进出明细html
安装python2.7 和 Visual Studio 2015前端
$ npm install -g vue-cli $ vue init simulatedgreg/electron-vue easy-invoices
打包选择electron-builder。builder能够打包成具体文件,也能够是exe安装程序,而packager只能打包具体文件。下面会具体说明打包。
该命令会生成一个easy-invoices文件夹,大体目录以下(有细微变更)vue
nodejs中使用c++模块会涉及到编译问题,该编译经常会致使一些问题发生。
详细的操做请见个人另一篇文章《electron项目中使用sqlite3的编译问题(windows)》node
在使用electron开发以前,咱们须要注意如下几点python
// vue入口文件 // src/renderer/main.js if (!process.env.IS_WEB) Vue.use(require('vue-electron'));
...linux
主进程向渲染进程发送消息:webpack
// src/main/index.js import { BrowserWindow } from 'electron'; const mainWindow = new BrowserWindow(); mainWindow.webContents.send('messageOne', 'haha'); // 某vue组件 <script> export default { created(){ this.$electron.ipcRenderer.on('messageOne', (event, msg) =>{ console.log(msg); // 'haha' } } } <script>
渲染进程向主进程发送消息:c++
// src/main/index.js import { ipcMain } from 'electron'; ipcMain.on('messageTwo', (event,msg) => { console.log(msg) // 'haha' }); // 某vue组件 <script> export default { created(){ this.$electron.ipcRenderer.send('messageTwo', 'haha'); } } <script>
也能够用once,表明只监听一次。通信的方法还有多种,好比remote模块等。git
程序刚启动的时候会在根路径下,咱们须要进行根路径的路由开发,或者将根路径重定向至开发的路由上。不然会一片白不显示
封装一个在开发环境下(环境变量:NODE_ENV=development)打印的函数,在关键的节点进行调用方便调试,好比sql语句等。我仅仅是使用console.log,也有其余的第三方浏览器日志插件可使用。
本项目里由于没有服务器可上报,因此没有作程序日志的收集,必要时能够去作一些本地日志存储,而且上报,好比错误信息、一些有意义的数据等。
程序启动的时候执行建表的sql并捕获错误,若是表存在会抛出错误,这里咱们不用管。暴露出去db对象挂载在Vue.prototype上,便可全局调用,接下来就是在业务中各类拼接编(e)写(xin)sql语句了。
这里我并无封装数据模型或者使用sequelize等orm库,有兴趣的同窗能够尝试。
网上SQL教程与sqlite3教程也比较多,这么不一一描述,下面是代码片断:
// src/renderer/utils/db.js // 建表脚本,导出db对象供以后使用 import fse from 'fs-extra'; import path from 'path'; import sq3 from 'sqlite3'; import logger from './logger'; import { docDir } from './settings'; // 将数据存至系统用户目录,防止用户误删程序 export const dbPath = path.join(docDir, 'data.sqlite3'); fse.ensureFileSync(dbPath); const sqlite3 = sq3.verbose(); const db = new sqlite3.Database(dbPath); db.serialize(() => { /** * 物品表 GOODS * name 品名 * standard_buy_unit_price 标准进价 * standard_sell_unit_price 标准售价 * total_amount 总金额 * total_count 总数量 * remark 备注 * create_time 建立时间 * update_time 修改时间 */ db.run(`CREATE TABLE GOODS( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, standard_buy_unit_price DECIMAL(15,2) NOT NULL, standard_sell_unit_price DECIMAL(15,2) NOT NULL, total_amount DECIMAL(15,2) NOT NULL, total_count DECIMAL(15,3) NOT NULL, remark VARCHAR(255) NOT NULL, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL )`, err => { logger(err); }); /** * 进出明细表 GOODS_DETAIL_LIST * goods_id 物品id * count 计数(+加 -减) * actual_buy_unit_price 实际进价 * actual_sell_unit_price 实际售价 * amount 实际金额 * remark 备注 * latest 是否某物品最新一条记录(不是最新操做没法删除)(1是 0不是) * create_time 建立时间 * update_time 修改时间 */ db.run(`CREATE TABLE GOODS_DETAIL_LIST( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, goods_id INTEGER NOT NULL, count DECIMAL(15,3) NOT NULL, actual_sell_unit_price DECIMAL(15,2) NOT NULL, actual_buy_unit_price DECIMAL(15,2) NOT NULL, amount DECIMAL(15,2) NOT NULL, remark VARCHAR(255) NOT NULL, latest INTEGER NOT NULL, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL, FOREIGN KEY (goods_id) REFERENCES GOODS(id) )`, err => { logger(err); }); }); export default db;
考虑到用户手误卸载或者删除程序安装目录,将数据文件和用户配置存放在C:Users${username}easy-invoices路径下。这样若是不当心删了,从新安装仍是能够和以前同样。作得更好一些能够在卸载的时候询问是否删除数据和配置(还没尝试过,不知道electron-builder是否支持)
不一样于B/S架构,C/S架构必需要作好本身的升级方案,不然用户装好了程序就没法再进行更新了。
主进程使用electron-updater来控制自动更新,渲染进程来作更新的逻辑,每一个程序更新的流程都不同,个人程序是每次启动检测更新,若是有更新就自动下载,下载完成后提示用户是否须要重启更新,用户选择取消则每次开启的时候都会提示一下,用户选择升级那么就重启升级。
由于个人程序是托管在github上,因此不须要设置feedurl(feedurl有默认值,和打包设置有关,个人项目中默认会去github的release api上检测)。若是放在其余服务器上,须要编写检测接口并设置url。electron-updater官方文档
下面是代码片断
$ npm i electron-updater
主进程中
// src/main/index.js import { autoUpdater } from 'electron-updater'; app.on('ready', () => { if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdatesAndNotify(); }); function sendUpdateMessage(message, data) { //往渲染进程发送消息,mainWindow来自new BrowserWindow mainWindow.webContents.send('update-message', { message, data }); } // 阻止程序关闭自动安装升级 autoUpdater.autoInstallOnAppQuit = false; autoUpdater.on('error', data => { sendUpdateMessage('error', data); }); /* // 检查更新 autoUpdater.on('checking-for-update', data => { sendUpdateMessage('checking-for-update', data); });*/ // 有可用更新 autoUpdater.on('update-available', data => { sendUpdateMessage('update-available', data); }); // 已经最新 autoUpdater.on('update-not-available', data => { sendUpdateMessage('update-not-available', data); }); // 更新下载进度事件 autoUpdater.on('download-progress', data => { sendUpdateMessage('download-progress', data); }); // 更新下载完成事件 autoUpdater.on('update-downloaded', () => { sendUpdateMessage('update-downloaded', {}); ipcMain.once('update-now', () => { autoUpdater.quitAndInstall(); }); });
注意:在升级中可能会有改表结构的操做,我在settings.json里存有版本信息,启动的时候将程序的版本号与settings里面的版本号对比,进行升级,升级完成以后将settings里的版本设置为程序版本
// src/renderer/utils/upgrade.js import settings from './settings'; import packageJson from '../../../package.json'; // 程序当前版本 const appCurrentVersion = packageJson.version; import db from './db'; // 罗列增量升级脚本 const incrementalUpgrade = { '1.0.1':()=>{ db.run( //修改表数据、结构的脚本等 ); }, '1.0.2':()=>{ db.run( //修改表数据、结构的脚本等 ); }, } // 升级前版本 const beforeUpgradeVersion = settings.get('version'); // 用户可能有不少个版本没有升级,寻找执行的脚本 增量执行。 // 遍历incrementalUpgrade对象,大于beforeUpgradeVersion的脚本都要依次执行。(比较时能够把点去掉转为数字类型比较) ... // 脚本执行完毕 settings.set('version', appCurrentVersion);
下载前能够拿到更新日志、时间、版本号和包大小,下载时能够拿到速度。部分效果展现:
前文提到,我采用的是electron-builder进行打包。electron-builder官方文档
打包的主要配置在package.json里:
{ "scripts":{ "build": "node .electron-vue/build.js && electron-builder", "build:dir": "node .electron-vue/build.js && electron-builder --dir" }, "build": { "productName": "easy-invoices", "copyright": "caandoll", "appId": "org.caandoll.easy-invoices", "directories": { "output": "build" }, "files": [ "dist/electron/**/*" ], "dmg": { "contents": [ { "x": 410, "y": 150, "type": "link", "path": "/Applications" }, { "x": 130, "y": 150, "type": "file" } ] }, "mac": { "icon": "build/icons/icon.png" }, "win": { "icon": "build/icons/icon.png" }, "linux": { "icon": "build/icons/icon.png" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true } } }
scripts:
build:
travis和appveyor是开源的两个自动化构建平台,免费服务于github等开源项目(不开源项目貌似要给钱)。若是你是在其余这两个CI平台不支持的仓库,可以使用其余构建工具,原理相同。
{ "repository": { "type": "git", "url": "https://github.com/CaanDoll/easy-invoices.git" }, "scripts":{ "build:ci": "node .electron-vue/build.js && electron-builder --publish always" }, }
version: 0.0.{build} branches: only: - master image: Visual Studio 2017 platform: - x64 cache: - node_modules - '%APPDATA%\npm-cache' - '%USERPROFILE%\.electron' - '%USERPROFILE%\AppData\Local\Yarn\cache' init: - git config --global core.autocrlf input install: - ps: Install-Product node 8 x64 - yarn build_script: - yarn build:ci test: off
接下来提交在github master分支或者merge到master分支(申请merge以后也会触发)就能够触发构建了,在appveyor平台上能够看到。
若是使用通常的a标签,会直接将程序的界面跳转至这个连接,由于自己就是浏览器内核。加上target:_blank的话更会没有反应了。这个时候须要调用electron.shell。上面的openExternal(url)方法就是打开浏览器,openItem(path)打开文件目录。
// vue入口文件 // src/renderer/main.js if (!process.env.IS_WEB) Vue.use(require('vue-electron')); // 某页面组件xxx.vue <script> export default { methods: { openUrl(url) { this.$electron.shell.openExternal(url); }, openPath(path) { this.$electron.shell.openItem(path); }, } }; </script>
若是在服务端进行导出,有两个步骤,第一步是将数据填充并生成excel,第二步是将文件发送出去。使用electron本地进行导出也不例外,但由于不是调用http接口,会有一些差别。
nodejs生成excel在这里就很少描述,之后我会补充相应的文章。在这里先推荐这两个库,若是生成的excel比较简单,横行数列并无任何样式的,可使用node-xlsx。若是须要生成较为复杂的excel,好比有样式要求,有合并单元格的需求,可使用ejsExcel。
假设咱们已经导出了一个名为test.xlsx的excel在系统临时目录(os.tmpdir()):C:UsersusernameAppDataLocalTempappnametest.xlsx
// src/main/index.js import { ipcMain } from 'electron'; // mainWindow来自new BrowserWindow ipcMain.on('download', (event, downloadPath) => { mainWindow.webContents.downloadURL(downloadPath);// 这个时候会弹出让用户选择下载目录 mainWindow.webContents.session.once('will-download', (event, item) => { item.once('done', (event, state) => { // 成功的话 state为completed 取消的话 state为cancelled mainWindow.webContents.send('downstate', state); }); }); }); // 渲染进程 ipcRenderer.send('download', 'C:\Users\username\AppData\Local\Temp\appname\test.xlsx'); ipcRenderer.once('downstate', (event, arg) => { if (arg === 'completed') { console.log('下载成功'); } else if (arg === 'cancelled'){ console.log('下载取消'); } else { console.log('下载失败') }
原生的窗口栏不是那么美观,咱们能够去掉原生窗口栏,本身写一个。
主进程
// src/main/index.js import { BrowserWindow、ipcMain } from 'electron'; // 建立窗口时配置 const mainWindow = new BrowserWindow({ frame: false, // 去掉原生窗口栏 ... }); // 主进程监听事件进行窗口最小化、最大化、关闭 // 窗口最小化 ipcMain.on('min-window', () => { mainWindow.minimize(); }); // 窗口最大化 ipcMain.on('max-window', () => { if (mainWindow.isMaximized()) { mainWindow.restore(); } else { mainWindow.maximize(); } }); // 关闭 ipcMain.on('close-window', () => { mainWindow.close(); });
头部组件或其余组件,这样就能够在本身定义的元素上去执行窗口操做了
<script> export default { methods: { minWindows() { this.$electron.ipcRenderer.send('min-window'); }, maxWindows() { this.$electron.ipcRenderer.send('max-window'); }, closeWindows() { this.$electron.ipcRenderer.send('close-window'); }, }; </script>
css设置拖拽区域,拖拽区域会自动有双击最大化的功能,注意:拖拽区域内的点击、移入移出等事件将无效,须要将拖拽区域内的按钮等元素设为非拖拽区域便可
header { -webkit-app-region: drag; // 拖拽区域 .version { .ivu-tooltip { -webkit-app-region: no-drag; // 非拖拽区域 } } .right { a { -webkit-app-region: no-drag; // 非拖拽区域 } } }
程序启动时,界面渲染须要必定时间,致使白屏一下,体验很差。解决方案一种是将程序的背景色设为html的背景色,另一种就是等界面加载完毕以后再显示窗口,代码以下:
主进程中
// src/main/index.js import { BrowserWindow} from 'electron'; const mainWindow = new BrowserWindow({ show: false, ... }); // 加载好html再呈现window,避免白屏 mainWindow.on('ready-to-show', () => { mainWindow.show(); mainWindow.focus(); });
electron很是好玩,它解放了咱们在浏览器中开发界面的束缚。C/S架构也有不少不一样于功能点须要多多考虑。第一次写比较长的文章,个中可能会有手误或者知识错误,顺序也不是最理想的。欢迎讨论,也请各路大牛多多指教,指出不正!