css
本章主要内容html
构造并设置Electron应用node
生成
package.json
,经过开发用Electron配置其工做webpack在你的项目中预先构建Electron版本git
配置你的
package.json
去启动主进程github从主进程生成渲染进程web
利用Electron沙盒,限制宽松的优势构建一般在浏览器没法构建的功能shell
使用Electron的内置模块来回避一些常见的问题npm
在第一章中,咱们从高的层次上,讨论了什么是Electron。说到底这本书叫作《Electron实战》,对吧?在本章中,咱们经过从头开始设置和构建一个简单的应用程序来管理书签列表,从而学习Electron的基本知识。该应用程序将利用只有在现代的浏览器中才能使用的特性。json
在上一章的高层次讨论中,我提到了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,因此咱们不会执行高级操做,好比从列表中删除单个网站。
应用程序结构的定义取决于您的团队或我的处理应用程序的方式。许多开发人员采用的方法略有不一样。观察学习一些更成熟的电子应用程序,咱们能够辨别出共同的模式,并在本书中决定如何处理咱们的应用程序。
出于咱们的目的,为了让本书文件结构达成一致。作出一下规定,咱们有一个应用程序目录,其中存储了全部的应用程序代码。咱们还有一个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
(http://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();