【easy-invoices】electron-vue、sqlite3 项目初探

父母都是作出纳相关的工做,但愿我能给他们作个简单的进销存,在上班的时候使用。开发一个不须要花钱买服务器,不须要依赖网络(更新除外),单机版的程序,对于前端出身的我来讲,那么electron或nwjs是最好的选择。
electron官网对electron与nwjs的比较
这里我选择了electron,由于很熟悉vue,就使用国人集成的electron-vue进行快速开发。本地数据库采用轻量嵌入型数据库sqlite3,不二之选。UI组件为iview。

物品管理
物品管理css

进出明细
进出明细html


1、环境准备

安装python2.7 和 Visual Studio 2015前端


2、安装vue-cli脚手架,初始化electron-vue目录

$ npm install -g vue-cli
$ vue init simulatedgreg/electron-vue easy-invoices

打包选择electron-builder。builder能够打包成具体文件,也能够是exe安装程序,而packager只能打包具体文件。下面会具体说明打包。
该命令会生成一个easy-invoices文件夹,大体目录以下(有细微变更)
目录结构vue

  • .electron-vue:主要是webpack配置,还有一些封装好了的命令行的输出,供开发、打包调试用。能够自行添加一些配置,如在webpack.render.config.js里添加less-loader和eslint-loader。
  • build:打包须要的素材,例如icon。打包后的默认目录也在于此
  • src:源码,main是主进程部分,render是渲染进程部分,下文会讲到这两个概念。index.ejs会被编译为html的入口。
  • static:静态资源
  • 有一些文件是我后来加上去的,好比eslint相关(.eslintrc.js,.eslintignore),与commit信息校验相关(verify_commmit_msg.js)等
  • .travis.yml为linux构建平台的配置,appveyor.yml为windows构建平台的配置。以后也会详细提到自动化构建。

3、sqlite3集成

nodejs中使用c++模块会涉及到编译问题,该编译经常会致使一些问题发生。
详细的操做请见个人另一篇文章《electron项目中使用sqlite3的编译问题(windows)》node


4、开始开发

在使用electron开发以前,咱们须要注意如下几点python

  • electron的运行依托于nodejs环境,渲染界面使用chromium。所以,咱们开发界面实则编写html,而且在开发的过程当中,可使用nodejs原生模块,好比fs文件模块、os系统模块等。这使得咱们的程序有更多的权限和功能,能够很是强大。但在强大的同时,开发者须要担起自身的责任,须要更多的去注意安全问题。
  • 在electron里,最核心的两个概念就是主进程和渲染进程。主进程负责整个程序的调度,控制一些功能,只有一个。而渲染进程负责界面的渲染。他们之间能够相互通讯。
  • electron加载html有两种方式,一种经过本地路径读取,一种经过http远程读取。远程读取会有许多限制,防止引发没必要要的安全隐患。electron-vue封装好了开发模式和生产模式,开发模式启动webpack-dev-server,渲染进程远程读取,生产模式打包至本地,渲染进程本地路径读取。
  • electron-vue将vue与webpack集成进来快速开发。前端界面使用vue去开发,并使用vue-router作单窗口路由控制。webpack进行模块打包与开发时的监听。electron-vue脚手架提供了vue-electron,并已经封装了这个方法,当运行环境为electron的时候,会将electron挂载在Vue.prototype上。electron对象上有许多api,详情请参考文档。
// vue入口文件
// src/renderer/main.js
if (!process.env.IS_WEB) Vue.use(require('vue-electron'));

...linux

1.主进程与渲染进程通讯

主进程向渲染进程发送消息: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

2. vue路由

程序刚启动的时候会在根路径下,咱们须要进行根路径的路由开发,或者将根路径重定向至开发的路由上。不然会一片白不显示

3. 前端日志

封装一个在开发环境下(环境变量:NODE_ENV=development)打印的函数,在关键的节点进行调用方便调试,好比sql语句等。我仅仅是使用console.log,也有其余的第三方浏览器日志插件可使用。
本项目里由于没有服务器可上报,因此没有作程序日志的收集,必要时能够去作一些本地日志存储,而且上报,好比错误信息、一些有意义的数据等。

4. sql语句编写

程序启动的时候执行建表的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;

5. 数据文件及用户配置、

考虑到用户手误卸载或者删除程序安装目录,将数据文件和用户配置存放在C:Users&dollar;{username}easy-invoices路径下。这样若是不当心删了,从新安装仍是能够和以前同样。作得更好一些能够在卸载的时候询问是否删除数据和配置(还没尝试过,不知道electron-builder是否支持)

6. 升级方案

不一样于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);

下载前能够拿到更新日志、时间、版本号和包大小,下载时能够拿到速度。部分效果展现:
20180813221631144.png
20180813221821255.png

7. 打包

前文提到,我采用的是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:打包成exe安装程序
  • build:dir:打包成文件形式

build:

  • productName:项目名
  • copyright:版权
  • directories:打包目录
  • win: windows配置。icon为程序图标目录,windows图标至少须要320 x 320,不然报错
  • nsis:windows安装程序exe配置,若是不配置,那么一键安装至C盘User一个local app目录下,不符合程序使用要求,这里我设置了oneClick:false和allowToChangeInstallationDirectory:true,就是不让程序一键安装,让用户去选择安装目录。
  • 其余如appId,dmg,linux、mac都是macOS和linux系统配置,没有仔细研究

8. CI自动构建发布

travis和appveyor是开源的两个自动化构建平台,免费服务于github等开源项目(不开源项目貌似要给钱)。若是你是在其余这两个CI平台不支持的仓库,可以使用其余构建工具,原理相同。

①. 在https://github.com/settings/tokens Generate new token,写上描述,勾上发布权限,生成token。该token只可见一次,注意保存

20180809161035898.png

②. https://www.appveyor.com/注册用户,使用github登陆。而后开启该项目的构建。

2018080916190511.png
20180809162000320.png

③. 将第一步生成的token填至项目环境变量,参数名为GH_TOKEN。发布的时候会自动使用GH_TOKEN进行github release api的调用。

20180809162324607.png

④. package.json
{
    "repository": {
        "type": "git",
        "url": "https://github.com/CaanDoll/easy-invoices.git"
    },
    "scripts":{
        "build:ci": "node .electron-vue/build.js && electron-builder --publish always"
    },
}
  • build:ci:执行后,不只打包,还会将打包后程序上传,发布成github的release草稿,手动编辑后便可发布。
⑥. appveyor.yml
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
  • version:为构建的版本号,会自增,这个和程序的版本号没有关系
  • branches:指定在哪一个分支进行构建
  • image:基础镜像,windows程序构建会用到vs
  • platform:系统位数:如x86(32位),x64(64位)
  • cache:npm缓存目录
  • init:初始执行命令,将全部代码换行符改成CRLF模式
  • install:安装包
  • build_script:执行命令

接下来提交在github master分支或者merge到master分支(申请merge以后也会触发)就能够触发构建了,在appveyor平台上能够看到。


5、其余一些细节

1.打开系统默认浏览器对应连接或者打开个人电脑对应文件目录

若是使用通常的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>

2.导出excel(下载文件)

若是在服务端进行导出,有两个步骤,第一步是将数据填充并生成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('下载失败')
  }

3.窗口相关

① 窗口栏

原生的窗口栏不是那么美观,咱们能够去掉原生窗口栏,本身写一个。
主进程

// 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架构也有不少不一样于功能点须要多多考虑。第一次写比较长的文章,个中可能会有手误或者知识错误,顺序也不是最理想的。欢迎讨论,也请各路大牛多多指教,指出不正!

相关文章
相关标签/搜索