第二章 你第首個Electron應用 | Electron in Action(中譯)


本章主要內容

  • 構建並啟動Electron應用

  • 生成package.json,配置成Electron應用

  • 在你的項目中包含預先構建Electron版本

  • 配置package.json以啟動主進程

  • 從主進程生成渲染進程

  • 利用Electron限制寬松的優點構建通常在瀏覽器無法構建的功能

  • 使用Electron的內置模塊來回避一些常見的問題

在第一章中,我們從高的層次上,討論了什么是Electron。說到底這本書叫做《Electron實戰》,對吧?在本章中,我們通過從頭開始設置和構建一個簡單的應用程序來管理書簽列表,從而學習Electron的基本知識。該應用程序將利用只有在現代的瀏覽器中才能使用的特性。

在上一章的高層次討論中,我提到了Electron是一個類似於Node的運行時。這仍然是正確的,但是我想回顧下這一點。Electron不是一個框架——它不提供任何框架,也沒有關於如何構造應用程序或命名文件的嚴格規則,這些選擇都留給了我們這些開發者。好的一面是,它也不強制執行任何約定,而且在入手之前,我們不需要多少概念上的樣板信息去學習。

 

構建書簽列表應用程序

 

讓我們從構建一個簡單而又有些幼稚的Electron應用程序開始,來加強我們已經介紹過的所有內容的理解。我們的應用程序接受url。當用戶提供URL時,我們獲取URL引用的頁面的標題,並將其保存在應用程序的localStorage中。最后,顯示應用程序中的所有鏈接。您可以在GitHub上找到本章的完整源代碼(https://github.com/electron-in-action/bookmarker)。

  在此過程中,我們將指出構建Electron應用程序的一些優點,例如,可以繞過對服務器的需求,使用最前沿的web api,這些web api並不廣泛支持所有瀏覽器,因為這些APIs是在現代版本的Chromium中實現。圖2.1是我們在本章構建的應用程序的效果圖。

圖2.1 我們在本章中構建的應用程序效果圖

 

  當用戶希望將網站URL保存並添加到輸入字段下面的列表中時,應用程序向網站發送一個請求來獲取標記。成功接收到標記后,應用程序獲取網站的標題,並將標題和URL添加到網站列表中,該列表存儲在瀏覽器的localStorage中。當應用程序啟動時,它從localStorage讀取並恢復列表。我們添加了一個帶有命令的按鈕來清除localStorage,以防出現錯誤。因為這個簡單的應用程序旨在幫助您熟悉Electron,所以我們不會執行高級操作,比如從列表中刪除單個網站。

 

搭建Electron應用

  1. npm init 生成package.json

  2. 搭建Electron目錄框架

 

應用程序結構的定義取決於您的團隊或個人處理應用程序的方式。許多開發人員采用的方法略有不同。觀察學習一些更成熟的電子應用程序,我們可以辨別出共同的模式,並在本書中決定如何處理我們的應用程序。

出於我們的目的,為了讓本書文件結構達成一致。做出一下規定,我們有一個應用程序目錄,其中存儲了所有的應用程序代碼。我們還有一個package.json將存儲依賴項列表、關於應用程序的元數據和腳本,並聲明Electron應該在何處查找主進程。在安裝了依賴項之后,最終會得到一個由Electron為我們創建的node_modules目錄,但是我們不會在初始設置中包含它

就文件而言,讓我們從應用程序中的兩個文件開始:main.jsrenderer.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

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。從源代碼編譯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對象和變量,比如_dirnameprocess模塊。同時,我們還有所有可用的瀏覽器APIs。只能在客戶端的工作和只能在服務端做的工作的分工開始消失不見。

圖2.6 一個帶有簡單HTML文檔的瀏覽器窗口

讓我們來看看實際情況。在傳統的瀏覽器環境中_dirname不可用,在Node中documentalert是不可用的。但在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 (http://browserify.org)或模塊bundlerwebpack,也可以使用任務運行器,如GulpGrunt

我們可以使用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實現跨域請求

你可能感覺到,也可能沒有感覺到,你脖子后面的一些毛發開始豎起來。你甚至可能對自己說:“這個計划不可能行得通。您不能向第三方服務器發出請求。瀏覽器不允許這樣做。”

通常來說,你是對的。在傳統的基於瀏覽器的應用程序中,不允許客戶端代碼向其他服務器發出請求。通常,客戶端代碼向服務器發出請求,然后將請求代理給第三方服務器。當它返回時,它將響應代理回客戶機。我們在第一章中討論了這背后的一些原因。

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();

 

總結

  • Electron不推薦或強制執行特定的項目結構。

  • Electron使用npm的package.json清單來決定那個文件被加載作為主進程

  • 我們通過使用npm init從樣板文件中生產package.json

  • 我們通常在每個項目中都在本地安裝Electron。這允許我們有特定項目版本的Electron。

  • 我們可以在Electron應用程序中使用require('electron')來訪問Electron特定的模塊和功能。

  • app模塊管理電子應用的生命周期。

  • 主進程無法呈現UI。

  • 我們可以使用Browser-window模塊從主進程創建渲染進程

  • Electron允許我們直接從第三方服務器發出請求,並不需要中間服務器的代理。傳統的web應用程序則不允許這樣做。

  • 在localStorage中存儲數據將允許它在我們退出並重新打開時保持。

 

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2zcuec310v8kg


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM