10分鐘實現Typora(markdown)編輯器
本章主要內容:
介紹我們將在接下來的幾章中構建的應用程式
配置我們的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
檢查器。 -
Visual Studio程式碼提供了一組豐富的工具,用於除錯應用程式主程序中的問題。