簡單來講,Electron就是可讓你用Javascript、HTML、CSS來編寫運行於Windows、macOS、Linux系統之上的桌面應用的庫。本文的目的是經過使用Electron開發一個完整但簡單的小應用:記事本,來體驗一下這個神器的開發過程。本文猶如Hello World
同樣的存在,是個入門級筆記,但若是你以前從未接觸過Electron,而又對它有興趣,某想信這會是一篇值得一看的入門教程。
PS:這篇文章是基於Windows的開發過程,未對macOS、Linux做測試。javascript
點擊 這裏 進入官網下載、安裝。php
因爲衆所周知的緣由,你須要一個cnpm
代替npm
,這裏 是官網。安裝命令(打開系統的cmd.exe來執行命令):css
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm install -g electron
這是一個相似於傻瓜開發包的Electron工具整合項目。具體介紹點擊 這裏。html
cnpm install -g electron-forge
H:\Electron
目錄下,項目名爲notepad
(字母所有小寫,多個單詞之間能夠用「-」鏈接)。cmd.exe
,一路cd到H:\Electron
。(也能夠在Electron
文件夾下,按住Shift
鍵並右鍵單擊空白處,選擇在此處打開命令窗口
來啓動cmd.exe
。)notepad
的項目文件夾,同時安裝項目所須要的模塊、依賴項等。electron-forge init notepad
notepad
目錄下,執行下面的命令來啓動app(也能夠簡單的用npm start
來運行)。electron-forge start
這樣就能夠看到基本的app界面了。java
notepad
文件夾整個拖到VS Code中打開(或者點菜單文件-打開文件夾
選擇notepad
文件夾打開項目),能夠看一下項目的目錄結構:node_modules
文件夾下是各類模塊、類庫,src
下是app的源代碼文件,package.json
是描述包的文件。package.json
,注意這裏默認已經將主進程入口文件配置爲index.js
(而不是main.js
)。src/index.js
改爲src/main.js
,同時也要將文件index.js
更名爲main.js
。main.js
,這是app主進程的入口,在這裏建立了mainWindow
瀏覽器窗口,使用mainWindow.loadURL("file://${__dirname}/index.html")
來加載index.html
主頁;使用mainWindow.webContents.openDevTools()
來打開開發者工具用於調試(這個操做一般在發佈app時刪除)。而後是app的事件處理:ready
: 當Electron完成初始化後觸發,這裏初始化後就會去建立瀏覽器窗口並加載主頁面。window-all-closed
: 當全部瀏覽器窗口被關閉後觸發,通常此時就退出應用了。activate
: 當app激活時觸發,通常針對macOS要須要處理。index.html
,這是主頁面,除了顯示Well hey there!!!
的信息外,沒什麼具體內容。main.js
和index.html
。main.js
是主進程入口,index.html
是一個web頁面,它須要使用一個瀏覽器窗口(BrowserWindow
)來加載和顯示,做爲應用的UI,它處在一個獨立的渲染進程中。app啓動時執行main.js
中的代碼建立窗口,加載頁面等。主進程與渲染進程之間不能直接互相訪問,須要經過ipcMain
和ipcRenderer
進行IPC通訊(Inter-process communication),或者使用remote
模塊在渲染進程中使用主進程中的資源(反過來,在主進程中使用webContents.executeJavascript
方法能夠訪問渲染進程)。這裏將實現一個相似於Windows的記事本的App。這個App具有如下功能:node
File
, Edit
, View
, Help
四個主菜單。重點是File
菜單下的三個子菜單:New
(新建文件)、Open
(打開文件)、Save
(保存文件),這三個菜單須要自定義點擊事件,其它的菜單基本使用內建的方法處理,因此沒什麼難度。*
號以表示文檔還沒有保存。.txt
, .js
, .html
, .md
等文本文件;能夠將文本內容保存爲本地文本文件。在打開或新建文件前,若是當前文檔還沒有保存,會提示用戶先保存文檔。因爲主進程與渲染進程不能直接互相訪問,因此部分細節有必要先考慮清楚。git
Open
(打開文件)命令,就須要與渲染進程進行通訊,這可使用ipcMain
和ipcRenderer
來實現。remote
模塊,能夠在渲染進程中調用主進程的對象和方法,而無需顯式地發送進程間消息,因此這一部分能夠由它來實現。PS:對於從主進程訪問渲染進程(反向操做),可使用webContents.executeJavascript
方法。關閉
按鈕,或者點擊Exit
菜單就會關閉窗口退出程序。在退出時,有必要檢查文檔是否須要保存,若是還沒有保存就提示用戶保存。要實現這一效果,首先,在主進程監測到用戶關閉窗口時,向渲染進程發送一個特定的消息代表窗口準備關閉,渲染進程得到該消息後查看文檔是否須要保存,若是須要就彈窗提示用戶保存,用戶保存或取消保存後,渲染進程再向主進程發送一個消息代表能夠關閉程序了,主進程得到該消息後關閉窗口退出程序。這個過程也由ipcMain
和ipcRenderer
來實現。整個App功能比較簡單,最終實現後也只用到了三個主要文件,包括:main.js
,index.html
,index.js
。github
這是主進程的入口,在這裏建立App窗口,生成菜單,載入頁面等。下面是該文件的完整源碼,二個//-------
之間是某根據功能須要添加的代碼,其他是模板自動生成的代碼。web
import { app, BrowserWindow } from 'electron'; //----------------------------------------------------------------- import { Menu, MenuItem, dialog, ipcMain } from 'electron'; import { appMenuTemplate } from './appmenu.js'; //是否能夠安全退出 let safeExit = false; //----------------------------------------------------------------- // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow; const createWindow = () => { // Create the browser window. mainWindow = new BrowserWindow({ width: 800, height: 600, }); // and load the index.html of the app. mainWindow.loadURL(`file://${__dirname}/index.html`); // Open the DevTools. //mainWindow.webContents.openDevTools(); //----------------------------------------------------------------- //增長主菜單(在開發測試時會有一個默認菜單,但打包後這個菜單是沒有的,須要本身增長) const menu=Menu.buildFromTemplate(appMenuTemplate); //從模板建立主菜單 //在File菜單下添加名爲New的子菜單 menu.items[0].submenu.append(new MenuItem({ //menu.items獲取是的主菜單一級菜單的菜單數組,menu.items[0]在這裏就是第1個File菜單對象,在其子菜單submenu中添加新的子菜單 label: "New", click(){ mainWindow.webContents.send('action', 'new'); //點擊後向主頁渲染進程發送「新建文件」的命令 }, accelerator: 'CmdOrCtrl+N' //快捷鍵:Ctrl+N })); //在New菜單後面添加名爲Open的同級菜單 menu.items[0].submenu.append(new MenuItem({ label: "Open", click(){ mainWindow.webContents.send('action', 'open'); //點擊後向主頁渲染進程發送「打開文件」的命令 }, accelerator: 'CmdOrCtrl+O' //快捷鍵:Ctrl+O })); //再添加一個名爲Save的同級菜單 menu.items[0].submenu.append(new MenuItem({ label: "Save", click(){ mainWindow.webContents.send('action', 'save'); //點擊後向主頁渲染進程發送「保存文件」的命令 }, accelerator: 'CmdOrCtrl+S' //快捷鍵:Ctrl+S })); //添加一個分隔符 menu.items[0].submenu.append(new MenuItem({ type: 'separator' })); //再添加一個名爲Exit的同級菜單 menu.items[0].submenu.append(new MenuItem({ role: 'quit' })); Menu.setApplicationMenu(menu); //注意:這個代碼要放到菜單添加完成以後,不然會形成新增菜單的快捷鍵無效 mainWindow.on('close', (e) => { if(!safeExit){ e.preventDefault(); mainWindow.webContents.send('action', 'exiting'); } }); //----------------------------------------------------------------- // Emitted when the window is closed. mainWindow.on('closed', () => { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null; }); }; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', createWindow); // Quit when all windows are closed. app.on('window-all-closed', () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) { createWindow(); } }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. //----------------------------------------------------------------- //監聽與渲染進程的通訊 ipcMain.on('reqaction', (event, arg) => { switch(arg){ case 'exit': //作點其它操做:好比記錄窗口大小、位置等,下次啓動時自動使用這些設置;不過由於這裏(主進程)沒法訪問localStorage,這些數據須要使用其它的方式來保存和加載,這裏就不做演示了。這裏推薦一個相關的工具類庫,可使用它在主進程中保存加載配置數據:https://github.com/sindresorhus/electron-store //... safeExit=true; app.quit();//退出程序 break; } }); //-----------------------------------------------------------------
首先,app.on('ready', createWindow)
也就是當Electron完成初始化後,就調用createWindow
方法來建立瀏覽器窗口mainWindow
(與主進程只能有1個不一樣,能夠根據須要適時建立更多個瀏覽器窗口,這些窗口由主進程負責建立和管理,每一個瀏覽器窗口使用一個獨立的渲染進程;本文只需使用一個瀏覽器窗口,即mainWindow
)。同時,使用Menu.buildFromTemplate(appMenuTemplate)
經過一個菜單模板來建立app應用主菜單,模板代碼存放在appmenu.js
文件中(這個文件包含在本文的源碼中,也能夠點擊這裏查看),這個模板的寫法能夠參考官方的 Electron API Demos
中Customize Menus
的例子。模板的第一個菜單是File
菜單,它的子菜單被設計成空的,在這裏使用menu.items[0].submenu.append
方法向這個File
菜單添加四個子菜單,分別是:New
(新建文檔),Open
(打開文檔),Save
(保存文檔),Exit
(退出程序)。其中,前三個菜單在點擊後都會向渲染進程發送信息,通知渲染進程執行相關處理。如對於New
菜單,使用mainWindow.webContents.send('action', 'new')
的方式,通知渲染進程要新建一個文檔。渲染進程會使用ipcRenderer.on
方法來執行監聽,監聽到消息後就會執行相應處理(這部分在index.js
中實現)。最後使用Menu.setApplicationMenu(menu)
將主菜單安裝到瀏覽器窗體中(全部窗體會共享主菜單)。npm
這是App的文本編輯頁面。這個頁面很簡單,整個頁面就只有一個TextArea
控件(id爲txtEditor
),平鋪滿整個窗口。該頁面使用require('./index.js')
載入index.js
。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Notepad</title> <style type="text/css"> body,html{ margin:0px; height:100%; } #txtEditor{ width:100%; height:99.535%; padding:0px; margin:0px; border:0px; font-size: 18px; } </style> </head> <body> <textarea id="txtEditor"></textarea> </body> <script> require('./index.js'); </script> </html>
全部主頁面index.html
涉及到的頁面處理、與主進程交互等的操做都會放到該js文件中。該文件完整代碼:
import { ipcRenderer, remote } from 'electron'; const { Menu, MenuItem, dialog } = remote; let currentFile = null; //當前文檔保存的路徑 let isSaved = true; //當前文檔是否已保存 let txtEditor = document.getElementById('txtEditor'); //得到TextArea文本框的引用 document.title = "Notepad - Untitled"; //設置文檔標題,影響窗口標題欄名稱 //給文本框增長右鍵菜單 const contextMenuTemplate=[ { role: 'undo' }, //Undo菜單項 { role: 'redo' }, //Redo菜單項 { type: 'separator' }, //分隔線 { role: 'cut' }, //Cut菜單項 { role: 'copy' }, //Copy菜單項 { role: 'paste' }, //Paste菜單項 { role: 'delete' }, //Delete菜單項 { type: 'separator' }, //分隔線 { role: 'selectall' } //Select All菜單項 ]; const contextMenu=Menu.buildFromTemplate(contextMenuTemplate); txtEditor.addEventListener('contextmenu', (e)=>{ e.preventDefault(); contextMenu.popup(remote.getCurrentWindow()); }); //監控文本框內容是否改變 txtEditor.oninput=(e)=>{ if(isSaved) document.title += " *"; isSaved=false; }; //監聽與主進程的通訊 ipcRenderer.on('action', (event, arg) => { switch(arg){ case 'new': //新建文件 askSaveIfNeed(); currentFile=null; txtEditor.value=''; document.title = "Notepad - Untitled"; //remote.getCurrentWindow().setTitle("Notepad - Untitled *"); isSaved=true; break; case 'open': //打開文件 askSaveIfNeed(); const files = remote.dialog.showOpenDialog(remote.getCurrentWindow(), { filters: [ { name: "Text Files", extensions: ['txt', 'js', 'html', 'md'] }, { name: 'All Files', extensions: ['*'] } ], properties: ['openFile'] }); if(files){ currentFile=files[0]; const txtRead=readText(currentFile); txtEditor.value=txtRead; document.title = "Notepad - " + currentFile; isSaved=true; } break; case 'save': //保存文件 saveCurrentDoc(); break; case 'exiting': askSaveIfNeed(); ipcRenderer.sendSync('reqaction', 'exit'); break; } }); //讀取文本文件 function readText(file){ const fs = require('fs'); return fs.readFileSync(file, 'utf8'); } //保存文本內容到文件 function saveText(text, file){ const fs = require('fs'); fs.writeFileSync(file, text); } //保存當前文檔 function saveCurrentDoc(){ if(!currentFile){ const file = remote.dialog.showSaveDialog(remote.getCurrentWindow(), { filters: [ { name: "Text Files", extensions: ['txt', 'js', 'html', 'md'] }, { name: 'All Files', extensions: ['*'] } ] }); if(file) currentFile=file; } if(currentFile){ const txtSave=txtEditor.value; saveText(txtSave, currentFile); isSaved=true; document.title = "Notepad - " + currentFile; } } //若是須要保存,彈出保存對話框詢問用戶是否保存當前文檔 function askSaveIfNeed(){ if(isSaved) return; const response=dialog.showMessageBox(remote.getCurrentWindow(), { message: 'Do you want to save the current document?', type: 'question', buttons: [ 'Yes', 'No' ] }); if(response==0) saveCurrentDoc(); //點擊Yes按鈕後保存當前文檔 }
首先,前面說了,在渲染進程中不能直接訪問菜單,對話框等,它們只存在於主進程中,但能夠經過remote
來使用這些資源。
import { remote } from 'electron'; const { Menu, MenuItem, dialog } = remote;
而後,const contextMenu=Menu.buildFromTemplate(contextMenuTemplate)
即便用contextMenuTemplate
模板來建立編輯器的右鍵菜單(雖然建立過程在渲染進程中進行,但實際上使用remote
來建立的菜單、對話框等,仍然只存在於主進程內),因爲這裏涉及到的菜單都只須要使用系統的內建功能,不須要自定義,因此這裏比較簡單。使用txtEditor.addEventListener('contextmenu')
來監聽右鍵菜單請求,使用contextMenu.popup(remote.getCurrentWindow())
來彈出右鍵菜單。
txtEditor.oninput
用於監控文本框內容變化,若是有改變,則將文檔標記爲還沒有保存,並在標題欄最右側顯示一個*
號做爲提示。
PS:在Win7上若是沒有啓用Aero效果,使用document.title = xxx
或remote.getCurrentWindow().setTitle(xxx)
都看不到程序標題欄的標題變化,只當你好比縮放一下窗口後這個修改纔會被刷新。
ipcRenderer.on
用於監聽由主進程發來的消息。前面說過,主進程使用mainWindow.webContents.send('action', 'new')
的方式向渲染進程發送特定消息,渲染進程監聽到消息後,根據消息內容作出相應處理。好比,這裏,當主進程發來new
的消息後,渲染進程就開始着手新建一個文檔,在新建前會使用askSaveIfNeed
方法檢測文檔是否須要保存,並提示用戶保存;對於open
的消息就會調用remote.dialog.showOpenDialog
來顯示一個文件打開對話框,由用戶選擇要打開的文檔而後加載文本數據;而對於save
消息就會對當前文檔進行保存操做。
正如前面在App功能細節中討論的同樣,在關閉程序前,友好的作法是檢測文檔是否須要保存,若是還沒有保存,通知用戶保存。要實現這一功能,須要在主進程和渲染進程間進行相互通訊,以得到窗體關閉和文檔保存的確認,實現安全退出。
首先在main.js
中,使用mainWindow.on('close')
來監控mainWindow
窗口的關閉。
mainWindow.on('close', (e) => { if(!safeExit){ e.preventDefault(); mainWindow.webContents.send('action', 'exiting'); } });
這裏safeExit
開關用於標記渲染進程是否已經向主進程反饋它已經完成全部操做了。若是還沒有反饋,則使用e.preventDefault()
阻止窗口關閉,並使用mainWindow.webContents.send('action', 'exiting')
向渲染進程發送一個exiting
消息,告訴渲染進程:嘿,我要關掉窗口了,你趕忙看看還要什麼沒作完的,作完後通知我。
既然主進程要等渲染進程的反饋,就須要監聽渲染進程發回的消息,因此主進程使用ipcMain.on
來執行監聽。若是渲染進程發送一個exit
消息過來,就表示能夠安全退出了。
ipcMain.on('reqaction', (event, arg) => { switch(arg){ case 'exit': safeExit=true; app.quit(); break; } });
在渲染進程這邊的index.js
中,在ipcRenderer.on
監聽方法中,相應的有一個消息處理是針對主進程發來的exiting
消息的,當獲知主進程準備關閉窗口,渲染進程就先去檢查文檔是否保存過了,若是還沒有保存就通知用戶保存,用戶保存或取消保存後,使用ipcRenderer.sendSync('reqaction', 'exit')
來向主進程發送一個exit
消息,表示:我要作的都作完了,你想退就退吧。
case 'exiting': askSaveIfNeed(); ipcRenderer.sendSync('reqaction', 'exit'); break;
主進程監聽到這個消息後,將safeExit
標記爲true
,表示已經獲得渲染進程的確認,而後就可使用app.quit()
安全退出了。固然,在退出前,能夠再執行一些其它操做(好比保存參數配置等)。
npm run make
該命令會將文件打包到當前項目目錄下的out
文件夾下。打包後發現,源碼直接暴露在[app項目目錄]\out\notepad-win32-x64\resources\app\src
目錄下。
package.json
,在electronPackagerConfig
部分添加"asar": true
。"electronPackagerConfig": { "asar": true }
從新打包後源碼文件會被打包進app.asar
文件中(該文件仍然在src
目錄下)。
notepad.exe
啓動程序。