父母都是作出納相關的工做,但願我能給他們作個簡單的進銷存,在上班的時候使用。開發一個不須要花錢買服務器,不須要依賴網絡(更新除外),單機版的程序,對於前端出身的我來講,那麼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架構也有不少不一樣於功能點須要多多考慮。第一次寫比較長的文章,箇中可能會有手誤或者知識錯誤,順序也不是最理想的。歡迎討論,也請各路大牛多多指教,指出不正!