构建并启动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应用
-
npm init 生成
package.json
-
搭建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
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对象和变量,比如_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实现跨域请求
你可能感觉到,也可能没有感觉到,你脖子后面的一些毛发开始竖起来。你甚至可能对自己说:“这个计划不可能行得通。您不能向第三方服务器发出请求。浏览器不允许这样做。”
通常来说,你是对的。在传统的基于浏览器的应用程序中,不允许客户端代码向其他服务器发出请求。通常,客户端代码向服务器发出请求,然后将请求代理给第三方服务器。当它返回时,它将响应代理回客户机。我们在第一章中讨论了这背后的一些原因。
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.