Electron 實(shí)戰(zhàn)桌面計(jì)算器應(yīng)用
Electron 是一個(gè)搭建跨平臺(tái)桌面應(yīng)用的框架,僅僅使用 JavaScript、HTML 以及 CSS,即可快速而容易地搭建一個(gè)原生應(yīng)用。這對(duì)于想要涉及其他領(lǐng)域的開(kāi)發(fā)者來(lái)說(shuō)是一個(gè)非常大的福利。
項(xiàng)目介紹
倉(cāng)庫(kù)地址:lin-xin/calculator
我這里通過(guò) Electron 實(shí)現(xiàn)了仿 iPhone 的計(jì)算器,通過(guò)菜單可以切換橫屏和豎屏,橫屏有更多的運(yùn)算。而對(duì)于 JavaScript 進(jìn)行浮點(diǎn)數(shù)計(jì)算來(lái)說(shuō),精度丟失是個(gè)很大問(wèn)題,所以我這里使用了第三方庫(kù) math.js 來(lái)解決這個(gè)精度的問(wèn)題。
盡可能的實(shí)現(xiàn)了跟 iPhone 一樣的運(yùn)算:
- 1 + 2 × 3 = 7
- 3 += 6 (再按 = 等于 9)
- 0.1 + 0.2 = 0.3 (浮點(diǎn)數(shù)精度處理)
不過(guò)我下面并不是要講計(jì)算器,而是用到的 Electron 的知識(shí)點(diǎn)。
生命周期
在主進(jìn)程中通過(guò) app 模塊控制整個(gè)應(yīng)用的生命周期。
當(dāng) Electron 完成初始化時(shí)觸發(fā) ready 事件:
- app.on('ready', () => {
- // 創(chuàng)建窗口、加載頁(yè)面等操作
- })
當(dāng)所有的窗口都被關(guān)閉時(shí)會(huì)觸發(fā) window-all-closed 事件:
- app.on('window-all-closed', () => {
- if(process.platform !== 'darwin'){
- app.quit(); // 退出應(yīng)用
- }
- })
在開(kāi)發(fā)中發(fā)現(xiàn),沒(méi)有監(jiān)聽(tīng)該事件,打包后的應(yīng)用關(guān)閉后,進(jìn)程還保留著,會(huì)占用系統(tǒng)的內(nèi)存。
窗口
本來(lái)我們的 html 只顯示在瀏覽器中,而 electron 提供了一個(gè) BrowserWindow 模塊用于創(chuàng)建和控制瀏覽器窗口,我們的頁(yè)面就是顯示在這樣的窗口中。
創(chuàng)建窗口
通過(guò)關(guān)鍵字 new 實(shí)例化返回 win 對(duì)象,該對(duì)象有豐富的方法對(duì)窗口進(jìn)行控制。
- win = new BrowserWindow({
- width: 390, // 窗口寬度
- height: 670, // 窗口高度
- fullscreen: false, // 不允許全屏
- resizable: false // 不允許改變窗口size,不然布局就亂了啊
- });
加載頁(yè)面
窗口創(chuàng)建完是一片空白的,可以通過(guò) win.loadURL() 來(lái)加載要顯示的頁(yè)面。
- const path = require('path');
- const url = require('url');
- win.loadURL(url.format({ // 加載本地的文件
- pathname: path.join(__dirname, 'index.html'),
- protocol: 'file',
- slashes: true
- }))
也可以直接加載遠(yuǎn)程鏈接 win.loadURL(‘http://blog.gdfengshuo.com‘);
菜單
桌面應(yīng)用菜單欄是最常見(jiàn)的功能。Electron 提供了 Menu 模塊來(lái)創(chuàng)建原生的應(yīng)用菜單和 context 菜單,
- const template = [ // 創(chuàng)建菜單模板
- {
- label: '查看',
- submenu: [
- {label: '豎屏', type: 'radio', checked: true}, // type 屬性讓菜單為 radio 可選
- {label: '橫屏', type: 'radio', checked: false},
- {label: '重載',role:'reload'},
- {label: '退出',role:'quit'},
- ]
- }
- ]
- const menu = Menu.buildFromTemplate(template); // 通過(guò)模板返回菜單的數(shù)組
- Menu.setApplicationMenu(menu); // 將該數(shù)組設(shè)置為菜單
在子菜單中,通過(guò)點(diǎn)擊豎屏或橫屏來(lái)進(jìn)行一些操作,那就可以給 submenu 監(jiān)聽(tīng) click 事件。
- const template = [
- {
- label: '查看',
- submenu: [
- {
- label: '橫屏'
- click: () => { // 監(jiān)聽(tīng)橫屏的點(diǎn)擊事件
- win.setSize(670,460); // 設(shè)置窗口的寬高
- }
- }
- ]
- }
- ]
主進(jìn)程和渲染進(jìn)程通信
雖然點(diǎn)擊橫屏的時(shí)候,可以設(shè)置窗口的寬高,但是要如何去觸發(fā)頁(yè)面里的方法,這里就需要主進(jìn)程跟渲染進(jìn)程之間進(jìn)行通信。
主進(jìn)程,可以理解為 main.js 用來(lái)寫 electron api 的就是主進(jìn)程,渲染進(jìn)程就是渲染出來(lái)的頁(yè)面。
ipcMain
在主進(jìn)程中可以使用 ipcMain 模塊,它控制著由渲染進(jìn)程(web page)發(fā)送過(guò)來(lái)的異步或同步消息。
- const {ipcMain} = require('electron')
- ipcMain.on('send-message', (event, arg) => {
- event.sender.send('reply-message', 'hello world')
- })
ipcMain 監(jiān)聽(tīng) send-message 事件,當(dāng)消息到達(dá)時(shí)可以調(diào)用 event.sender.send 來(lái)回復(fù)異步消息,向渲染進(jìn)程發(fā)送 reply-message 事件,也可以帶著參數(shù)發(fā)送過(guò)去。
ipcRenderer
在渲染進(jìn)程可以調(diào)用 ipcRenderer 模塊向主進(jìn)程發(fā)送同步或異步消息,也可以收到主進(jìn)程的相應(yīng)。
- const {ipcRenderer} = require('electron')
- ipcRenderer.on('reply-message', (event, arg) => {
- console.log(arg); // hello world
- })
- ipcRenderer.send('anything', 'hello everyone');
ipcRenderer 可以監(jiān)聽(tīng)到來(lái)自主進(jìn)程的 reply-message 事件并拿到參數(shù)進(jìn)行操作,也可以使用 send() 方法向主進(jìn)程發(fā)送消息。
webContents
webContents 是一個(gè)事件發(fā)出者,它負(fù)責(zé)渲染并控制網(wǎng)頁(yè),也是 BrowserWindow 對(duì)象的屬性。在 ipcMain 中的 event.sender,返回發(fā)送消息的 webContents 對(duì)象,所以包含著 send() 方法用于發(fā)送消息。
- const win = BrowserWindow.fromId(1); // fromId() 方法找到ID為1的窗口
- win.webContents.on('todo', () => {
- win.webContents.send('done', 'well done!')
- })
remote
remote 模塊提供了一種在渲染進(jìn)程(網(wǎng)頁(yè))和主進(jìn)程之間進(jìn)行進(jìn)程間通訊(IPC)的簡(jiǎn)便途徑。在 Electron 中,有許多模塊只存在主進(jìn)程中,想要調(diào)用這些模塊的方法需要通過(guò) ipc 模塊向主進(jìn)程發(fā)送消息,讓主進(jìn)程調(diào)用這些方法。而使用 remote 模塊,則可以在渲染進(jìn)程中調(diào)用這些只存在于主進(jìn)程對(duì)象的方法了。
- const {remote} = require('electron')
- const BrowserWindow = remote.BrowserWindow // 訪問(wèn)主進(jìn)程中的BrowserWindow模塊
- let win = new BrowserWindow(); // 其他的跟主進(jìn)程的操作都一樣
remote 模塊除了可以訪問(wèn)主進(jìn)程的內(nèi)置模塊,自身還有一些方法。
- remote.require(module) // 返回在主進(jìn)程中執(zhí)行 require(module) 所返回的對(duì)象
- remote.getCurrentWindow() // 返回該網(wǎng)頁(yè)所屬的 BrowserWindow 對(duì)象
- remote.getCurrentWebContents() // 返回該網(wǎng)頁(yè)的 WebContents 對(duì)象
- remote.getGlobal(name) // 返回在主進(jìn)程中名為 name 的全局變量(即 global[name])
- remote.process // 返回主進(jìn)程中的 process 對(duì)象,等同于 remote.getGlobal('process') 但是有緩存
shell 模塊
使用系統(tǒng)默認(rèn)應(yīng)用管理文件和 URL,而且在主進(jìn)程和渲染進(jìn)程中都可以用到該模塊。在菜單中,我想點(diǎn)擊子菜單打開(kāi)一個(gè)網(wǎng)站,那么就可以用到 shell.openExternal() 方法,則會(huì)在默認(rèn)瀏覽器中打開(kāi) URL
- const {shell} = require('electron');
- shell.openExternal('https://github.com/lin-xin/calculator');
打包應(yīng)用
其實(shí)將程序打包成桌面應(yīng)用才是比較麻煩的事。我這里嘗試了 electron-packager 和 electron-builder。
electron-packager
electron-packager 可以將項(xiàng)目打包成各平臺(tái)可直接運(yùn)行的程序,而不是安裝包。
先使用 npm 安裝: npm install electron-packager -S
運(yùn)行打包命令:
- electron-packager ./ 計(jì)算器 --platform=win32 --overwrite --icon=./icon.ico
打包會(huì)把項(xiàng)目文件包括 node_modules 也一起打包進(jìn)去,當(dāng)然可以通過(guò) –ignore=node_modules 來(lái)忽略文件,但是如果項(xiàng)目中有用到第三方庫(kù),忽略的話則找不到文件報(bào)錯(cuò)了。
正確的做法就是嚴(yán)格區(qū)分 dependencies 和 devDependencies,打包的時(shí)候只會(huì)把 dependencies 的庫(kù)打包,而使用 cnpm 安裝的會(huì)有一大堆 .0.xx@xxx 的文件,也會(huì)被打包,所以***不要用 cnpm
electron-builder
electron-builder 是基于 electron-packager 打包出來(lái)的程序再做安裝處理,將項(xiàng)目打包成安裝文件。
安裝:npm install electron-builder -S
打包:electron-builder –win
打***程中,***次下載 electron 可能會(huì)出現(xiàn)連接超時(shí),可以使用 yarn 試試。還有 winCodeSign 和 nsis-resources 也可能會(huì)失敗,可以參考 electron-builder/issues 解決。
總結(jié)
Electron 用起來(lái)還是相對(duì)容易的,可以創(chuàng)建個(gè)簡(jiǎn)單的桌面應(yīng)用,只是打包的過(guò)程比較容易遇到問(wèn)題,網(wǎng)上好像也有一鍵打包的工具,沒(méi)嘗試過(guò)。以上也都是基于 windows 7 的實(shí)踐,畢竟沒(méi)有 Mac 搞不了。
【本文為51CTO專欄作者“林鑫”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)微信公眾號(hào)聯(lián)系作者獲取授權(quán)】