【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架構也有不少不一樣於功能點須要多多考慮。第一次寫比較長的文章,箇中可能會有手誤或者知識錯誤,順序也不是最理想的。歡迎討論,也請各路大牛多多指教,指出不正!

相關文章
相關標籤/搜索