本章主要內容javascript
- 構建並啓動Electron應用
- 生成
package.json
,配置成Electron應用- 在你的項目中包含預先構建Electron版本
- 配置
package.json
以啓動主進程- 從主進程生成渲染進程
- 利用Electron限制寬鬆的優勢構建一般在瀏覽器沒法構建的功能
- 使用Electron的內置模塊來回避一些常見的問題
在第一章中,咱們從高的層次上,討論了什麼是Electron。說到底這本書叫作《Electron實戰》,對吧?在本章中,咱們經過從頭開始設置和構建一個簡單的應用程序來管理書籤列表,從而學習Electron的基本知識。該應用程序將利用只有在現代的瀏覽器中才能使用的特性。css
在上一章的高層次討論中,我提到了Electron是一個相似於Node的運行時。這仍然是正確的,可是我想回顧下這一點。Electron不是一個框架——它不提供任何框架,也沒有關於如何構造應用程序或命名文件的嚴格規則,這些選擇都留給了咱們這些開發者。好的一面是,它也不強制執行任何約定,並且在入手以前,咱們不須要多少概念上的樣板信息去學習。html
讓咱們從構建一個簡單而又有些幼稚的Electron應用程序開始,來增強咱們已經介紹過的全部內容的理解。咱們的應用程序接受url。當用戶提供URL時,咱們獲取URL引用的頁面的標題,並將其保存在應用程序的localStorage中。最後,顯示應用程序中的全部連接。您能夠在GitHub上找到本章的完整源代碼(github.com/electron-in…)。java
在此過程當中,咱們將指出構建Electron應用程序的一些優勢,例如,能夠繞過對服務器的需求,使用最前沿的web api,這些web api並不普遍支持全部瀏覽器,由於這些APIs是在現代版本的Chromium中實現。圖2.1是咱們在本章構建的應用程序的效果圖。node
圖2.1 咱們在本章中構建的應用程序效果圖webpack
當用戶但願將網站URL保存並添加到輸入字段下面的列表中時,應用程序向網站發送一個請求來獲取標記。成功接收到標記後,應用程序獲取網站的標題,並將標題和URL添加到網站列表中,該列表存儲在瀏覽器的localStorage
中。當應用程序啓動時,它從localStorage讀取並恢復列表。咱們添加了一個帶有命令的按鈕來清除localStorage,以防出現錯誤。由於這個簡單的應用程序旨在幫助您熟悉Electron,因此咱們不會執行高級操做,好比從列表中刪除單個網站。git
npm init 生成package.json
github
搭建Electron目錄框架web
應用程序結構的定義取決於您的團隊或我的處理應用程序的方式。許多開發人員採用的方法略有不一樣。觀察學習一些更成熟的電子應用程序,咱們能夠辨別出共同的模式,並在本書中決定如何處理咱們的應用程序。shell
出於咱們的目的,爲了讓本書文件結構達成一致。作出一下規定,咱們有一個應用程序目錄,其中存儲了全部的應用程序代碼。咱們還有一個package.json
將存儲依賴項列表、關於應用程序的元數據和腳本,並聲明Electron應該在何處查找主進程。在安裝了依賴項以後,最終會獲得一個由Electron爲咱們建立的node_modules目錄,可是咱們不會在初始設置中包含它
就文件而言,讓咱們從應用程序中的兩個文件開始:main.js和renderer.js。它們是帶有標識的文件名,所以咱們能夠跟蹤這兩種類型的進程。咱們在本書中構建的全部應用程序的開始大體遵循圖2.2中所示的目錄結構。(若是你在運行macOS,你能夠經過安裝brew install tree
使用tree命令。)
圖2.2 咱們第一個Electron應用的文件結構樹
建立一個名爲「bookmarker」的目錄,並進入此目錄。您能夠經過從命令行工具運行如下兩個命令來快速建立這個結構。當你使用npm init
以後,你會生成一個package.json
文件。
mkdir app
touch app/main.js app/renderer.js app/style.css app/index.html
複製代碼
Electron自己不須要這種結構,但它受到了其餘Electron應用程序創建的一些最佳實踐的啓發。Atom
將全部應用程序代碼保存在一個app目錄中,將全部樣式表和其餘資產(如圖像)保存在一個靜態目錄中。LevelUI
在頂層有一個index.js和一個client.js,並將全部依賴文件保存在src目錄中,樣式表保存在styles目錄中。Yoda
將全部文件(包括加載應用程序其他部分的文件)保存在src目錄中。app、src和lib是存放應用程序大部分代碼的文件夾的經常使用名稱,style、static和assets是存放應用程序中使用的靜態資產的目錄的經常使用名稱。
package.json
清單用於許多甚至說大多數Node項目。此清單包含有關項目的重要信息。它列出了元數據,好比做者的姓名以及他們的電子郵件地址、項目是在哪一個許可下發布的、項目的git存儲庫的位置以及文件問題的位置。它還爲一些常見的任務定義了腳本,好比運行測試套件或者與咱們的需求相關的構建應用程序。package.json
文件還列出了用於運行和開發應用程序的全部依賴項。
理論上,您可能有一個沒有package.json
的Node項目。可是,當加載或構建應用程序時,Electron依賴於該文件及其主要屬性來肯定從何處開始。
npm是Node附帶的包管理器,它提供了一個有用的工具幫助生成package.json
。在前面建立的「bookmarker」目錄中運行npm init。若是您將提示符留空,npm將冒號後面括號中的內容做爲默認內容。您的內容應該相似於圖2.3,固然,除了做者的名字以外。
在package.json中,值得注意的是main
條目。這裏,你能夠看到我將它設置爲"./app/main.js"。基於咱們如何設置應用程序。你能夠指向任何你想要的文件。咱們要用的主文件剛好叫作main.js。可是它能夠被命名爲任何東西(例如,sandwich.js、index.js、app.js)。
圖2.3 npm init 提供一系列提示並設置一個package.json文件
咱們已經創建了應用程序的基本結構,可是卻找不到Electron。從源代碼編譯Electron須要一段時間,並且可能很乏味。所以咱們根據每一個平臺(macOS、Windows和Linux)以及兩種體系結構(32位和64位)預先構建了electronic版本。咱們經過npm安裝Electron。
下載和安裝電子很容易。在您運行npm init以前,在你的項目目錄中運行如下命令:
npm install electron --save-dev
複製代碼
此命令將在你的項目node_modules目錄下下載並安裝Electron(若是您尚未目錄,它還會建立目錄)。--save-dev
標誌將其添加到package.json的依賴項列表中。這意味着若是有人下載了這個項目並運行npm install,他們將默認得到Electron。
漫談electron-prebuilt
假如您瞭解Electron的歷史,您可能會看到博客文章、文檔,甚至本書的早期版本,其中提到的是
electron-prebuilt
,而不是electron
。在過去,前者是爲操做系統安裝預編譯版Electron的首選方法。後者是新的首選方法。從2017年初開始,再也不支持electron-prebuilt
。
npm還容許您定義在package.json
中運行公共腳本的快捷方式。當您運行package.json定義的腳本時。npm自動添加node_modules到這個路徑。這意味着它將默認使用本地安裝的Electron版本。讓咱們向package.json添加一個start腳本。
列表2.1 向package.json添加一個啓動腳本
{ +
"name": "bookmarker", |當咱們運行npm start
"version": "1.0.0", |npm將會運行什麼腳本
"description": "Our very first Electron application", |
"main": "./app/main.js", |
"scripts": { |
"start": "electron .", <------+
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Steve Kinney",
"license": "ISC",
"dependencies": {
"electron": "^2.0.4"
}
}
複製代碼
如今,當咱們運行npm start時,npm使用咱們本地安裝的版本Electron去啓動Electron應用程序。你會注意到彷佛沒有什麼事情發生。在你的終端中,它實際運行如下程式碼:
>bookmarker@1.0.0 start /Users/stevekinney/Projects/bookmarker
>electron .
複製代碼
您還將在dock或任務欄中看到一個新應用程序(咱們剛剛設置的Electron應用程序),如圖2.4所示。它被簡稱爲「Electron」,並使用Electron的默認應用程序圖標。在後面的章節中,咱們將看到如何定製這些屬性,可是目前默認值已經足夠好了。咱們全部的代碼文件都是徹底空白的。所以,這個應用程序還有不少操做須要去作,可是它確實存在並正確啓動。咱們認爲這是一場暫時的勝利。在windows上關閉應用程序的全部窗口或選擇退出應用程序菜單終止進程。或者,您能夠在Windows命令提示符或終端中按Control-C
退出應用程序。按下Command-Period
將終止macOS上的進程。
圖2.4 dock上的應用程序就是咱們剛創建的電子應用
如今咱們有了一個Electron應用,若是咱們真的能讓它作點什麼,那就太好了。若是你還記得第一章,咱們從能夠建立一個或多個渲染器進程的主進程開始。咱們首先經過編寫main.js代碼,邁出咱們應用程序的第一步。
要處理Electron,咱們須要導入electron
庫。Electron附帶了許多有用的模塊,咱們在本書中使用了這些模塊。第一個—也能夠說是最重要的——是app
模塊。
列表2.2 添加一個基本的主進程: ./app/main.js
const {app} = require('electron'); +
app.on('ready', () => { <---+ 在應用程序徹底
console.log('Hello from Electron'); + 啓後當即調用
});
複製代碼
app
是一個處理應用程序生命週期和配置的模塊。咱們可使用它退出、隱藏和顯示應用程序,以及獲取和設置應用程序的屬性。app
模塊還能夠運行事件-包括before-quit
, window -all-closed
,
browser-window-blur
, 和browser-window-focus
-當應用程序進入不一樣狀態時。
在應用程序徹底啓動並準備就緒以前,咱們沒法處理它。幸運的是,app觸發了一個ready
事件。這意味着在作任何事以前,咱們須要耐心等待並監聽應用程序啓動ready
事件。在前面的代碼中,咱們在控制檯打印日誌,這是一件無需Electron就能夠輕鬆完成的事情,可是這段代碼強調了如何偵聽ready
事件。
咱們的主進程與其餘Node進程很是類似。它能夠訪問Node的全部內置庫以及由Electron提供的一組特殊模塊,咱們將在本書中對此進行探討。可是,與任何其餘Node進程同樣,咱們的主進程沒有DOM(文檔對象模型),也不能呈現UI。主進程負責與操做系統交互,管理狀態,並與應用程序中的全部其餘流程進行協調。它不負責呈現HTML和CSS。這就是渲染器進程的工做。參與整個Electron主要功能之一是爲Node進程建立一個GUI。
主進程可使用BrowserWindow
建立多個渲染器進程。每一個BrowserWindow
都是一個單獨的、唯一的渲染器器進程,包括一個DOM,訪問Chromium web APIs,以及Node內置模塊。訪問BrowserWindow模塊的方式與訪問app模塊的方式相同。
列表2.3 引用BrowserWindow模塊: ./app/main.js
const {app, BrowserWindow} = require('electron');
複製代碼
您可能已經注意到BrowserWindow模塊以大寫字母開頭。根據標準JavaScript約定,這一般意味着咱們用new
關鍵字將其調用爲構造函數。咱們可使用這個構造函數建立儘量多的渲染器進程,只要咱們喜歡,或者咱們的計算機能夠處理。當應用程序就緒時,咱們建立一個BrowserWindow實例。讓咱們按照如下方式更新代碼。
列表2.4 生成一個BrowserWindow: ./app/main.js
+
const {app, BrowserWindow} = require('electron'); |在咱們的應用程序中建立一個
let mainWindow = null; <----+window對象的全局引用 app.on('ready', () => { + + console.log('Hello from Electron.'); |當應用程序準備好時, mainWindow = new BrowserWindow(); <----+建立一個瀏覽器窗口 }); +並將其分配給全局變量 複製代碼
咱們在ready
事件監聽器外聲明瞭mainWindow。JavaScript使用函數做用域。若是咱們在事件監聽器中聲明mainWindow
, mainWindow
將進行垃圾回收,由於分配給ready事件的函數已經運行完畢。若是被垃圾回收,咱們的窗戶就會神祕地消失。若是咱們運行這段代碼,咱們會在屏幕中央看到一個不起眼的小窗口,如圖2.5所示。
一個沒有加載HTML文檔的空BrowserWindow
這是一扇窗口,並什麼好看的。下一步是將HTML頁面加載到咱們建立的BrowserWindow
實例中。全部BrowserWindow
實例都有一個web content屬性,該屬性具備幾個有用的特性,好比將HTML文件加載到渲染器進程的窗口中、從主進程向渲染器進程發送消息、將頁面打印爲PDF或打印機等等。如今,咱們最關心的是將內容加載到咱們剛剛建立的那個無聊的窗口中。
咱們須要加載一個HTML頁面,所以在您項目的app目錄中建立index.html。讓咱們將如下內容添加到HTML頁面,使其成爲一個有效的文檔。
列表2.5 建立index.html: ./app/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline';
connect-src *
"
>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bookmarker</title>
</head>
<body>
<h1>Hello from Electron</h1>
</body>
</html>
複製代碼
這很簡單,但它完成了工做,併爲構建打下了良好的基礎。咱們將如下代碼添加到app/main.js
中,以告訴渲染器進程在咱們以前建立的窗口中加載這個HTML文檔。
列表2.6 將HTML文檔加載到主窗口: ./app/main.js
咱們使用file://protocol
和_dirname
變量,該變量在Node中全局可用。_dirname
是Node進程正在執行的目錄的完整路徑。在個人例子中,_dirname
擴展爲**/Users/stevekinney/Projects/bookmarker/app**。
如今,咱們可使用npm start啓動應用程序,並觀察它加載新的HTML文件。若是一切順利,您應該會看到相似於圖2.6的內容。
從渲染器進程加載的HTML文件中,咱們能夠像在傳統的基於瀏覽器的web應用程序中同樣加載可能須要的任何其餘文件-即<script>
和<link>
標籤。
Electron與咱們習慣的瀏覽器不一樣之處在於咱們能夠訪問全部Node——甚至是咱們一般認爲的「客戶端」。這意味着,咱們可使用require
甚至Node-only對象和變量,好比_dirname
或process
模塊。同時,咱們還有全部可用的瀏覽器APIs。只能在客戶端的工做和只能在服務端作的工做的分工開始消失不見。
圖2.6 一個帶有簡單HTML文檔的瀏覽器窗口
讓咱們來看看實際狀況。在傳統的瀏覽器環境中_dirname
不可用,在Node中document
或alert
是不可用的。但在Electron,咱們能夠無縫地將它們結合在一塊兒。讓咱們在頁面上添加一個按鈕。
列表2.7 添加一個按鈕到HTML文檔: ./app/index. html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF+8">
<meta http+equiv="Content+Security+Policy" content=" default+src 'self'; script+src 'self' 'unsafe+inline';connect+src *">
<meta name="viewport" content="width=device+width,initial+scale=1">
<title>Bookmarker</title>
</head>
<body>
<h1>Hello from Electron</h1>
<p>
<button class="alert">Current Directory</button> <---+
</p> |這是咱們
</body> |的新按鈕
</html> +
複製代碼
如今,咱們已經有了按鈕,讓咱們添加一個事件監聽器,它將提醒咱們運行應用程序的當前目錄。
<script>
const button = document.querySelector('.alert');
button.addEventListener('click', () =^ {
alert(__dirname); <------+單擊按鈕時, }); |使用瀏覽器警告顯示 </script> |Node全局變量
+
複製代碼
alert()
僅在瀏覽器中可用。_dirname
僅在Node中可用。當咱們點擊按鈕時,咱們被處理成Node和Chromium在一塊兒工做,甜美和諧,如圖2.7所示。
圖2.7 在渲染器進程的上下文中,BrowserWindow執行JavaScript。
在HTML文件中編寫代碼顯然有效,可是不難想象,咱們的代碼量可能會增加到這種方法再也不可行的地步。咱們能夠添加帶有src
屬性的腳本標記來引用其餘文件,可是這很快就會變得很麻煩。
這就是web開發變得棘手的地方。雖然模塊被添加到ECMAScript規範中,目前沒有瀏覽器具備模塊系統的工做實現。在客戶端上,咱們能夠考慮使用一些構建工具,如Browserify
(browserify.org)或模塊bundler
、webpack
,也可使用任務運行器,如Gulp
或Grunt
。
咱們可使用Node的模塊系統,而不須要額外的配置。讓咱們移除<script>
標籤中的全部代碼到-如今是空的-app/renderer.js文件中。如今咱們能夠用一個<script>
標記去引用renderer.js文件去替代以前的內容。
列表2.9 從renderer.js加載JavaScript: ./app/index.html
+
<script> |使用Node的require函數 require('./renderer'); <--+將額外的JavaScript模塊 </script> |加載到渲染器進程中
+
複製代碼
若是咱們啓動應用程序,您將看到它的功能沒有改變。一切都照常進行。這在軟件開發中不多發生。在繼續以前,讓咱們先體驗一下這種感受。
當咱們在Electron應用程序中引用樣式表時,不多會發生意外。稍後,咱們將討論如何使用Sass而不是Electron。 在電子應用程序中添加樣式表與在傳統web應用程序中添加樣式表沒有多大不一樣。儘管如此,一些細微差異仍是值得討論的。
讓咱們從將style.css文件添加到應用程序目錄開始。咱們將如下內容添加到style.css中。
列表2.10 添加基礎樣式: ./app/style.css
html {
box+sizing: border+box;
}
*, *:before, *:after {
box+sizing: inherit; +使用頁面所運行
} |的操做系統的
body, input { |默認系統字體
font: menu; <------+ } 複製代碼
最後一項聲明可能看起來有點陌生。它是Chromium獨有的,容許咱們在CSS中使用系統字體。這種能力對於使咱們的應用程序與其原生本機程序相適應很是重要。在macOS上,這是使用San Francisco的惟一方法,該系統字體附帶El Capitan 10.11及之後版本。
在Electron應用程序中使用CSS,這是咱們應該考慮的另外一個重要的區別。咱們的應用程序將只在應用程序附帶的Chromium版本中運行。咱們沒必要擔憂跨瀏覽器支持或兼容性考慮。正如在第1章中提到的,電子與相對較新版本的Chromium一塊兒發佈。這意味着咱們能夠自由地使用flexbox和CSS變量等技術。
咱們像在傳統瀏覽器環境中同樣引用新樣式表,而後將如下內容添加到index.html的<head>
部分。 我將包含連接到樣式表的HTML標記—由於,在我做爲web開發人員的20年裏,我仍然不記得如何第一次嘗試就作到這一點。
列表2.11 在HTML文檔中引用樣式表: ./app/index.html
<link rel="stylesheet" href="style.css" type="text/css">
複製代碼
咱們首先使用UI所需的標記更新index.html。
列表2.12 爲應用程序的UI添加標記: ./app/index.html
<h1>Bookmarker</h1>
<div class="error-message"></div>
<section class="add-new-link">
<form class="new-link-form">
<input type="url" class="new-link-url" placeholder="URL"size="100"
required>
<input type="submit" class="new-link-submit" value="Submit" disabled>
</form>
</section>
<section class="links"></section>
<section class="controls">
<button class="clear-storage">Clear Storage</button>
</section>
複製代碼
咱們有一個用於添加新連接的部分,一個用於顯示全部精彩連接的部分,以及一個用於清除全部連接並從新開始的按鈕。你的應用程序中的<script>
標籤應該和咱們在本章早些時候討論時同樣,可是以防萬一,我在下方給出代碼:
<script>
  require('./renderer');
</script>
複製代碼
標記就緒後,咱們如今能夠將注意力轉向功能。讓咱們清除app/renderer.js
中的全部內容,從新開始。在咱們一塊兒學習的過程當中,咱們將須要處理添加到標記中的一些元素,因此讓咱們首先查詢這些選擇器並將它們緩存到變量中。將如下內容添加到app/renderer.js
。
列表2.13 緩存DOM元素選擇器: ./app/renderer.js
const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');
複製代碼
回顧清單2.12,您會注意到在標記中咱們將input元素的type屬性設置「url」。若是內容不匹配有效的URL模式,Chromium將把該字段標記爲無效。不幸的是,咱們沒法訪問Chrome或Firefox中內置的錯誤消息彈出框。這些彈出窗口不是Chromium web
模塊的一部分,所以也不是Electron的一部分。如今,咱們在默認狀況下禁用start按鈕,而後在每次用戶在URL輸入框內中鍵入字母時檢查是否有一個有效的URL語法。
若是用戶提供了一個有效的URL,那麼咱們將打開submit
按鈕並容許他們提交URL。讓咱們將這段代碼添加到app/renderer.js中。
列表2.14 添加事件監聽器以啓用submit按鈕
newLinkUrl.addEventListener('keyup', () => {
newLinkSubmit.disabled = !newLinkUrl.validity.valid; <------+ }); 當用戶在輸入字段中敲入url時 | 經過使用Chromium ValidityState API | 來肯定輸入是否是有效,若是是這樣,從 +                 submit按鈕中移除disable屬性 複製代碼
如今也是添加一個協助函數來清除URL字段內容的好時機。在理想的狀況下,只要成功存儲了連接,就會調用這個函數。
列表2.15 添加幫助函數來清除輸入框: ./app/renderer.js
+
const clearForm= () => { |經過設置新鏈接輸入框爲空
newLinkUrl.value = null; <----+來清除該字段 }; | + 複製代碼
當用戶提交一個連接,咱們但願瀏覽器請求URL,而後把獲取回覆體,解析它,找到title元素,獲得標題的文本元素,存儲書籤的標題和URL在localStorage,和then-finally-update
書籤的頁面。
你可能感受到,也可能沒有感受到,你脖子後面的一些毛髮開始豎起來。你甚至可能對本身說:「這個計劃不可能行得通。您不能向第三方服務器發出請求。瀏覽器不容許這樣作。」
一般來講,你是對的。在傳統的基於瀏覽器的應用程序中,不容許客戶端代碼向其餘服務器發出請求。一般,客戶端代碼向服務器發出請求,而後將請求代理給第三方服務器。當它返回時,它將響應代理回客戶機。咱們在第一章中討論了這背後的一些緣由。
Electron具備Node服務器的全部功能,以及瀏覽器的全部功能。這意味着咱們能夠自由地發出跨源請求,而不須要服務器。
在Electron中編寫應用程序的另外一個好處是咱們可使用正在興起的Fetch API
來向遠程服務器發出請求。Fetch API免去了手工設置XMLHttpRequest的麻煩,併爲處理咱們的請求提供了一個良好的、基於承諾的接口。在撰寫本文時,主要瀏覽器對Fetch的支持有限。也就是說,它在當前版本的Chromium中有完整的支持,這意味着咱們可使用它。
咱們向表單添加一個事件偵聽器,以便在表單有動做時,當即執行提交。咱們沒有服務器,因此須要確保避免發出請求的默認操做。咱們經過防止默認操做來作到這一點。咱們還緩存URL輸入字段的值,以便未來使用。
列表2.16 向submit按鈕添加事件偵聽器: ./app/renderer.js
newLinkForm.addEventListener('submit', (event) => {
event.preventDefault(); <-----+告訴Chromium不要觸發HTTP請求, |這是表單提交的默認操做 const url = newLinkUrl.value; <--+ | | + // More code to come... |獲取新連接輸入框中的URL字段, }); +咱們很塊就會用到這個值。 複製代碼
Fetch API
做爲全局可用的fetch變量。抓取的URL返回一個promise
對象,該對象將在瀏覽器完成時被實現 獲取遠程資源。使用這個promise對象,咱們能夠根據是否獲取網頁、圖像或其餘類型的內容來處理不一樣的響應。在本例中,咱們正在獲取一個網頁,所以咱們將響應轉換爲文本。咱們從事件監聽器中的如下代碼開始。
列表2.17 使用Fetch API請求遠程資源./app/renderer.js
fetch(url) //使用Fetch API獲取提供的URL的內容
.then(response => response.text()); //將響應解析爲純文本
複製代碼
Promises是鏈式的,咱們可使用先前承諾的返回值,並將另外一個調用附加到then。此外,response.text()
自己返回一個promise。咱們的下一步將是獲取接收到的大塊標記,並解析它來遍歷它並找到title
元素。
Chromium提供了一個解析器,它將爲咱們作這件事,可是咱們須要實例化它。在app/renderer的頂部。咱們建立了一個DOMParser
實例,並將其存儲起來供之後使用。
列表2.18 實例化一個DOMParser: ./app/renderer.js
const parser = new DOMParser(); //建立一個DOMParser實例。咱們將在獲取所提供URL的文本內容後使用此方法。
複製代碼
讓咱們設置一對幫助函數來解析響應併爲咱們找到標題。
列表2.19 添加用於解析響應和查找標題的函數: ./app/renderer.js
const parseResponse = (text) => {
return parser.parseFromString(text, 'text/html'); //從URL獲取HTML字符串並將其解析爲DOM樹。
}
const findTitle = (nodes) =>{
return nodes.querySelector('title').innerText; //遍歷DOM樹以找到標題節點。
}
複製代碼
如今咱們能夠將這兩個步驟添加到咱們的處理鏈中。
列表2.20 解析響應並在獲取頁面時查找標題: ./app/renderer.js
fetch(url)
.then(response => response.text())
.then(parseResponse)
.then(findTitle);
複製代碼
此時,app/renderer.js
中的代碼看起來是這樣的。
const parser = new DOMParser();
const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');
newLinkUrl.addEventListener('keyup', () => {
newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});
newLinkForm.addEventListener('submit', (event) => {
event.preventDefault();
const url = newLinkUrl.value;
fetch(url)
.then(response => response.text())
.then(parseResponse)
.then(findTitle)
});
const clearForm = () => {
newLinkUrl.value = null;
}
const parseResponse = (text) => {
return parser.parseFromString(text, 'text/html');
}
const findTitle = (nodes) => {
return nodes.querySelector('title').innerText;
}
複製代碼
web storage APIs
存儲響應localStorage
是一個簡單的鍵/值存儲,內置在瀏覽器中並持久保存之間的會話。您能夠在任意鍵下存儲簡單的數據類型,如字符串和數字。讓咱們設置另外一個幫助函數,它將從標題和URL生成一個簡單的對象,使用內置的JSON庫將其轉換爲字符串,而後使用URL做爲鍵存儲它。
圖2.22 建立一個函數來在本地存儲中保存連接: ./app/renderer.js
const storeLink = (title, url) => {
localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
};
複製代碼
咱們的新storeLink
函數須要標題和URL來完成它的工做,可是前面的處理只返回標題。咱們使用一個箭頭函數將對storeLink
的調用封裝在一個匿名函數中,該匿名函數能夠訪問做用域中的url變量。若是成功,咱們也清除表單。
圖2.23 存儲連接並在獲取遠程資源時清除表單: ./app/renderer.js
fetch(url)
.then(response => response.text())
.then(parseResponse) |
.then(findTitle) |將標題和URL存儲到localStorage
.then(title => storeLink(title, url)) <---+
.then(clearForm);
複製代碼
存儲連接是不夠的。咱們還但願將它們顯示給用戶。這意味着咱們須要建立功能來遍歷存儲的全部連接,將它們轉換爲DOM節點,而後將它們添加到頁面中。
讓咱們從從localStorage
獲取全部連接的能力開始。若是你還記得,localStorage
是一個鍵/值存儲。咱們可使用對象。獲取對象的全部鍵。咱們必須爲本身提供另外一個幫助函數來將全部連接從localStorage
中取出。這沒什麼大不了的,由於咱們須要將它們從字符串轉換回實際對象。讓咱們定義一個getLinks函數。
圖2.24 建立用於從本地存儲中獲取連接的函數: ./app/renderer.js
const getLinks = () => { |
|獲取當前存儲在localStorage中的全部鍵的數組
return Object.keys(localStorage) <---+
.map(key => JSON.parse(localStorage.getItem(key))); <----+ } |對於每一個鍵,獲取其值 |並將其從JSON解析爲JavaScript對象 複製代碼
接下來,咱們將這些簡單的對象轉換成標記,以便稍後將它們添加到DOM中。咱們建立了一個簡單的convertToElement 幫助函數,它也能夠處理這個問題。須要指出的是,咱們的convertToElement函數有點幼稚,而且不嘗試清除用戶輸入。理論上,您的應用程序很容易受到腳本注入攻擊。這有點超出了本章的範圍,因此咱們只作了最低限度的渲染這些連接到頁面上。我將把它做爲練習留給讀者來確保這個特性的安全性。
列表2.25 建立一個從連接數據建立DOM節點的函數: ./app/renderer.js
const convertToElement = (link) => {
return ` <div class="link"> <h3>${link.title}</h3> <p> <a href="${link.url}">${link.url}</a> </p> </div> `;
};
複製代碼
最後,咱們建立一個renderLinks()函數,它調用getLinks,鏈接它們,使用convertToElement()轉換集合,而後替換頁面上的linksSection元素。
列表2.26 建立一個函數來呈現全部連接並將它們添加到DOM中: ./app/renderer.js
const renderLinks = () => {
const linkElements = getLinks().map(convertToElement).join(''); //將全部連接轉換爲HTML元素並組合它們
linksSection.innerHTML = linkElements; //用組合的連接元素替換links部分的內容
};
複製代碼
如今咱們能夠往處理鏈上添加最後一步。
列表2.27 獲取遠程資源後呈現連接: ./app/renderer.js
fetch(url)
.then(response => response.text())
.then(parseResponse)
.then(findTitle)
.then(title => storeLink(title, url))
.then(clearForm)
.then(renderLinks);
複製代碼
當頁面初始加載時,咱們還經過在頂層範圍內調用renderLinks()
來呈現全部連接。
列表2.28 加載和渲染連接: ./app/renderer.js
renderLinks(); //一旦頁面加載,就調用咱們以前建立的renderLinks()函數
複製代碼
使用promise與將功能分解爲命名的幫助函數相協調的一個優勢是,咱們的代碼經過獲取外部頁面、解析它、存儲結果和從新對連接列表進行排序的過程很是清楚。
最後一件事,咱們須要完成咱們的簡單應用程序的全部功能安裝的方法是鏈接「清除存儲」按鈕。咱們在localStorage上調用clear
方法,而後在linksSection中清空列表。
列表2.29 編寫清除存儲按鈕: ./app/renderer.js
clearStorageButton.addEventListener('click', () => {
localStorage.clear(); //清空localStorage中的全部連接
linksSection.innerHTML = ''; //從UI上移除全部連接
});
複製代碼
有了Clear Storage按鈕,彷佛咱們已經具有了大部分功能。咱們的應用程序如今看起來如圖2.8所示。此時,呈現器過程的代碼應該如清單2.30所示。
列表2.30 獲取、存儲和呈現連接的渲染器進程: ./app/renderer.js
const parser = new DOMParser();
const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');
const newLinkUrl.addEventListener('keyup', () => {
const newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});
newLinkForm.addEventListener('submit', (event) => {
event.preventDefault();
const url = newLinkUrl.value;
fetch(url)
.then(response => response.text())
.then(parseResponse)
.then(findTitle)
.then(title => storeLink(title, url))
.then(clearForm)
.then(renderLinks);
});
clearStorageButton.addEventListener('click', () => {
localStorage.clear();
linksSection.innerHTML = '';
});
const clearForm = () => {
newLinkUrl.value = null;
}
const parseResponse = (text) => {
return parser.parseFromString(text, 'text/html');
}
const findTitle = (nodes) => {
return nodes.querySelector('title').innerText;
}
const storeLink = (title, url) => {
localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
}
const getLinks = () => {
return Object.keys(localStorage)
.map(key => JSON.parse(localStorage.getItem(key)));
}
const convertToElement = (link) => {
return `<div class="link"><h3>${link.title}</h3> <p><a href="${link.url}">${link.url}</a></p></div>`;
}
const renderLinks = () => {
const linkElements = getLinks().map(convertToElement).join('');
linksSection.innerHTML = linkElements;
}
renderLinks();
複製代碼
到目前爲止,一切彷佛都運轉良好。咱們的應用程序從外部頁面獲取標題,在本地存儲連接,在頁面上呈現連接,並在須要時從頁面中清除它們。
可是若是出了什麼問題呢?若是咱們給它一個無效連接會發生什麼?若是請求超時會發生什麼?咱們將處理兩種最可能的狀況:當用戶提供一個URL,該URL經過了輸入字段的驗證檢查,但實際上並不有效;當URL有效,但服務器返回400或500級錯誤時。
咱們添加的第一件事是處理任何錯誤的能力。咱們須要提供一個捕獲異常的方法,當出現錯誤的時候,進行調用。咱們在這個事件中定義了另外一個幫助方法。
圖2.31 顯示錯誤消息: ./app/renderer.js
const handleError = (error, url) => { +若是獲取連接失敗,
  errorMessage.innerHTML = ` |則設置錯誤消息元素的內容   There was an issue adding "${url}": ${error.message} | +   `.trim(); <----+ |   setTimeout(() => errorMessage.innerText = null, 5000); <----+5秒後清除錯誤消息 } + 複製代碼
咱們能夠把它加到鏈上。咱們使用另外一個匿名函數傳遞帶有錯誤消息的URL。這主要是爲了提供更好的錯誤消息。若是不但願在錯誤消息中包含URL,則沒有必要這樣作。
圖2.32 在獲取、解析和呈現連接時捕獲錯誤: ./app/renderer.js
fetch(url)
 .then(response => response.text())
 .then(parseResponse) +
 .then(findTitle) |
 .then(title => storeLink(title, url)) |若是此處理鏈中的任何錯誤拒絕或拋出錯誤
 .then(clearForm) |則捕獲錯誤並將其顯示在UI中
 .then(renderLinks) |
 .catch(error => handleError(error, url)); <--+ 複製代碼
咱們還在前面添加了一個步驟,用於檢查請求是否成功。若是是,它將請求傳遞給處理鏈中的下一個操做。若是沒有成功,那麼咱們將拋出一個錯誤,這將繞過處理鏈中的其他操做,並直接跳到handleError()
步驟。這裏有一個我沒有處理的異常狀況:若是Fetch API不能創建網絡鏈接,那麼它返回的承諾將被徹底拒絕。我把它做爲練習留給讀者來處理,由於咱們在這本書中有不少內容要講,並且頁數有限。響應。若是狀態碼在400或500範圍內,response.ok
將爲false。
圖2.33 驗證來自遠程服務器的響應: ./app/renderer.js
+
|若是響應成功,則將其
const validateResponse = (response) => { |傳遞給下一個處理鏈
 if (response.ok) { return response; } <-----+
 throw new Error(`Status code of ${response.status} +   ${response.statusText}`); <-----+ }  |若是請求收到400或500系列響應  +則引起錯誤。 複製代碼
若是沒有錯誤,此代碼將傳遞響應對象。可是,若是出現錯誤,它會拋出一個錯誤,handleError()
會捕捉到這個錯誤並相應地進行處理。
圖2.34 在處理鏈中添加
validateResponse()
: ./app/renderer.js
fetch(url)
.then(validateResponse)
.then(response => response.text())
.then(parseResponse)
.then(findTitle)
.then(title => storeLink(title, url))
.then(clearForm)
.then(renderLinks)
.catch(error => handleError(error, url));
複製代碼
咱們尚未走出困境——若是一切順利的話,咱們還有一個問題。若是單擊應用程序中的一個連接會發生什麼?也許並不奇怪,它指向了那個連接。咱們的Electron應用程序的Chromium部分認爲它是一個web瀏覽器,因此它作了web瀏覽器最擅長的事情—它進入頁面。
只是咱們的應用程序並非真正的web瀏覽器。它缺乏後退按鈕或位置欄等重要功能。若是咱們點擊應用程序中的任何連接,咱們就會幾乎被困在那裏。咱們惟一的選擇是關閉應用程序,從新開始。
解決方案是在真正的瀏覽器中打開連接。但這引出了一個問題,哪一個瀏覽器?咱們如何知道用戶將什麼設置爲默認瀏覽器?咱們固然不想作任何僥倖的猜想,由於咱們不知道用戶安裝了什麼瀏覽器,並且沒有人喜歡看到錯誤的應用程序僅僅由於他們點擊了一個連接就開始打開。 Electron隨shell
模塊一塊兒載運,shell
模塊提供了一些與之相關的功能,高級桌面集成。shell模塊能夠詢問用戶的操做系統他們更喜歡哪一個瀏覽器,並將URL傳遞給要打開的瀏覽器。讓咱們從引入Electron開始,並在app/renderer.js
的頂部存儲對其shell模塊的引用。
列表2.35 引用Electron的shell 模塊: ./app/renderer.js
const {shell} = require('electron');
複製代碼
咱們可使用JavaScript來肯定咱們但願在應用程序中處理哪些url,以及咱們但願將哪些url傳遞給默認瀏覽器。在咱們的簡單應用程序中,區別很簡單。咱們但願全部的連接都在默認瀏覽器中打開。這個應用程序中正在添加和刪除連接,所以咱們在linksSection
元素上設置了一個事件監聽器,並容許單擊事件彈出。若是目標元素具備href
屬性,咱們將阻止默認操做並將URL傳遞給默認瀏覽器。
列表2.36 在默認瀏覽器中打開連接: ./app/renderer.js
+
|經過查找href屬性
|檢查被單擊的元素是否爲連接
linksSection.addEventListener('click', (event) => { |
if (event.target.href) { <---+
event.preventDefault(); <----+ shell.openExternal(event.target.href); <--+ |若是它不是一個鏈接, } | |不打開 Uses Electron’s shell module | + }); 在默認瀏覽器中使用Electorn | 打開連接 + 複製代碼
經過相對簡單的更改,咱們的代碼的行爲就像預期的那樣。單擊連接將在用戶的默認瀏覽器中打開該頁。咱們有一個簡單但功能齊全的桌面應用程序了。
咱們完成的代碼應該以下面的代碼示例所示。你可能以不一樣的順序使用您的功能。
列表2.37 完成的應用程序: ./app/renderer.js
const {shell} = require('electron');
const parser = new DOMParser();
const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');
newLinkUrl.addEventListener('keyup', () => {
newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});
newLinkForm.addEventListener('submit', (event) => {
event.preventDefault();
const url = newLinkUrl.value;
fetch(url)
.then(response => response.text())
.then(parseResponse)
.then(findTitle)
.then(title => storeLink(title, url))
.then(clearForm)
.then(renderLinks)
.catch(error => handleError(error, url));
});
clearStorageButton.addEventListener('click', () => {
localStorage.clear();
linksSection.innerHTML = '';
});
linksSection.addEventListener('click', (event) => {
if (event.target.href) {
event.preventDefault();
shell.openExternal(event.target.href);
}
});
const clearForm = () => {
newLinkUrl.value = null;
}
const parseResponse = (text) => {
return parser.parseFromString(text, 'text/html');
}
const findTitle = (nodes) => {
return nodes.querySelector('title').innerText;
}
const storeLink = (title, url) => {
localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
}
const getLinks = () => {
return Object.keys(localStorage)
.map(key => JSON.parse(localStorage.getItem(key)));
}
const convertToElement = (link) => {
return `<div class="link"><h3>${link.title}</h3> <p><a href="${link.url}">${link.url}</a></p></div>`;
}
const renderLinks = () => {
const linkElements = getLinks().map(convertToElement).join('');
linksSection.innerHTML = linkElements;
}
const handleError = (error, url) => {
errorMessage.innerHTML = ` There was an issue adding "${url}": ${error.message} `.trim();
setTimeout(() => errorMessage.innerText = null, 5000);
}
const validateResponse = (response) => {
if (response.ok) { return response; }
throw new Error(`Status code of ${response.status} ${response.statusText}`);
}
renderLinks();
複製代碼
npm init
從樣板文件中生產package.jsonrequire('electron')
來訪問Electron特定的模塊和功能。