介紹我們將在接下來的幾章中構建的應用程序
配置我們的CSS樣式表,使其看起來更像一個本機應用程序
回顧在Electron中主進程和渲染器進程之間的關系
為我們的主進程和渲染器進程實現基本功能
在Electron渲染進程中訪問Chrome開發者工具
我們的書簽管理器是一個很好的開始,但它只觸及了我們可以用Electron做什么。
在本章中,我們將更深入地探討,並為與用戶操作系統建立更緊密聯系的應用程序打下基礎。在接下來的幾章中,我們將實現觸發操作系統用戶界面,對文件系統進行讀寫和訪問剪貼板的功能。
我們正在構建一個簡單的Markdown編輯器,它允許我們創建新的或打開現有的Markdown文件,將它們轉換為HTML,並將HTML保存到文件系統和剪貼板中。讓我們把這個應用程序稱為Fire Sale,因為它畢竟是一個廉價編輯器,只是稍微聰明一點而已。
在本章的最后,我們將討論在出現問題時調試Electron應用程序的技術和工具。
定義我們的應用
讓我們從為我們不起眼的小應用程序設置目標開始。
對於桌面應用程序,我們的許多特性可能看起來有些平庸,這就是重點。它們是桌面應用程序的標准配置,但完全超出了傳統web應用程序的能力范圍,傳統web應用程序無法訪問獨立瀏覽器選項卡之外的任何內容。
我們的應用程序將由兩個窗格組成,用戶可以編寫或編輯Markdown和一個右窗格,該窗格以HTML形式呈現用戶的Markdown。在頂部有一系列按鈕,允許用戶從文件系統加載文本文件,並將結果寫入剪貼板或文件系統。
在應用程序的第一階段,我們構建了以下的界面。在圖3.1。我們還可以向效果圖(以及隨后的應用程序)添加額外的用戶界面元素,但這是一個很好的開始。
圖3.1 我們的應用程序的線框顯示,用戶可以在左側窗格中輸入文本,或者從用戶的文件系統的文件中加載文本。
在這一章中,我們為我們的應用奠定了基礎。我們創建項目的結構,安裝依賴項,設置主進程和呈現器進程,構建用戶界面,並在用戶向左側窗格輸入文本時實現markdown到HTML的轉換。
我們將在接下來的幾章中分階段構建應用程序的其余部分。在每一章中,您將下載我們應用程序的當預期目標代碼。通過這種方式,您可以切換到一個章節,其中包含您感興趣的功能,而不必從頭構建整個應用程序。
在第一階段,我們的應用程序將能夠
-
打開並保存文件到文件系統
-
從這些文件獲取Markdown內容
-
將Markdown內容呈現為HTML
-
將生成的HTML保存到文件系統中
-
將生成的HTML寫入剪貼板
在后面的章節中,我們的應用程序使用本地操作系統接口跟蹤最近打開的文檔。我們可以將Markdown文件從Finder或Windows資源管理器拖放到應用程序上,並讓應用程序立即打開該Markdown文件。當我們右鍵單擊應用程序的不同區域時,應用程序將有自己的自定義應用程序菜單和自定義上下文菜單。
我們還利用了操作系統特有的特性,比如更新應用程序的標題欄,以顯示當前打開的文件,以及自上次保存以來是否已經更改。如果計算機上的其他應用程序在打開文件時更改了文件,我們還實現了其他功能,比如更新應用程序中的內容。
奠定基礎
如圖3.2所示的文件結構與我們在前一章中商定並用於書簽管理器的結構非常相似。
為了簡化和清晰,在我們繼續熟悉Electron時,我們在app/main.js
中保存了主進程的所有代碼,在app/renderer.js
中保存了單渲染器進程的所有代碼。我們將app文件夾存儲在基於unix的操作系統上,以便能夠快速生成它,如下面的清單所示。或者,您可以在GitHub上查看這個項目的主分支,網址是https://github.com/electron-in-action/firesale。
圖3.2 我們工程結構
列表3.1 生成應用文件結構
mkdir app && touch app/index.html app/main.js app/renderer.js app/style.css
項目的各個部分是
-
index.html-包含所有為UI提供結構的HTML標記
-
main.js-包含我們的主進程的代碼
-
renderer.js-包含UI的所有交互代碼
-
style.css-包含樣式的CSS
-
package.json-包含所有依賴項,並在啟動主進程時將Electron指向main.js
為了簡單起見,除了Electron之外,我們還從兩個依賴項開始作為運行時。我們使用一個名為marked的庫來處理Markdown到HTML轉換的繁重工作。
對於這個項目,通過運行npm init --yes生成一個package.json
。--yes標記允許您跳過前一章中的提示。生成package.json之后,運行以下命令安裝必要的依賴項:
npm install electron marked --save
圖3.3 Electron首先尋找我們的主進程,它負責生成一個或多個渲染器進程,其負責顯示我們的UI。
引導程序
在我們package.json的main條目被配置為加載index.js作為應用程序的主進程。如圖3.3所示,我們需要將其調整為app/main.js
。我們還需要一個渲染器進程,為用戶提供應用程序的界面。在app/main.js中,讓我們添加如下代碼。
列表3.2 引導主進程: ./app/main.js
1 const{ app, BrowserWindow } = require('electron') 2 3 //在頂層聲明mainWindow,以便在“ready”事件完成后不會將其回收為垃圾 4 let mainWindow = null; 5 6 app.on('ready', () => { 7 //使用默認屬性創建一個新的BrowserWindow 8 mainWindow = new BrowserWindow({ 9 webPreferences: { 10 // webPreferences中的nodeIntegrationInWorker選項設置為true,Electron5.x以后,缺省為false 11 nodeIntegration: true 12 } 13 }) 14 15 //在剛才創建的BrowserWindow實例中加載app/index.html 16 mainWindow.loadFile('app/index.html'); 17 18 mainWindow.on('closed', () => { 19 //在窗口關閉時將進程設置為null 20 mainWindow = null; 21 }); 22 });
這足以啟動我們的應用程序。也就是說,由於我們的主進程目前在渲染器進程中加載了一個空文件,所以沒有發生太多事情。
實現用戶界面
在Electron中要獲得圖3.1中效果圖的可行版本,實現必要的HTML和CSS是相當容易的。因為我們只需要支持一個瀏覽器,而這個瀏覽器支持web平台提供的最新和最強大的特性,如圖3.4所示。
圖3.4 主進程將創建一個渲染器程序進程並告訴它加載index.html。然后,它將像在瀏覽器中一樣加載CSS和JavaScript。
在index.html,我們添加清單3.3中的標記來創建圖3.5中的瀏覽器窗口。
圖3.5 開始我們第一個未樣式化的Electron應用
列表3.3 我們應用的標記:./app/index.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width,initial-scale=1"> 6 <title>Fire Sale</title> 7 <link rel="stylesheet" href="style.css" type="text/css"> 8 </head> 9 <body> 10 <!--控件部分在頂部添加了用於打開和保存文件的按鈕。稍后我們將向這些按鈕添加功能。--> 11 <section class="controls"> 12 <button id="new-file">New File</button> 13 <button id="open-file">Open File</button> 14 <button id="save-markdown" disabled>Save File</button> 15 <button id="revert" disabled>Revert</button> 16 <button id="save-html">Save HTML</button> 17 <button id="show-file" disabled>Show File</button> 18 <button id="open-in-default" disabled>Open in Default Application</button> 19 </section> 20 <!--我們的應用程序允許使用.raw-markdown類編寫和編輯文本區域中的內容,並使用.rendered-html類在div元素中呈現該內容。--> 21 <section class="content"> 22 <!--<label>標簽是可選的,並且包含了這些標簽,以使視障用戶更容易訪問應用程序。 --> 23 <label for="markdown" hidden>Markdown Content</label> 24 <textarea class="raw-markdown" id="markdown"></textarea> 25 <div class="rendered-html" id="html"></div> 26 </section> 27 </body> 28 <!--在文件末尾的標記中,我們需要渲染進程的代碼,它位於同一個目錄中的renderer.js中。 --> 29 <script> 30 require('./renderer'); 31 </script> 32 </html>
我們的應用程序目前還沒有太多需要查看的地方。
如果您和我一樣,您對我在效果圖中引入的兩列接口有點懷疑。在討論如何使用HTML和CSS實現列時,很少使用easy這個詞。
幸運的是,我們可以自信地使用添加到CSS3的名為Flexbox的新布局模式來快速定義應用程序的兩列布局。Flexbox使創建頁面布局變得很容易,可以在各種屏幕大小范圍內進行可預測的操作,如清單3.4所示。它對CSS來說是相對較新的,直到最近才得到Internet Explorer的支持。
正如我們在第1章和第2章中討論的,我們的應用程序總是跟上Chrome的最新版本,所以我們可以放心地使用Flexbox布局模式,而不用擔心跨瀏覽器兼容性。
使用Flexbox創建頁面布局:./app/style.css
/*選擇一個更新的CSS框模型,它將正確地設置元素的寬度和高度*/ html { box-sizing: border-box; } /* 將此設置傳遞給頁面上的所有其他元素和偽元素*/ *, *:before, *:after { box-sizing: inherit; } html, body { height: 100%; width: 100%; overflow: hidden; } body { margin: 0; padding: 0; position: absolute; } /* 在整個應用程序中使用操作系統的默認字體 */ body, input { font: menu; } /*移除瀏覽器圍繞活動輸入字段的默認突出顯示*/ textarea, input, div, button { outline: none; margin: 0; } .controls { background-color: rgb(217, 241, 238); padding: 10px 10px 10px 10px; } button { font-size: 14px; background-color: rgb(181, 220, 216); border: none; padding: 0.5em 1em; } button:hover { background-color: rgb(156, 198, 192); } button:active { background-color: rgb(144, 182, 177); } button:disabled { background-color: rgb(196, 204, 202); } .container { display: flex; flex-direction: column; min-height: 100vh; min-width: 100vw; position: relative; } /* 使用Flexbox對齊應用程序的兩個窗格*/ .content { height: 100vh; display: flex; } /* 使用Flexbox將兩個窗格設置為相同的寬度 */ .raw-markdown, .rendered-html { min-height: 100%; max-width: 50%; flex-grow: 1; padding: 1em; overflow: scroll; font-size: 16px; } .raw-markdown { border: 5px solid rgb(238, 252, 250);; background-color: rgb(238, 252, 250); font-family: monospace; }
樣式表有兩個主要目標。首先,我們想利用像Flexbox這樣的現代CSS特性來設計我們的UI。其次,我們希望采取一些小步驟,使應用程序的外觀和感覺更像一個真實的web應用程序(參見圖3.6)。
圖3.6 我們的應用程序已經使用CSS的現代特性給出了一些基本的樣式。
box-sizing
屬性在CSS中處理一個歷史上的奇怪現象,在一個寬度為200像素的元素中添加50個像素的填充將導致它的寬度為300像素(每邊添加50個像素的填充),對於邊框也是一樣。
當box-sizing
被設置為border-box時,我們的元素會考慮到我們設置它們的高度和寬度。總的來說,這是一件好事。在這個CSS規則中,我們還讓所有其他元素和偽元素都尊重我們通過將box-sizing設置為border-box所做的艱苦工作。
我們希望我們的應用程序能夠適應本地應用程序。朝着這個方向邁出的重要一步是使用所有其他應用程序都使用的系統字體。例如,盡管macOS在整個操作系統中使用San Francisco作為默認字體,但它不能作為常規字體使用。我們將font屬性設置為menu,它依賴於操作系統來使用它的默認字體——即使我們無法訪問它。
瀏覽器在當前活動的UI元素周圍設置一個邊框。在macOS中,這個邊框是藍色的輝光。您可能從未過多地考慮過它,因為我們已經習慣了在web上使用它,但是當我們開發桌面應用程序時,它看起來並不合適。在我們的應用程序中,它看起來尤其糟糕,其中一半的UI實際上是一個大型文本輸入。通過將outline
設置為none,我們刪除了活動元素周圍的非自然輝光。
在.content
、.raw-markdown
和.rendered-html
規則中,我們實現了一個簡單的Flexbox布局,這將使我們的應用程序看起來更像我們在圖3.1中介紹的效果。content類的元素將包含我們的兩列。我們將display屬性設置為flex,以使用前面討論的Flexbox技術。下一步,我們設置flex- growth,它指定flex項的增長因子, 當然可以。把它看作元素的尺度相對於它的兄弟元素可能是有幫助的。在本例中,我們使用Flexbox將兩列設置為相等的比例。
優雅地顯示瀏覽器窗口
如果你仔細觀察你的應用程序的啟動,您將注意到,在Electron加載index.html並在窗口中呈現DOM之前,窗口完全為空。用戶不習慣在本地應用程序中看到這種情況,我們可以通過重新思考如何啟動窗口來避免這種情況。
如果您認為應用程序第一次啟動時的虛無閃光是無意義的,考慮主進程中的代碼:它創建一個窗口,然后在其中加載內容。如果我們隱藏窗口直到內容被加載呢?然后,當UI准備好時,我們顯示窗口,並避免短暫地暴露一個空窗口。
列表3.5 當DOM就緒時優雅地顯示窗口
1 app.on('ready', () => { 2 //使用默認屬性創建一個新的BrowserWindow 3 mainWindow = new BrowserWindow({ 4 show: false, 5 webPreferences: { 6 // webPreferences中的nodeIntegrationInWorker選項設置為true,Electron5.x以后,缺省為false 7 nodeIntegration: true 8 } 9 }) 10 11 //在剛才創建的BrowserWindow實例中加載app/index.html 12 mainWindow.loadFile('app/index.html'); 13 14 mainWindow.once('ready-to-show', () => { 15 //當DOM就緒時顯示窗口。 16 mainWindow.show(); 17 }); 18 19 mainWindow.on('closed', () => { 20 //在窗口關閉時將進程設置為null 21 mainWindow = null; 22 }); 23 });
我們將一個對象傳遞給BrowserWindow構造函數,默認情況下將其設置為hidden
。當BrowserWindow實例觸發它的“ready-to-show”事件時,我們將調用它的show()方法,這將在UI完全准備好運行后使它不再隱藏。當應用程序通過網絡加載遠程資源時,這種方法甚至更有用,因為初始化頁面可能需要更長的時間。
實現基本功能
讓我們把一些基本功能放在適當的位置上。對於初學者,我們希望在左窗格中的Markdown發生更改時更新右窗格中呈現的HTML視圖(參見圖3.7)。這就是我們唯一的依賴—Marked—發揮作用的地方。
圖3.7 我們將在左側窗格中添加一個事件監聽器,它將以HTML的形式呈現標記並顯示在右側窗格中。
引入依賴項很容易,因為我們可以使用Node的require
來引入marked。讓我們在app/renderer.js中添加以下內容。
列表3.6 引入依賴: ./app/renderer.js
const marked = require('marked');
現在,我們可以通過變量marked使用Marked。鑒於我們在圖3.7中討論了應用程序的功能,您可能已經開始懷疑,在開發應用程序時,我們將大量使用#markdown文本區域和#html元素。讓我們使用一對變量來存儲對每個元素的引用,以便更容易地使用它們,如清單3.7所示。在此過程中,我們還將為UI頂部的每個按鈕創建變量。
列表3.7 緩存DOM選擇器: ./app/renderer.js
const markdownView = document.querySelector('#markdown'); const htmlView = document.querySelector('#html'); const newFileButton = document.querySelector('#new-file'); const openFileButton = document.querySelector('#open-file'); const saveMarkdownButton = document.querySelector('#save-markdown'); const revertButton = document.querySelector('#revert'); const saveHtmlButton = document.querySelector('#save-html'); const showFileButton = document.querySelector('#show-file'); const openInDefaultButton = document.querySelector('#open-in-default');
我們還相當頻繁地在htmlView中呈現Markdown,所以我們想給自己一個函數,以便將來更容易實現。
列表3.8 轉換markdown到HTML: ./app/renderer.js
marked將我們要呈現的Markdown內容作為第一個參數,並將選項的對象作為第二個參數。我們希望避免意外的腳本注入,因此我們傳入了一個對象,並將sanitize屬性設置為true。
最后,我們向markdownView添加了一個事件監聽器,它將在keyup上讀取它的內容(在textarea元素中,內容存儲在它的value屬性中),通過marked運行它們,然后將它們加載到htmlView中。結果如圖3.8所示。
列表3.9 當Markdown更改時重新呈現HTML: ./app/renderer.js
1 markdownView.addEventListener('keyup', (event) => { 2 const currentContent = event.target.value; 3 renderMarkdownToHtml(currentContent); 4 });
圖3.8 我們的應用程序接受用戶在左窗格中鍵入的內容,並在右窗格中將其自動呈現為HTML。該內容由用戶提供,不屬於我們的應用程序。
基本功能已經就緒,我們准備開始研究只有在Electron應用程序中才可能實現的特性,首先從文件系統中讀寫文件開始。當所有這些都完成后,應用程序的呈現程序流程應該是這樣的。
列表3.10 渲染進程: ./app/renderer.js
1 const marked = require('marked'); 2 3 const markdownView = document.querySelector('#markdown'); 4 const htmlView = document.querySelector('#html'); 5 const newFileButton = document.querySelector('#new-file'); 6 const openFileButton = document.querySelector('#open-file'); 7 const saveMarkdownButton = document.querySelector('#save-markdown'); 8 const revertButton = document.querySelector('#revert'); 9 const saveHtmlButton = document.querySelector('#save-html'); 10 const showFileButton = document.querySelector('#show-file'); 11 const openInDefaultButton = document.querySelector('#open-in-default'); 12 13 const renderMarkdownToHtml = (markdown) => { 14 htmlView.innerHTML = marked(markdown, { sanitize: true }); 15 }; 16 17 markdownView.addEventListener('keyup', (event) => { 18 const currentContent = event.target.value; 19 renderMarkdownToHtml(currentContent); 20 });
調試Electron應用程序
在理想的世界中,我們在編寫代碼時永遠不會出錯。
接口和方法永遠不會在不同的版本之間更改,而且您的作者不必每次發布本書中應用程序使用的依賴項的新版本時都屏住呼吸。
我們並不生活在那個世界上。因此,我們可以使用開發工具幫助我們跟蹤並有望消除缺陷。
調試渲染器進程
到目前為止,一切都進行得相當順利,但可能不久之后我們就必須調試一些棘手的情況。因為Electron應用程序是基於Chrome的,所以我們在構建Electron應用程序時可以使用Chrome開發者工具就不足為奇了(圖3.9)。
調試渲染器過程相對簡單。Electron的默認應用程序菜單提供了一個命令來打開應用程序中的Chrome開發工具。在第6章中,我們將學習如何創建我們自己的自定義菜單,並在您不希望將其公開給用戶的情況下消除此功能。
還有另外兩種訪問開發人員工具的方法。
在任何時候,您都可以按macOS上的Command-Option-I
或Windows或Linux上的Control-Shift-I
打開工具(圖3.10)。此外,您還可以通過編程方式觸發開發人員工具。
BrowserWindow實例上的webcontent屬性有一個名為openDevTools()
的方法。如清單3.11所示,這個方法將在調用它的BrowserWindow中打開開發工具。
圖3.9 Chrome開發工具在渲染器過程中可用,就像在基於瀏覽器的應用程序中一樣。
圖3.10 該工具可以在Electron提供的默認菜單中開或關。您還可以使用Windows上的Control-Shift-I或macOS上的Command-Option-I來觸發它們。
列表3.11 從主流程打開開發者工具: ./app/main.js
1 app.on('ready', () => { 2 mainWindow = new BrowserWindow({ 3 show: false, 4 webPreferences: { 5 nodeIntegration: true 6 } 7 }); 8 9 mainWindow.loadFile(`app/index.html`); 10 11 12 mainWindow.once('ready-to-show', () => { 13 mainWindow.show(); 14 mainWindow.webContents.openDevTools(); //我們可以通過編程方式在主窗口加載開發工具時立即打開它。 15 }); 16 17 mainWindow.on('closed', () => { 18 mainWindow = null; 19 }); 20 }); 21
調試主進程
調試主進程並不容易。Node Inspector是調試Node.js應用程序的常用工具,為了提供一個可以調試主進程的方法,Electron 提供了 --inspect
開關。使用如下的命令行開關來調試 Electron 的主進程:--insepct=[port]
當這個開關用於 Electron 時,它將會監聽 V8 引擎中有關 port
的調試器協議信息。 默認的port
是 5858
。
electron --inspect=5858 your/appCopy
使用VSCode進行主進程調試
Visual Studio Code是一個免費的開放源碼的IDE,適用於Windows、Linux和macOS,並且是由Microsoft在Electron之上構建的。Visual Studio Code提供了一組用於調試節點應用程序的豐富工具,這使得調試Electron應用程序比前面提到的要容易得多。
設置構建任務的一種快速方法是讓Visual Studio Code在沒有構建任務的情況下構建應用程序。 在Windows上按Control-Shift-B
或在macOS上按Command-Shift-B
,將提示您創建一個構建任務,如圖3.11所示。
圖3.11 在沒有適當的構建任務的情況下觸發構建任務,Visual Studio Code將提示為您創建一個。
列表3.12 在Windows的Visual Studio Code中設置構建任務: task.json
1 { 2 // 有關 tasks.json 格式的文檔,請參見 3 // https://go.microsoft.com/fwlink/?LinkId=733558 4 "version": "2.0.0", 5 "tasks": [ 6 { 7 "type": "npm", 8 "script": "start", 9 "problemMatcher": [] 10 } 11 ] 12 }
現在,當您按下Windows上的Control-Shift-B
或macOS上的Command-Shift-B
時,您的電子應用程序將啟動。這不僅對於在Visual Studio Code中設置調試非常重要,而且通常也是啟動應用程序的一種方便方法。下一步是設置Visual Studio Code來啟動應用程序,並將其連接到其內置調試器(圖3.12)。
要創建啟動任務,請轉到上面的終端選項卡,並單擊配置默認生成任務。Visual Studio Code將詢問您想要創建哪種配置文件。選擇Node並用清單3.13替換文件的內容。
圖3.12 在Debug選項卡中,單擊gear, Visual Studio Code將創建一個配置文件,用於代表您啟動調試器。
列表3.13 為Windows的Visual Studio代碼設置啟動任務
{ "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceRoot}", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "args" : ["."], "outputCapture": "std" } ] }
有了這個配置文件,您可以單擊主進程中任何一行的左邊緣來設置斷點,然后按F5運行應用程序。 執行將在斷點處暫停,允許您檢查調用堆棧,確定范圍內的變量,並與活動控制台進行交互。斷點並不是調試代碼的唯一方法。 您還可以監視特定的表達式,或者在拋出未捕獲異常時將其放入調試器(圖3.13)。
圖3.13 內置在Visual Studio Code中的調試器允許您暫停應用程序的執行,並順便檢查bug。
您很可能沒有使用Visual Studio Code。這很好。這並不是本書的先決條件,使用您最熟悉的文本編輯器或IDE幾乎肯定沒問題。 此外,Visual Studio Code並不是唯一支持調試主進程。例如,您可以在這里找到配置WebStorm的詳細信息:http://mng.bz/Y5T6。
總結
-
在接下來的幾章中,我們將制做一個markdown到html編輯器。
-
Flexbox受到現代瀏覽器的支持,允許我們輕松地實現一個雙窗格界面,當用戶改變窗口的大小時,這個界面將進行調整。
-
Chrome開發工具在所有渲染器進程中都可用,可以從默認的電子應用程序、鍵盤快捷鍵或主進程觸發。
-
此時Electron中還沒有完全支持
Node Inspector
檢查器。 -