Angular 项目第三版(二)
原文:
zh.annas-archive.org/md5/a5e00c8078625cc7b0d1e54adc65ace7译者:飞龙
第四章:使用 Angular 服务工作者构建 PWA 天气应用程序
我们可以使用不同类型的设备访问网络应用程序,例如桌面、移动或平板电脑,以及各种类型的网络,如宽带、Wi-Fi 和蜂窝网络。网络应用程序应该无缝工作,并独立于用户的设备和网络提供相同的用户体验。
渐进式网络应用(PWA) 是考虑到上述因素构建的应用程序。一种流行的技术是 服务工作者,它可以提高 Web 应用程序的加载时间。在本章中,我们将使用 Angular 框架的服务工作者实现来构建一个 PWA,使用 OpenWeather API 显示城市的天气。
我们将详细介绍以下主题:
-
设置 OpenWeather API
-
显示天气数据
-
使用服务工作者启用离线模式
-
通过应用内通知保持最新
-
使用 Firebase Hosting 部署我们的应用程序
必要的背景理论和上下文
传统的网络应用程序通常托管在 Web 服务器上,并且任何给定时间对任何用户都是立即可用的。本地应用程序安装在用户的设备上,可以访问其本地资源,并且可以与任何网络无缝工作。PWA 横跨 Web 和本地应用程序的两个世界,并具有两者的特征,总结如下:
-
能力:它可以访问本地保存的数据,并与连接到用户设备的外围硬件交互。
-
可靠性:它可以在任何网络连接中提供相同的性能和体验,即使在网络连接和覆盖范围较低的地区。
-
可安装性:它可以在用户的设备上安装,可以直接从主屏幕启动,并与其他已安装的本地应用程序交互。
将网络应用程序转换为 PWA 涉及多个步骤和技术。其中最重要的一项是配置服务工作者。服务工作者是一种在 Web 浏览器上运行的机制,充当应用程序与外部 HTTP 端点或其他应用程序内资源(如 JavaScript 和 CSS 文件)之间的代理。服务工作者的主要任务是拦截对这些资源的请求,并通过提供缓存的或实时响应来对其采取行动。
服务工作者在浏览器标签页关闭后仍然保持持久。
Angular 框架提供了一个服务工作者的实现,我们可以使用它将我们的 Angular 应用程序转换为 PWA。
它还包含一个内置的 HTTP 客户端,我们可以使用它通过 HTTP 与服务器通信。Angular HTTP 客户端公开了一个基于观察器的 API,具有所有标准 HTTP 方法,如 POST 和 GET。观察器基于观察者模式,这是响应式编程的核心。在观察者模式中,多个称为观察者的对象可以订阅观察器并接收有关其状态变化的任何通知。观察器通过异步发射事件流来向观察者发送更改。Angular 框架使用一个名为RxJS的库,其中包含用于处理观察器的各种工具。其中之一是一组称为操作符的函数,可以对观察器应用各种操作,如转换和过滤。接下来,让我们对我们的项目进行概述。
项目概述
在这个项目中,我们将构建一个 PWA 来显示城市的天气状况。最初,我们将学习如何配置 OpenWeather API,我们将使用它来获取天气数据。然后,我们将学习如何使用 API 在 Angular 组件中显示天气信息。我们将了解如何使用服务工作者将我们的 Angular 应用程序转换为 PWA。我们还将为我们的应用程序实现更新通知机制。最后,我们将把我们的 PWA 部署到 Firebase Hosting 提供者。以下图表展示了项目的架构概述:
图 4.1 – 项目架构
构建时间:90 分钟
入门
完成此项目所需的以下软件工具:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
GitHub 材料:本章的相关代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter04文件夹中找到。
设置 OpenWeather API
OpenWeather 团队创建了 OpenWeather API,该 API 包含来自全球 200,000 多个城市的当前和历史天气信息。它还支持更详细信息的天气预报数据。
我们需要首先获取一个 API 密钥,才能开始使用 OpenWeather API:
-
导航到 OpenWeather API 网站
openweathermap.org/api。您将看到 OpenWeather 团队提供的所有可用 API 的列表。
-
找到当前天气数据部分,并点击订阅按钮。
您将被重定向到包含服务可用定价方案的页面。每个方案支持每分钟和每月不同的 API 调用组合。对于这个项目,我们将使用免费级别。
-
点击获取 API 密钥按钮。
您将被重定向到服务的注册页面。
-
完成所有必填信息,并点击创建账户按钮。
将确认消息发送到您用于创建账户的电子邮件地址。
-
找到确认电子邮件并点击 验证您的电子邮件 按钮以完成注册。
您将很快收到来自 OpenWeather 的另一封电子邮件,其中包含有关您当前订阅的详细信息,包括您的 API 密钥以及您将与 API 通信的 HTTP 端点。
API 密钥可能需要一些时间才能激活,通常在您可以使用它之前需要几个小时。
一旦 API 密钥被激活,我们就可以在 Angular 应用程序中使用它。我们将在下一节中学习如何做到这一点。
显示天气数据
在本节中,我们将创建一个 Angular 应用程序来显示给定城市的天气信息。用户将在输入字段中输入城市的名称,应用程序将使用 OpenWeather API 获取指定城市的天气数据。我们将更详细地介绍以下主题:
-
设置 Angular 应用程序
-
与 OpenWeather API 通信
-
显示城市的天气信息
让我们从创建 Angular 应用程序开始,接下来的部分将介绍如何进行。
设置 Angular 应用程序
我们将使用 Angular CLI 的 ng new 命令从头创建一个新的 Angular 应用程序:
ng new weather-app --style=scss --routing=false
上述命令将创建一个新的 Angular CLI 应用程序,具有以下属性:
-
weather-app:Angular 应用程序的名称 -
--style=scss:表示我们的 Angular 应用程序将使用 SCSS 样式表格式 -
--routing=false:禁用应用程序中的 Angular 路由
用户应在输入字段中输入城市的名称,并且该城市的天气信息应以卡片布局进行可视化。Angular Material 库提供了一套 UI 组件来满足我们的需求,包括输入和卡片。
Angular Material 组件遵循 Material Design 原则,并由 Angular 的 Components 团队维护。我们可以使用以下 Angular CLI 命令安装 Angular Material 库:
ng add @angular/material --theme=indigo-pink --animations=enabled --typography
上述代码使用了 Angular CLI 的 ng add 命令,并传递了额外的配置选项:
-
@angular/material:Angular Material 库的 npm 包名。它还将安装 Angular CDK 包,这是一个用于构建 Angular Material 的行为和交互的集合。这两个包都将添加到应用程序的package.json文件的dependencies部分中。 -
--theme=indigo-pink:我们想要使用的 Angular Material 主题的名称。添加主题涉及修改 Angular CLI 工作区的几个文件。它将 CSS 主题文件的条目添加到angular.json配置文件中:@angular/material/prebuilt-themes/indigo-pink.css它还包括
index.html文件中的 Material Design 图标:<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">Angular Material 随带一套预定义的主题,我们可以使用。或者,我们可以构建一个符合我们特定需求的自定义主题。
-
--animations=enabled:通过将BrowserAnimationsModule导入主应用程序模块app.module.ts来在应用程序中启用浏览器动画:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; **import** **{** **BrowserAnimationsModule** **}** **from****'@angular/platform-browser/animations'****;** @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **BrowserAnimationsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
--typography:在应用程序中全局启用 Angular Material 字体排印。字体排印定义了文本内容的显示方式,并默认使用 Roboto 字体,该字体包含在index.html文件中:<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">它向 HTML 文件的
<body>标签中添加以下类:<body **class****=****"mat-typography"**> <app-root></app-root> </body>它还向应用程序的全局
styles.scss文件中添加了一些 CSS 样式:html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
我们现在拥有了构建 Angular 应用程序的所有组件。在下一节中,我们将创建一个与 OpenWeather API 交互的机制。
与 OpenWeather API 通信
应用程序应通过 HTTP 与 OpenWeather API 交互以获取天气数据。让我们看看我们如何在应用程序中设置这种类型的通信:
-
首先,我们必须创建一个接口来描述我们将从 API 获取的数据类型。使用以下 Angular CLI 命令创建一个:
ng generate interface weather上述命令将在 Angular CLI 项目的
src\app文件夹中创建weather.ts文件。 -
打开
weather.ts文件并按以下方式修改它:export interface Weather { weather: WeatherInfo[], main: { temp: number; pressure: number; humidity: number; }; wind: { speed: number; }; sys: { country: string }; name: string; } interface WeatherInfo { main: string; icon: string; }每个属性都对应于 OpenWeather API 响应中的天气字段。您可以在
openweathermap.org/current#parameter上找到每个字段的描述。然后,我们必须设置 Angular 框架提供的内置 HTTP 客户端。
-
打开
app.module.ts文件并将HttpClientModule添加到@NgModule装饰器的imports数组中:**import** **{** **HttpClientModule** **}** **from****'@angular/common/http'****;** import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, **HttpClientModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
使用以下 Angular CLI 命令创建一个新的 Angular 服务:
ng generate service weather上述命令将在 Angular CLI 项目的
src\app文件夹中创建weather.service.ts文件。 -
打开
weather.service.ts文件并将HttpClient服务注入到其constructor中:**import** **{** **HttpClient** **}** **from****'@angular/common/http'****;** import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class WeatherService { constructor(**private** **http: HttpClient**) { } } -
将以下属性添加以定义 OpenWeather API 的端点 URL 和我们的 API 密钥:
private apiUrl = 'https://api.openweathermap.org/data/2.5/'; private apiKey = '<Your API key>';将
apiKey属性的值替换为您拥有的 API 密钥。-
在服务中添加一个方法,该方法接受城市名称作为单个参数并查询 OpenWeather API 以获取该城市的天气:
getWeather(city: string): Observable<Weather> { const options = new HttpParams() .set('units', 'metric') .set('q', city) .set('appId', this.apiKey); return this.http.get<Weather>(this.apiUrl + 'weather', { params: options }); }
getWeather方法使用HttpClient服务的get方法,它接受两个参数。第一个参数是 OpenWeather API 的 URL 端点。第二个参数是一个options对象,用于将额外的配置传递给请求,例如带有params属性的 URL 查询参数。我们使用
HttpParams对象的构造函数并调用其set方法为要添加到 URL 的每个查询参数。在我们的例子中,我们传递q参数用于城市名称,appId用于 API 密钥,以及我们想要使用的units类型。您可以在openweathermap.org/current#data上了解更多关于支持的单位信息。我们使用
set方法创建查询参数,因为HttpParams对象是不可变的。为每个要传递的参数调用构造函数将引发错误。我们还在
get方法中将响应数据类型设置为Weather。请注意,getWeather方法不返回Weather数据,而是一个此类型的Observable。 -
-
在文件顶部添加以下
import语句:import { HttpClient, **HttpParams** } from '@angular/common/http'; import { Injectable } from '@angular/core'; **import** **{** **Observable** **}** **from****'rxjs'****;** **import** **{** **Weather** **}** **from****'./weather'****;**
我们创建的 Angular 服务包含与 OpenWeather API 交互所需的所有必要组件。在下一节中,我们将创建一个 Angular 组件来发起请求并显示数据。
显示城市的天气信息
用户应该能够使用我们应用程序的 UI 并输入他们想要查看天气详情的城市名称。应用程序将使用该信息查询 OpenWeather API,并将请求结果以卡片布局的形式显示在 UI 上。让我们开始构建一个 Angular 组件来创建所有这些类型的交互:
-
使用以下 Angular CLI 命令创建 Angular 组件:
ng generate component weather -
打开主组件的模板,
app.component.html,并用新组件的选择器<app-weather>替换其内容:<app-weather></app-weather> -
打开
app.module.ts文件,并将 Angular Material 库中的以下模块添加到@NgModule装饰器的imports数组中:@NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, **MatIconModule****,** **MatInputModule****,** **MatCardModule** ], providers: [], bootstrap: [AppComponent] })还需要在文件顶部添加必要的
import语句:import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -
打开
weather.component.ts文件,为Weather类型创建一个weather属性,并将WeatherService注入到WeatherComponent类的constructor中:import { Component } from '@angular/core'; **import** **{** **Weather** **}** **from****'****../weather'****;** **import** **{** **WeatherService** **}** **from****'../weather.service'****;** @Component({ selector: 'app-weather', templateUrl: './weather.component.html', styleUrls: ['./weather.component.scss'] }) export class WeatherComponent { **weather****:** **Weather** **|** **undefined****;** **constructor****(****private** **weatherService: WeatherService****){ }** } -
创建一个组件方法,它订阅
WeatherService的getWeather方法,并将结果分配给weather组件属性:search(city: string) { this.weatherService.getWeather(city).subscribe(weather => this.weather = weather); }
我们已经完成了与组件的 TypeScript 类文件的协作。现在让我们将其连接到其模板。打开weather.component.html文件,并用以下 HTML 代码替换其内容:
<mat-form-field>
<input matInput placeholder="Enter city" #cityCtrl (keydown.enter)="search(cityCtrl.value)">
<mat-icon matSuffix (click)="search(cityCtrl.value)">search</mat-icon>
</mat-form-field>
<mat-card *ngIf="weather">
<mat-card-header>
<mat-card-title>{{weather.name}}, {{weather.sys.country}}</mat-card-title>
<mat-card-subtitle>{{weather.weather[0].main}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="img/{{weather.weather[0].icon}}@2x.png" [alt]="weather.weather[0].main">
<mat-card-content>
<h1>{{weather.main.temp | number:'1.0-0'}} ℃</h1>
<p>Pressure: {{weather.main.pressure}} hPa</p>
<p>Humidity: {{weather.main.humidity}} %</p>
<p>Wind: {{weather.wind.speed}} m/s</p>
</mat-card-content>
</mat-card>
上述模板由 Angular Material 库中的几个组件组成,包括一个包含以下子元素的<mat-form-field>组件:
-
一个用于输入城市名称的
<input>HTML 元素。当用户完成编辑并按下Enter键时,它会调用search组件方法,并将cityCtrl变量的值属性作为参数传递。cityCtrl变量是一个模板引用变量,表示原生 HTML<input>元素的实体对象。 -
<mat-icon>组件在输入元素的末尾显示一个放大镜图标,如matSuffix指令所示。点击时,它还会调用search组件的方法。
cityCtrl模板引用变量由#表示,并在组件模板内部任何地方都可以访问。
<mat-card> 组件以卡片布局展示信息,并且仅在 weather 组件属性有值时显示。它由以下子元素组成:
-
<mat-card-header>:卡片的头部。它由一个<mat-card-title>组件组成,显示城市名称和国家代码,以及一个<mat-card-subtitle>组件,显示当前的天气状况。 -
mat-card-image:显示天气状况图标的卡片图片,以及作为替代文本的描述。 -
<mat-card-content>:卡片的主要内容。它显示当前天气的温度、压力、湿度和风速。温度以没有小数点的形式显示,如number管道所示。
现在我们来增加一些样式,让我们的组件更有趣:
weather.component.scss
:host {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding-top: 25px;
}
mat-form-field {
width: 20%;
}
mat-icon {
cursor: pointer;
}
mat-card {
margin-top: 30px;
width: 250px;
}
h1 {
text-align: center;
font-size: 2.5em;
}
:host 选择器是 Angular 独特的 CSS 选择器,它针对托管我们的组件的 HTML 元素,在我们的例子中,是 <app-weather> HTML 元素。
如果我们使用 ng serve 运行我们的应用程序,导航到 http://localhost:4200,并在 Athens 中搜索天气信息,我们应该在屏幕上得到以下输出:
图 4.2 – 应用程序输出
恭喜!现在,你拥有了一个完全工作的 Angular 应用程序,它可以显示特定城市的天气信息。该应用程序由一个单一的 Angular 组件组成,通过 Angular 服务使用 HTTP 与 OpenWeather API 进行通信。我们学习了如何使用 Angular Material 来美化我们的组件,并让我们的用户在使用我们的应用程序时获得愉悦的体验。但当我们离线时会发生什么?应用程序是否按预期工作?用户的体验是否保持不变?让我们在下一节中找出答案。
通过服务工作者启用离线模式
来自任何地方的用户现在都可以访问我们的 Angular 应用程序,以获取他们感兴趣的任何城市的天气信息。当我们说“任何地方”时,我们指的是任何网络类型,例如宽带、蜂窝(3G/4G/5G)和 Wi-Fi。考虑一个用户处于覆盖范围低或频繁断网的地方的情况。我们的应用程序会如何表现?让我们通过实验来找出答案:
-
使用 Angular CLI 的
ng serve命令运行 Angular 应用程序。 -
打开你喜欢的浏览器,导航到
http://localhost:4200,这是 Angular CLI 项目的默认地址和端口号。你应该能看到输入字段,用于输入城市的名称:
图 4.3 – 输入城市名称
- 打开你浏览器的开发者工具,并导航到 网络 选项卡。将 Throttling 下拉菜单的值设置为 Offline:
图 4.4 – 离线网络模式
- 尝试刷新您的浏览器。您将看到您已从互联网断开连接的指示,如下面的截图所示:
图 4.5 – 无互联网连接(Google Chrome)
在低质量互联网连接的地区,这种情况是标准的。那么,我们能为这样的用户做些什么呢?幸运的是,Angular 框架包含了一个服务工作者的实现,当在离线模式下运行时,它可以显著提高我们应用程序的用户体验。它可以缓存某些应用程序部分并相应地提供它们,而不是进行实际请求。
Angular 服务工作者也可以用于具有大网络延迟连接的环境。考虑在这种类型的网络中也使用服务工作者来改善用户的体验。
运行以下 Angular CLI 命令以在我们的 Angular 应用程序中启用服务工作者:
ng add @angular/pwa
上述命令将根据 PWA 支持相应地转换 Angular CLI 工作区:
-
它将
@angular/service-workernpm 包添加到应用程序的package.json文件的dependencies部分。 -
它在应用程序的
src文件夹中创建manifest.webmanifest文件。该清单文件包含有关应用程序的信息,这些信息是安装和运行原生应用程序所需的。它还将其添加到angular.json文件的build选项的assets数组中。 -
它在项目根目录中创建
ngsw-config.json文件,这是服务工作者配置文件。我们使用它来定义特定配置的工件,例如哪些资源被缓存以及如何缓存。您可以在以下链接中找到有关服务工作者配置的更多详细信息:angular.io/guide/service-worker-config#service-worker-configuration。 -
配置文件也在
angular.json文件的build配置中的ngswConfigPath属性中设置。 -
它在
angular.json文件的build配置中将serviceWorker属性设置为true。 -
它在
app.module.ts文件中注册服务工作者:@NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, **ServiceWorkerModule****.****register****(****'ngsw-worker.js'****, {** **enabled****: !****isDevMode****(),** **// Register the ServiceWorker as soon as the application is stable** **// or after 30 seconds (whichever comes first).** **registrationStrategy****:** **'registerWhenStable:30000'** **})** ], providers: [], bootstrap: [AppComponent] }) -
ngsw-worker.js文件是包含服务工作者实际实现的 JavaScript 文件。当构建我们的应用程序时,它会为我们自动创建。Angular 使用ServiceWorkerModule类的register方法在我们的应用程序中注册它。 -
它为当应用程序作为原生应用安装到用户设备上时使用创建几个图标。
-
它在
index.html文件的<head>元素中包含清单文件和一个theme-color的<meta>标签:<link rel="manifest" href="manifest.webmanifest"> <meta name="theme-color" content="#1976d2">
现在我们已经完成了服务工作者的安装,是时候测试它了!在继续之前,我们应该安装一个外部网络服务器,因为 Angular CLI 的内置功能不支持服务工作者。一个不错的选择是 http-server:
-
运行 npm 客户端的
install命令来安装http-server:npm install -D http-server上述命令将
http-server作为我们的 Angular CLI 项目的开发依赖项安装。 -
使用 Angular CLI 的
ng build命令构建 Angular 应用程序。 -
打开 Angular CLI 工作区的
package.json文件,并将以下条目添加到scripts属性中:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", **"server"****:** **"http-server -p 8080 -c-1 dist/weather-app"** } -
使用以下命令启动 HTTP 网络服务器:
npm run server上述命令将在端口 8080 上启动 http-server 并禁用缓存。
-
打开您的浏览器并导航到
http://localhost:8080。建议在私密或隐身模式下打开页面,以避免服务工作者出现意外行为。
-
重复本节开头为切换到离线模式所遵循的过程。
-
如果您现在刷新页面,您会注意到应用程序按预期工作。
服务工作者为我们做了所有工作,整个过程如此无缝,以至于我们无法判断我们是处于在线还是离线状态。您可以通过检查 网络 选项卡来验证这一点:
图 4.6 – 服务工作者(离线模式)
大小 列中的 (ServiceWorker) 值表示服务工作者为我们提供了应用程序的缓存版本。
我们已成功安装服务工作者,并更接近将我们的应用程序转换为 PWA。在下一节中,我们将学习如何通知用户应用程序的潜在更新。
保持应用程序内通知的更新
当我们想在网络应用程序中应用更改时,我们进行更改并构建应用程序的新版本。然后,应用程序被部署到网络服务器,每个用户都可以立即访问最新版本。但 PWA 是不同的。
当我们部署 PWA 的新版本时,服务工作者必须相应地采取行动并应用特定的更新策略。它应该通知用户新版本或立即安装它。我们遵循哪种更新策略取决于我们的需求。在这个项目中,我们想向用户显示提示,并让他们决定是否想要更新应用程序。让我们看看如何在我们的应用程序中实现这个功能:
-
打开
app.module.ts文件,并将MatSnackBarModule添加到@NgModule装饰器的imports数组中:**import** **{** **MatSnackBarModule** **}** **from****'@angular/material/snack-bar'****;** @NgModule({ declarations: [ AppComponent, WeatherComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, **MatSnackBarModule**, ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), // Register the ServiceWorker as soon as the application is stable // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000' }) ], providers: [], bootstrap: [AppComponent] })MatSnackBarModule是一个 Angular Material 模块,它允许我们与 snack bars 进行交互。snack bar 是一个通常出现在页面底部的弹出窗口,用于通知目的。 -
打开
app.component.ts文件,并将OnInit接口添加到AppComponent类实现的接口中:import { Component, **OnInit** } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent **implements****OnInit** { title = 'weather-app'; } -
在
AppComponent类的constructor中注入MatSnackBar和SwUpdate服务:import { Component, OnInit } from '@angular/core'; **import** **{** **MatSnackBar** **}** **from****'@angular/material/snack-bar'****;** **import** **{** **SwUpdate****,** **VersionReadyEvent** **}** **from****'@angular/service-worker'****;** @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { title = 'weather-app'; **constructor****(****private** **updates: SwUpdate,** **private** **snackbar: MatSnackBar****) {}** }MatSnackBar服务是从MatSnackBarModule中公开的 Angular 服务。SwUpdate服务是服务工作者的一部分,包含我们可以用来通知用户应用程序更新过程的可观察对象。 -
创建以下
ngOnInit方法:ngOnInit() { this.updates.versionUpdates.pipe( filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), switchMap(() => this.snackbar.open('A new version is available!', 'Update now').afterDismissed()), filter(result => result.dismissedByAction), map(() => this.updates.activateUpdate().then(() => location.reload())) ).subscribe(); }ngOnInit方法是OnInit接口的一个实现方法,在组件初始化时被调用。SwUpdate服务包含一个versionUpdates可观察属性,我们可以使用它来获取通知,当我们的应用程序有新版本可用时。通常,我们倾向于订阅可观察对象,但在这个情况下,我们没有这样做。相反,我们订阅了pipe方法,这是一个用于组合多个操作符的 RxJS 操作符。 -
在
app.component.ts文件的顶部添加以下import语句:import { filter, map, switchMap } from 'rxjs';
在我们之前定义的 ngOnInit 方法内部有很多事情在进行,所以让我们将其分解成几个部分以进一步理解它。pipe 操作符组合了四个 RxJS 操作符:
-
filter:我们使用它来过滤掉从versionUpdates可观察对象发出的除表示版本准备安装之外的所有值。` -
switchMap: 当我们的应用程序有新版本可用时,会调用此方法。它使用snackbar属性的open方法来显示带有操作按钮的 snack bar,并订阅其afterDismissed可观察对象。afterDismissed可观察对象在 snack bar 通过点击操作按钮或使用其 API 方法程序化关闭时发出。 -
filter: 当使用操作按钮关闭 snack bar 时,会调用此方法。 -
map: 这调用updates属性的activateUpdate方法来应用应用程序的新版本。一旦应用程序已更新,它将重新加载浏览器窗口以使更改生效。
让我们看看更新到新版本的整个过程:
-
运行以下 Angular CLI 命令来构建 Angular 应用程序:
ng build -
启动 HTTP 服务器以提供应用程序:
npm run server -
打开一个私密或隐身浏览器窗口,并导航到
http://localhost:8080。 -
在不关闭浏览器窗口的情况下,让我们在我们的应用程序中引入一个更改并添加一个 UI 标题。运行 Angular CLI 的
generate命令来创建一个组件:ng generate component header -
打开
app.module.ts文件并导入以下 Angular Material 模块:**import** **{** **MatButtonModule** **}** **from****'@angular/material/button'****;** **import** **{** **MatToolbarModule** **}** **from****'@angular/material/toolbar'****;** @NgModule({ declarations: [ AppComponent, WeatherComponent, HeaderComponent ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, MatIconModule, MatInputModule, MatCardModule, MatSnackBarModule, **MatButtonModule****,** **MatToolbarModule****,** ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), // Register the ServiceWorker as soon as the application is stable // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000' }) ], providers: [], bootstrap: [AppComponent] }) -
打开
header.component.html文件,并创建一个包含两个 HTML<button>元素的<mat-toolbar>组件,每个按钮都包含一个<mat-icon>组件:<mat-toolbar color="primary"> <span>Weather App</span> <span class="spacer"></span> <button mat-icon-button> <mat-icon>refresh</mat-icon> </button> <button mat-icon-button> <mat-icon>share</mat-icon> </button> </mat-toolbar> -
将以下 CSS 样式添加到
header.component.scss文件中,以便将按钮定位在标题的右端:.spacer { flex: 1 1 auto; } -
打开
app.component.html文件,并在顶部添加<app-header>组件:**<****app-header****></****app-header****>** <app-weather></app-weather> -
重复步骤 1 和 2,并刷新指向
http://localhost:8080的浏览器窗口。几秒钟后,你应在页面底部看到以下通知:
图 4.7 – 新版本通知
- 点击 立即更新 按钮,等待浏览器窗口重新加载,你应该能看到你的更改:
图 4.8 – 应用程序输出
我们的 Angular 应用程序已经开始转变为 PWA 应用程序。随着 Angular 服务工作者提供的缓存机制,我们添加了安装我们应用程序新版本的机制。在下一节中,我们将学习如何在我们的设备上本地部署和安装我们的应用程序。
使用 Firebase Hosting 部署我们的应用程序
Firebase 是由 Google 提供的托管解决方案,我们可以使用它来部署我们的 Angular 应用程序。Firebase 团队投入了大量精力创建了一个 Angular CLI 脚本,用于通过单个命令部署 Angular 应用程序。在深入了解之前,让我们学习如何设置 Firebase Hosting:
-
使用 Google 账户登录 Firebase,网址为
console.firebase.google.com。 -
点击 添加项目 按钮以创建新的 Firebase 项目。
-
输入项目名称,
weather-app,然后点击 继续 按钮。Firebase 在项目名称下方生成一个独特的标识符,例如 weather-app-b11a2,该标识符将在项目的主机 URL 中使用。
-
禁用项目中 Google Analytics 的使用,然后点击 创建项目 按钮。
-
一旦项目创建完成,屏幕上将会显示以下内容:
图 4.9 – Firebase 项目创建
- 点击 继续 按钮,你将被重定向到你的新 Firebase 项目仪表板。
我们现在已经完成了 Firebase Hosting 的配置。现在是时候将其与我们的 Angular 应用程序集成:
-
在终端窗口中运行以下命令来安装 Firebase 工具:
npm install -g firebase-tools -
在相同的终端窗口中运行以下命令以使用 Firebase CLI 进行身份验证:
firebase login -
最后,运行以下 Angular CLI 命令来在 Angular CLI 项目中安装
@angular/firenpm 包:ng add @angular/fire上述命令将找到库的最新版本,并提示我们安装它。
-
首先,它将询问我们想要启用 Firebase 的哪些功能:
? What features would you like to setup?确保选择了
ng deploy -- hosting选项,然后按 Enter。 -
然后,它将询问我们想要使用哪个 Firebase 账户:
? Which Firebase account would you like to use?确保选择了之前使用的账户,然后按 Enter。
-
在下一个问题中,我们将选择我们将要部署应用程序的项目:
? Please select a project:选择我们之前创建的
weather-app项目,然后按 Enter。 -
最后,我们必须选择将托管我们应用程序的网站:
? Please select a hosting site:
选择我们之前创建的托管网站,然后按 Enter。
之前的流程将根据需要修改 Angular CLI 工作区,以适应其部署到 Firebase:
-
它将在根目录下创建一个
.firebaserc文件,其中包含所选 Firebase 项目的详细信息。 -
它将在根目录下创建一个
firebase.json文件,这是 Firebase 配置文件。配置文件指定了将部署到 Firebase 的文件夹以及任何重写规则。默认部署的文件夹是当运行
ng build命令时由 Angular CLI 创建的dist输出文件夹。 -
它将在
angular.json配置文件中添加一个deploy目标。
要部署应用程序,我们只需要运行一个 Angular CLI 命令,Angular CLI 将处理其余部分:
ng deploy
之前的命令将构建并将应用程序部署到所选的 Firebase 项目。一旦部署完成,Angular CLI 将报告以下信息:
-
项目控制台:Firebase 项目的仪表板。
-
托管 URL:已部署应用程序版本的 URL。它由 Firebase 项目的唯一标识符和 Firebase 自动添加的
.web.app后缀组成。
服务工作者需要以 HTTPS 协议提供服务才能作为 PWA 正确工作,除了用于开发的 localhost 之外。Firebase 默认使用 HTTPS 托管 Web 应用程序。
现在我们已经部署了我们的应用程序,让我们看看我们如何将其作为 PWA 安装到我们的设备上:
-
导航到托管 URL,然后在浏览器地址栏旁边点击 安装 weather-app 按钮:
图 4.10 – 安装应用程序(Google Chrome)
安装按钮可能在其他浏览器的不同位置。
浏览器将提示我们安装应用程序。
-
点击 安装 按钮,应用程序将在我们的设备上以原生窗口的形式打开:
![图片 B18465_04_11.png]
图 4.11 – PWA
它还会在我们的设备上创建一个启动应用程序的快捷方式。恭喜!我们现在有一个完整的 PWA,可以显示城市的天气信息。
摘要
在本章中,我们构建了一个显示特定城市天气信息的 PWA。
初始时,我们设置了 OpenWeather API 以获取天气数据,并从头创建了一个 Angular 应用程序来集成它。我们学习了如何使用 Angular 框架内置的 HTTP 客户端与 OpenWeather API 进行通信。我们还安装了 Angular Material 库,并使用了一些现成的 UI 组件来构建我们的应用程序。
在创建 Angular 应用程序后,我们介绍了 Angular 服务工作者并使其能够离线工作。我们学习了如何与服务工作者交互并为我们的应用程序提供更新通知。最后,我们将应用程序的生产版本部署到 Firebase Hosting 并将其安装到我们的设备上。
在下一章中,我们将学习如何使用 Electron(PWAs 的主要竞争对手)创建 Angular 桌面应用程序。
练习
使用 OpenWeather API 显示所选城市的每周天气预报。OpenWeather API 提供了5 天/3 小时预报集合,可以用于此。该集合为每天每 3 小时返回一次预报,因此,对于每周预报,你只需关注每天中午 12:00 的天气即可。预报应以网格列表的形式显示为卡片组件,并应位于城市当前天气下方。
你可以在github.com/PacktPublishing/Angular-Projects-Third-Edition/tree/exercise的exercise分支的Chapter04文件夹中找到练习的解决方案。
进一步阅读
-
OpenWeather API:
openweathermap.org/api -
Angular Material:
material.angular.io -
Angular HTTP 客户端:
angular.io/guide/http -
Angular 服务工作者:
angular.io/guide/service-worker-getting-started -
与 Angular 服务工作者通信:
angular.io/guide/service-worker-communications -
HTTP 服务器:
www.npmjs.com/package/http-server -
Firebase Hosting:
firebase.google.com/docs/hosting -
Angular 部署:
angular.io/guide/deployment#automatic-deployment-with-the-cli
第五章:使用电子构建桌面 WYSIWYG 编辑器
Web 应用程序传统上是用 HTML、CSS 和 JavaScript 构建的。它们的使用也广泛扩展到使用Node.js的服务器开发。近年来,出现了各种工具和框架,它们使用 HTML、CSS 和 JavaScript 来创建桌面和移动应用程序。在本章中,我们将探讨如何使用 Angular 和电子创建桌面应用程序。
电子是一个 JavaScript 框架,用于使用 Web 技术构建原生桌面应用程序。结合 Angular 框架,我们可以创建快速且高性能的 Web 应用程序。在本章中,我们将构建一个桌面WYSIWYG编辑器,并涵盖以下主题:
-
为 Angular 添加 WYSIWYG 编辑器库
-
在工作区中集成电子
-
Angular 与电子之间的通信
-
打包桌面应用程序
必要的背景理论和上下文
电子框架是一个跨平台框架,用于构建 Windows、Linux 和 Mac 桌面应用程序。许多流行的应用程序,如 Visual Studio Code、Skype 和 Slack,都是使用电子框架制作的。电子框架建立在 Node.js 和 Chromium 之上。Web 开发者可以利用他们现有的 HTML、CSS 和 JavaScript 技能来创建桌面应用程序,而无需学习新的语言,如 C++或 C#。
电子应用程序与 PWA 应用程序有许多相似之处。考虑为以下场景构建电子应用程序,如高级文件系统操作或当你需要为你的应用程序提供更原生的外观和感觉时。另一个用例是当你为你的主要桌面产品构建补充工具并希望将它们一起发布时。
电子应用程序由两个进程组成:
-
主进程:这通过 Node.js API 与本地资源进行交互。
-
渲染器:负责管理应用程序的用户界面。
电子应用程序只能有一个主进程,该进程与一个或多个渲染进程进行通信。每个渲染进程与其他进程完全隔离运行。
电子框架提供了ipcMain和ipcRenderer接口,我们可以使用这些接口与这些进程进行交互。交互是通过**进程间通信(IPC)**完成的,这是一种通过基于 Promise 的 API 在公共通道上安全异步交换消息的机制。
项目概述
在此项目中,我们将构建一个桌面 WYSIWYG 编辑器,其内容保留在文件系统中。最初,我们将使用 ngx-wig,一个流行的 WYSIWYG Angular 库,将其构建为 Angular 应用程序。然后,我们将使用 Electron 将其转换为桌面应用程序,并学习如何在 Angular 和 Electron 之间同步内容。我们还将了解如何将编辑器的内容持久化到文件系统中。最后,我们将打包我们的应用程序为一个可执行的单一文件,该文件可以在桌面环境中运行。以下图表描述了项目的架构概述:
图 5.1 – 项目架构
构建时间:1 小时。
开始使用
完成此项目所需的软件工具如下:
-
Angular CLI:Angular 的命令行界面,您可以在
angular.io/cli找到。 -
Visual Studio Code:一个代码编辑器,您可以从
code.visualstudio.com下载。 -
GitHub 材料:本章的代码可以在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter05文件夹中找到。
为 Angular 添加 WYSIWYG 编辑器库
我们将通过创建一个作为 Angular 应用程序的 WYSIWYG 编辑器来启动我们的项目。使用 Angular CLI 从头开始创建一个新的 Angular 应用程序:
ng new my-editor --defaults
我们将以下选项传递给 ng new 命令:
-
my-editor: 定义应用程序的名称 -
--defaults: 定义 CSS 为应用程序的首选样式表格式,并禁用路由,因为我们的应用程序将仅由一个包含编辑器的组件组成
WYSIWYG 编辑器是一种富文本编辑器,例如 Microsoft Word。我们可以使用 Angular 框架从头开始创建一个,但这将非常耗时,我们只会重蹈覆辙。Angular 生态系统包含大量用于此目的的库。其中之一是 ngx-wig 库,它没有外部依赖,只有 Angular!让我们将库添加到我们的应用程序中,并学习如何使用它:
-
使用
npm客户端从 npm 包注册库安装ngx-wig:npm install ngx-wig -
打开
app.module.ts文件,并将NgxWigModule添加到@NgModule装饰器的imports数组中:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **NgxWigModule** **}** **from****'****ngx-wig'****;** import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, **NgxWigModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }NgxWigModule是 ngx-wig 库的主要模块。 -
创建一个新的 Angular 组件,该组件将托管我们的 WYSIWYG 编辑器:
ng generate component editor -
打开新创建的组件的模板文件,
editor.component.html,并用以下 HTML 片段替换其内容:<ngx-wig placeholder="Enter your content"></ngx-wig>NgxWigModule提供了一组 Angular 服务和组件,我们可以在应用程序中使用。该模块的主要组件是<ngx-wig>组件,它显示实际的 WYSIWYG 编辑器。它公开了一组我们可以设置的输入属性,例如编辑器的占位符。 -
打开
app.component.html文件,并用<app-editor>组件替换其内容:<app-editor></app-editor> -
打开
styles.css文件,该文件包含 Angular 应用程序的全局样式,并添加以下样式以使编辑器可停靠并占满整个页面:html, body { margin: 0; width: 100%; height: 100%; } .ng-wig, .nw-editor-container, .nw-editor { display: flex !important; flex-direction: column; height: 100% !important; overflow: hidden; } -
打开 Angular 应用程序的主要 HTML 文件
index.html,并从<head>元素中移除<base>标签。浏览器使用<base>标签通过相对 URL 引用脚本和 CSS 文件。保留它将使我们的桌面应用程序失败,因为它将直接从本地文件系统中加载所有必要的资源。我们将在集成 Angular 与 Electron部分了解更多。
让我们看看我们已经取得了哪些成果。运行ng serve并导航到http://localhost:4200以预览应用程序:
图 5.2 – 应用程序输出
我们的应用程序包括以下内容:
-
一个带有按钮的工具栏,允许我们应用不同的样式到编辑器的内容
-
一个用作编辑器主要容器的文本区域,用于添加内容
我们现在已经使用 Angular 创建了一个具有完全可操作的 WYSIWYG 编辑器的 Web 应用程序。在下一节中,我们将学习如何使用 Electron 将其转换为桌面应用程序。
在工作空间中集成 Electron
Electron 框架是一个我们可以使用以下命令安装的 npm 包:
npm install -D electron
之前的命令将在 Angular CLI 工作空间中安装最新版本的electron npm 包。它还将在我们的项目package.json文件的devDependencies部分添加相应的条目。
Electron 被添加到package.json文件的devDependencies部分,因为它是我们应用程序的开发依赖项。它仅用于将我们的应用程序作为桌面应用程序准备和构建,而不是在运行时使用。
Electron 应用程序在 Node.js 运行时上运行,并使用 Chromium 浏览器进行渲染。一个 Node.js 应用程序至少有一个 JavaScript 文件,通常称为index.js或main.js,它是应用程序的主要入口点。由于我们使用 Angular 和 TypeScript 作为我们的开发堆栈,我们将首先创建一个单独的 TypeScript 文件,最终将其编译成 JavaScript:
-
在 Angular CLI 工作空间的
src文件夹内创建一个名为electron的文件夹。electron文件夹将包含任何与 Electron 相关的源代码。我们可以将我们的应用程序视为两个不同的平台。Web 平台是 Angular 应用程序,位于
src\app文件夹中。桌面平台是 Electron 应用程序,位于src\electron文件夹中。这种方法有许多优点,包括它强制我们在应用程序中分离关注点,并允许它们独立于彼此独立开发。从现在起,我们将它们称为 Angular 和 Electron 应用程序。 -
在
electron文件夹内创建一个main.ts文件,并包含以下内容:import { app, BrowserWindow } from 'electron'; function createWindow () { const mainWindow = new BrowserWindow({ width: 800, height: 600 }); mainWindow.loadFile('index.html'); } app.whenReady().then(() => { createWindow(); });在前面的代码中,我们首先从
electronnpm 包中导入BrowserWindow和app模块。BrowserWindow类用于为我们的应用程序创建桌面窗口。我们通过构造函数传递一个options对象来定义窗口的尺寸,该对象设置了窗口的width和height值。然后我们调用loadFile方法,将作为参数传递我们想要在窗口内加载的 HTML 文件。我们在
loadFile方法中传递的index.html文件是 Angular 应用程序的主 HTML 文件。它是使用文件协议加载的,这就是为什么我们在为 Angular 添加 WYSIWYG 编辑器库部分中移除了<base>标签。app对象是我们桌面应用程序的全局对象,就像网页上的window对象一样。它暴露了一个whenReadyPromise,当它解析时,允许我们运行应用程序的任何初始化逻辑,包括创建窗口。 -
在
electron文件夹内创建一个tsconfig.json文件,并添加以下内容:{ "extends": "../../tsconfig.json", "compilerOptions": { "importHelpers": false }, "include": [ "**/*.ts" ] }main.ts文件必须编译成 JavaScript,因为浏览器不理解 TypeScript。编译过程称为转译,需要一个 TypeScript 配置文件。配置文件包含驱动 TypeScript 转译器的选项,转译器负责转译过程。上述 TypeScript 配置文件使用
include属性定义了 Electron 源代码文件的路径,并将importHelpers属性设置为false。如果我们启用
importHelpers标志,它将包括来自tslib库的帮助程序,从而导致包的大小更大。 -
运行以下命令以安装Webpack CLI:
npm install -D webpack-cliWebpack CLI 从命令行调用流行的模块打包器webpack。我们将使用 webpack 来构建和打包我们的 Electron 应用程序。
-
使用以下命令安装
ts-loadernpm 包:npm install -D ts-loader
ts-loader库是一个 webpack 插件,可以加载 TypeScript 文件。
我们已经创建了将我们的 Angular 应用程序转换为桌面应用程序所需的全部组件,使用 Electron。我们只需要将它们组合起来以构建和运行我们的桌面应用程序。协调 Electron 应用程序的主要组件是我们需要在 Angular CLI 工作区的根目录中创建的 webpack 配置文件:
webpack.config.js
const path = require('path');
const src = path.join(process.cwd(), 'src', 'electron');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: path.join(src, 'main.ts'),
output: {
path: path.join(process.cwd(), 'dist', 'my-editor'),
filename: 'shell.js'
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: path.join(src, 'tsconfig.json')
}
}
]
},
target: 'electron-main'
};
前面的文件使用以下选项配置了我们的应用程序中的 webpack:
-
mode:指示我们当前正在开发环境中运行。 -
devtool:启用源映射文件生成,用于调试目的。 -
entry:指示 Electron 应用程序的主入口点,即main.ts文件。 -
输出: 定义了从 webpack 生成的 Electron 包的路径和文件名。path属性指向 Angular CLI 创建 Angular 应用包所使用的相同文件夹。filename属性设置为shell.js,因为 webpack 默认生成的文件名为main.js,这将会与 Angular 应用生成的main.js文件冲突。 -
module: 指示 webpack 加载ts-loader插件来处理 TypeScript 文件。 -
target: 表示我们目前正在 Electron 的主进程中运行。
Webpack 模块打包器现在包含了构建和打包 Electron 应用所需的所有信息。另一方面,Angular CLI 负责构建 Angular 应用。让我们看看我们如何将它们结合起来并运行我们的桌面应用:
-
运行以下命令来安装
concurrentlynpm 包:npm install -D concurrentlyconcurrently库使我们能够同时执行多个进程。在我们的案例中,它将允许我们并行运行 Angular 和 Electron 应用。
-
打开
package.json文件,并在scripts属性中添加一个新条目:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", **"start:desktop"****:****"concurrently \"ng build --delete-output-path=false --watch\" \"webpack --watch\""** **}**start:desktop脚本使用 Angular CLI 的ng build命令构建 Angular 应用,并使用webpack命令构建 Electron 应用。两个应用都使用--watch选项以监视模式运行,所以每次我们更改代码,应用都会重新构建以反映更改。当我们修改 Angular 应用时,Angular CLI 默认会删除
dist文件夹。我们可以使用--delete-output-path=false选项来防止这种行为,因为 Electron 应用也是在同一个文件夹中构建的。我们没有将 webpack 配置文件传递给
webpack命令,因为它默认假设文件名为webpack.config.js。 -
点击 Visual Studio Code 侧边栏中存在的运行菜单:
图 5.3 – 运行菜单
- 在出现的运行和调试面板中,从下拉菜单中选择**添加配置…**选项:
图 5.4 – 运行和调试面板
-
Visual Studio Code 将打开一个下拉菜单,允许我们选择运行应用的环境。选择**{} Node.js: Electron Main**配置。
-
在打开的
launch.json文件中,将program属性的值设置为${workspaceFolder}/dist/my-editor/shell.js。program属性指向 Electron 包文件的绝对路径。
现在我们已经准备好运行我们的桌面应用并预览它。运行以下命令来构建应用:
npm run start:desktop
之前的命令将首先构建 Electron 应用,然后是 Angular 应用。等待 Angular 构建完成,从运行和调试面板的下拉菜单中选择Electron Main选项,然后点击播放按钮来预览应用:
图 5.5 – 应用程序窗口
在前面的屏幕截图中,我们可以看到我们的带有 WYSIWYG 编辑器的 Angular 应用程序托管在原生桌面窗口中。它包含以下我们在桌面应用程序中通常会发现的特点:
-
带有图标的标题
-
主菜单
-
最小化、最大化和关闭按钮
Angular 应用程序在 Chromium 浏览器内部渲染。为了验证这一点,点击 视图 菜单项并选择 切换开发者工具 选项。
干得好!你已经成功创建了自己的桌面 WYSIWYG 编辑器。在下一节中,我们将学习如何进行 Angular 和 Electron 之间的交互。
Angular 和 Electron 之间的通信
根据项目的规格,WYSIWYG 编辑器的内容需要保存在本地文件系统中。此外,内容将在应用程序启动时从文件系统加载。
Angular 应用程序使用渲染进程处理 WYSIWYG 编辑器与数据之间的任何交互,而 Electron 应用程序使用主进程管理文件系统。因此,我们需要建立一个 IPC 机制,以便在两个 Electron 进程之间进行通信,如下所示:
-
配置 Angular CLI 工作区
-
与编辑器交互
-
与文件系统交互
让我们先设置 Angular CLI 项目以支持所需的通信机制。
配置 Angular CLI 工作区
我们需要修改几个文件来配置我们应用程序的工作区:
-
打开位于
src\electron文件夹中的main.ts文件,并在BrowserWindow构造函数中相应地设置webPreferences属性:function createWindow () { const mainWindow = new BrowserWindow({ width: 800, height: 600, **webPreferences****: {** **nodeIntegration****:** **true****,** **contextIsolation****:** **false** **}** }); mainWindow.loadFile('index.html'); }之前的标志将在渲染进程中启用 Node.js 并公开
ipcRenderer接口,这是我们与主进程通信所需的。 -
运行以下命令来安装
ngx-electronyzernpm 包:npm install ngx-electronyzer
ngx-electronyzer 库允许我们将 Electron API 集成到 Angular 应用程序中。
Angular 和 Electron 应用程序现在已准备好通过 IPC 机制相互交互。让我们首先在 Angular 应用程序中实现必要的逻辑。
与编辑器交互
Angular 应用程序负责管理 WYSIWYG 编辑器。编辑器的内容通过 Electron 的渲染进程与文件系统保持同步。让我们了解如何使用渲染进程:
-
使用以下 Angular CLI 命令创建一个新的 Angular 服务:
ng generate service editor -
打开
editor.service.ts文件,并从ngx-electronyzernpm 包中注入ElectronService类:import { Injectable } from '@angular/core'; **import** **{** **ElectronService** **}** **from****'ngx-electronyzer'****;** @Injectable({ providedIn: 'root' }) export class EditorService { constructor(**private** **electronService: ElectronService**) { } }ElectronService类公开了部分 Electron API,包括我们目前感兴趣的ipcRenderer接口。 -
创建一个方法,该方法将被调用来从文件系统获取编辑器的内容:
getContent(): Promise<string> { return this.electronService.ipcRenderer.invoke('getContent'); }我们使用
ipcRenderer属性的invoke方法,将通信通道的名称作为参数传递。getContent方法的返回值是一个Promise对象,其类型为string,因为编辑器的内容是原始文本数据。invoke方法通过getContent通道与主进程建立连接。在 与文件系统交互 部分,我们将看到如何设置主进程以响应该通道中的invoke方法调用。 -
创建一个方法,当需要将编辑器的内容保存到文件系统时将被调用:
setContent(content: string) { this.electronService.ipcRenderer.invoke('setContent', content); }setContent方法再次调用ipcRenderer对象的invoke方法,但使用不同的通道名称。它还使用invoke方法的第二个参数将数据传递给主进程。在这种情况下,content参数将包含编辑器的内容。我们将在 与文件系统交互 部分看到如何配置主进程以处理数据。 -
打开
editor.component.ts文件并创建一个myContent属性来保存编辑器数据。同时,在EditorComponent类的constructor中注入EditorService,并添加来自@angular/corenpm 包的OnInit接口:import { Component, **OnInit** } from '@angular/core'; **import** **{** **EditorService** **}** **from****'../editor.service'****;** @Component({ selector: 'app-editor', templateUrl: './editor.component.html', styleUrls: ['./editor.component.css'] }) export class EditorComponent **implements****OnInit** { **myContent =** **''****;** **constructor****(****private** **editorService: EditorService****) {}** } -
创建一个方法,该方法调用
editorService变量的getContent方法,并在ngOnInit方法内部执行它:ngOnInit(): void { this.getContent(); } private async getContent() { this.myContent = await this.editorService.getContent(); }我们使用
async/await语法,它允许我们在基于 Promise 的方法调用中同步执行我们的代码。 -
创建一个方法,该方法调用
editorService变量的setContent方法:saveContent(content: string) { this.editorService.setContent(content); } -
让我们将我们创建的方法与组件的模板绑定起来。打开
editor.component.html文件并添加以下绑定:<ngx-wig placeholder="Enter your content" **[****ngModel****]=****"myContent"** **(****contentChange****)=****"saveContent($event)"**></ngx-wig>我们使用
ngModel指令将编辑器的模型绑定到myContent组件属性,该属性将用于最初显示内容。我们还使用contentChange事件绑定,以便在编辑器内容更改时(即用户输入时)保存编辑器的内容。 -
ngModel指令是@angular/formsnpm 包的一部分。将FormsModule导入到app.module.ts文件中以便使用:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; **import** **{** **FormsModule** **}** **from****'@angular/forms'****;** import { NgxWigModule } from 'ngx-wig'; import { AppComponent } from './app.component'; import { EditorComponent } from './editor/editor.component'; @NgModule({ declarations: [ AppComponent, EditorComponent ], imports: [ BrowserModule, NgxWigModule, **FormsModule** ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
我们已经实现了 Angular 应用程序与主进程通信的所有逻辑。现在是时候实现通信机制的另一端,即 Electron 应用程序及其主进程了。
与文件系统交互
主进程通过内置在 Electron 框架中的 fs 库与文件系统进行交互。让我们看看如何使用它:
-
打开位于
src\electron文件夹中的main.ts文件并导入以下组件:import { app, BrowserWindow, **ipcMain** } from 'electron'; **import** ***** **as** **fs** **from****'fs'****;** **import** ***** **as** **path** **from****'path'****;**fs库负责与文件系统交互。path库提供了用于处理文件和文件夹路径的实用工具。ipcMain对象允许我们与 Electron 的主进程进行交互。 -
创建一个变量来保存包含编辑器内容的文件路径:
const contentFile = path.join(app.getPath('userData'), 'content.html');保存编辑器内容的文件是位于预留的
userData文件夹中的content.html文件。userData文件夹是一个特殊用途的系统文件夹的别名,每个操作系统不同,用于存储特定于应用程序的文件,如配置。您可以在www.electronjs.org/docs/api/app#appgetpathname找到有关userData文件夹和其他系统文件夹的更多详细信息。app对象的getPath方法是跨平台的,用于获取特殊文件夹的路径,例如用户的家目录或应用程序数据。 -
调用
ipcMain对象的handle方法以在getContent通道中开始监听请求:ipcMain.handle('getContent', () => { if (fs.existsSync(contentFile)) { const result = fs.readFileSync(contentFile); return result.toString(); } return ''; });当主进程接收到此通道的请求时,它使用
fs库的existsSync方法检查包含编辑器内容的文件是否已存在。如果存在,它将使用readFileSync方法读取它,并将内容返回给渲染进程。 -
再次调用
handle方法,但这次是为setContent通道:ipcMain.handle('setContent', ({}, content: string) => { fs.writeFileSync(contentFile, content); }); writeFileSync method of the fs library to write the value of the content property in the file.
现在我们已经连接了 Angular 和 Electron 应用程序,是时候预览我们的 WYSIWYG 桌面应用程序了:
-
执行
start:desktopnpm 脚本,并按 F5 运行应用程序。 -
使用编辑器和其工具栏输入一些内容,例如以下内容:
图 5.6 – 编辑器内容
- 关闭应用程序窗口并重新运行应用程序。如果一切正常,您应该看到编辑器中输入的内容。
恭喜!您已经通过向其添加持久性功能来丰富了您的 WYSIWYG 编辑器。在下一节中,我们将采取创建桌面应用程序的最后一步,并学习如何打包和分发它。
打包桌面应用程序
网络应用程序通常被打包并部署到托管服务器。另一方面,桌面应用程序被打包并打包成一个可轻松分发的单个可执行文件。打包我们的 WYSIWYG 应用程序需要以下步骤:
-
配置生产模式下的 webpack
-
使用 Electron 打包器
我们将在下一节中更详细地介绍它们。
配置生产环境下的 webpack
我们已经为开发环境创建了一个 webpack 配置文件。我们现在需要为生产环境创建一个新的配置文件。这两个配置文件将共享一些功能,所以让我们先创建一个通用的配置文件:
-
在 Angular CLI 工作区的根目录中创建一个
webpack.dev.config.js文件,内容如下:const path = require('path'); const baseConfig = require('./webpack.config'); module.exports = { ...baseConfig, mode: 'development', devtool: 'source-map', output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'shell.js' } }; -
从
webpack.config.js文件中移除mode、devtool和output属性。 -
打开
package.json文件,并在start:desktop脚本中传递新的 webpack 开发配置文件:"start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack **--config webpack.dev.config.js** --watch\"" -
在 Angular CLI 工作区的根目录中创建一个
webpack.prod.config.js文件,内容如下:const path = require('path'); const baseConfig = require('./webpack.config'); module.exports = { ...baseConfig, output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'main.js' } };与开发环境的 webpack 配置文件相比,主要区别在于我们将
output包的filename改为了main.js。Angular CLI 在生产环境中向 Angular 应用的main.js文件中添加一个哈希数字,因此不会发生冲突。 -
在
package.json文件的scripts属性中添加一个新的条目,以在生产模式下构建我们的应用程序:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack --config webpack.dev.config.js --watch\"", **"build:electron"****:** **"ng build && webpack --config webpack.prod.config.js"** }build:electron脚本同时以生产模式构建 Angular 和 Electron 应用程序。
我们已经完成了打包桌面应用程序所需的所有配置。在下一节中,我们将学习如何将其转换为针对每个操作系统的单个包。
使用 Electron 打包器
Electron 框架拥有开源社区创建和维护的各种工具。其中之一是 electron-packager 库,我们可以使用它将我们的桌面应用程序打包成每个操作系统(Windows、Linux 和 macOS)的单个可执行文件。让我们看看如何将其集成到我们的开发工作流程中:
-
运行以下命令将
electron-packager作为我们的项目的开发依赖项安装:npm install -D electron-packager -
在
package.json文件的scripts属性中添加一个新的条目以打包我们的应用程序:"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "start:desktop": "concurrently \"ng build --delete-output-path=false --watch\" \"webpack --config webpack.dev.config.js --watch\"", "build:electron": "ng build && webpack --config webpack.prod.config.js", **"package"****:****"electron-packager dist/my-editor --out=dist --asar"** }在前面的脚本中,
electron-packager将读取dist/my-editor文件夹中的所有文件,将它们打包,并将最终的包输出到dist文件夹。--asar选项指示打包器将所有文件存档为 ASAR 格式,类似于 ZIP 或 TAR 文件。 -
在
src\electron文件夹中创建一个package.json文件,并添加以下内容:{ "name": "my-editor", "main": "main.js" }electron-packager库要求输出文件夹中存在一个package.json文件,该文件指向 Electron 应用程序的主入口文件。 -
打开
webpack.prod.config.js文件,并在plugins属性中添加CopyWebpackPlugin:const path = require('path'); const baseConfig = require('./webpack.config'); **const****CopyWebpackPlugin** **=** **require****(****'copy-webpack-plugin'****);** module.exports = { ...baseConfig, output: { path: path.join(process.cwd(), 'dist', 'my-editor'), filename: 'main.js' }, **plugins****: [** **new****CopyWebpackPlugin****({** **patterns****: [** **{** **context****: path.****join****(process.****cwd****(),** **'src'****,** **'electron'****),** **from****:** **'****package.json'** **}** **]** **})** **]** };我们使用
CopyWebpackPlugin在生产模式下构建应用程序时将package.json文件从src\electron文件夹复制到dist\my-editor文件夹。 -
运行以下命令以在生产模式下构建应用程序:
npm run build:electron -
现在运行以下
npm命令来打包它:npm run package前面的命令将为当前运行的操作系统打包应用程序,这是
electron-packager库的默认行为。您可以通过传递额外的选项来更改此行为,这些选项可以在库的 GitHub 仓库中找到,列在 进一步阅读 部分。 -
导航到 Angular CLI 工作区的
dist文件夹。您将找到一个名为my-editor-{OS}的文件夹,其中{OS}是您的当前操作系统及其架构。例如,在 Windows 上,它将是my-editor-win32-x64。打开文件夹,您将得到以下文件:
图 5.7 – 应用程序包(Windows)
在前面的屏幕截图中,my-editor.exe 文件是我们桌面应用程序的可执行文件。我们的应用程序代码不包含在这个文件中,而是在 resources 文件夹中的 app.asar 文件中。我们的应用程序代码不包含在这个文件中,而是在 resources 文件夹中的 app.asar 文件中。
运行可执行文件,桌面应用程序应该可以正常打开。您可以整个文件夹上传到服务器,或者通过其他任何方式分发。现在,您的 WYSIWYG 编辑器可以触及更多用户,例如那些大部分时间都在离线状态的用户。太棒了!
摘要
在本章中,我们使用 Angular 和 Electron 构建了一个桌面 WYSIWYG 编辑器。最初,我们创建了一个 Angular 应用程序,并添加了 ngx-wig,一个流行的 Angular WYSIWYG 库。然后,我们学习了如何构建 Electron 应用程序,并实现了 Angular 和 Electron 应用程序之间的数据交换通信机制。最后,我们学习了如何打包我们的应用程序以进行打包,并准备好分发。
在下一章中,我们将学习如何使用 Angular 和 Ionic 构建一个移动照片地理标记应用程序。
实践问题
让我们看看几个实践问题:
-
哪个类负责在 Electron 中创建桌面窗口?
-
我们如何在 Electron 中的主进程和渲染进程之间进行通信?
-
哪些标志可以启用在渲染进程中使用 Node.js?
-
我们如何在 Angular 应用程序中加载 Electron?
-
我们在 Angular 应用程序中与 Electron 交互时使用哪个接口?
-
我们如何从 Angular 应用程序向主 Electron 进程传递数据?
-
我们在 Electron 中使用哪个包进行文件系统操作?
-
我们使用哪个库来打包 Electron 应用程序?
进一步阅读
这里有一些链接,可以帮助我们巩固本章所学的内容:
-
Electron:
www.electronjs.org -
Electron 快速入门:
www.electronjs.org/docs/tutorial/quick-start -
ngx-wig:www.npmjs.com/package/ngx-wig -
Webpack 配置:
webpack.js.org/configuration -
ts-loader:webpack.js.org/guides/typescript -
ngx-electronyzer:www.npmjs.com/package/ngx-electronyzer -
文件系统 API:
nodejs.org/api/fs.html -
electron-packager:www.npmjs.com/package/electron-packager -
concurrently:www.npmjs.com/package/concurrently
第六章:使用 Capacitor 和 3D 地图构建移动照片地理标记应用程序
Angular 是一个跨平台的 JavaScript 框架,可用于构建不同平台(如 Web、桌面和移动)的应用程序。此外,它允许开发者使用相同的代码库并将相同的 Web 技术应用于每个平台,从而享受相同的使用体验和性能。在本章中,我们将探讨如何使用 Angular 构建移动应用程序。
Ionic是一个流行的 UI 工具包,允许我们使用如 Angular 等 Web 技术构建移动应用程序。Capacitor库通过使它们能够在 Android 和 iOS 设备上本地运行,极大地增强了 Ionic 应用程序。在本章中,我们将使用这两种技术构建一个移动应用程序,用于拍摄带有地理标记的照片并在 3D 地图上显示它们。
我们将详细介绍以下主题:
-
使用 Ionic 创建移动应用程序
-
使用 Capacitor 拍照
-
在Firebase中存储数据
-
使用CesiumJS预览照片
必要的背景理论和上下文
电容器是一个原生移动运行时,使我们能够使用包括 Angular 在内的 Web 技术构建 Android 和 iOS 应用程序。它为 Web 应用程序提供了一个抽象 API 层,以便与移动操作系统的原生资源进行交互。它不包括 UI 层或任何其他与用户界面交互的方式。
Ionic 是一个包含我们可以用于使用 Capacitor 构建的应用程序中的 UI 组件的移动框架。Ionic 的主要优势是我们可以在所有原生移动平台上维护单个代码库。也就是说,我们只需编写一次代码,它就可以在任何地方工作。Ionic 支持所有流行的 JavaScript 框架,包括 Angular。
Firebase 是由 Google 提供的**后端即服务(BaaS)**平台,其中包含用于构建应用程序的工具和服务。Cloud Firestore是 Firebase 提供的一种数据库解决方案,它具有灵活和可扩展的 NoSQL 文档导向数据库,可用于 Web 和移动应用程序。Firebase Storage是一种服务,允许我们与存储机制进行交互并上传或下载文件。
CesiumJS 是一个用于在浏览器中创建交互式 3D 地图的 JavaScript 库。它是一个开源、跨平台的库,使用 WebGL,并允许我们在多个平台上共享地理空间数据。它由Cesium提供支持,这是一个用于构建高质量和性能优异的 3D 地理空间应用的平台。
项目概述
在这个项目中,我们将构建一个可以根据当前位置拍照并在地图上预览照片的移动应用程序。最初,我们将学习如何使用 Angular 和 Ionic 创建移动应用程序。然后,我们将使用 Capacitor 通过移动设备的相机拍照,并通过 GPS 标记当前位置。我们将把这些照片及其位置数据上传到 Firebase。最后,我们将使用 CesiumJS 在 3D 球上加载位置数据,并预览照片。以下图表展示了项目的架构概述:
图 6.1 – 项目架构
在本章中,你将学习如何使用 Angular 和 Ionic 构建移动应用程序。为了跟进项目并预览你的应用程序,你必须遵循你的开发环境(Android 或 iOS)的入门指南,你可以在 进一步阅读 部分找到。
构建时间:2 小时
入门
完成此项目,你需要以下软件和工具:
-
对于 Android 开发:Android Studio 以及最新的 Android SDK。
-
对于 iOS 开发:Xcode 以及 iOS SDK 和 Xcode 命令行工具。
-
一个物理移动设备。
-
Angular CLI:Angular 的命令行界面,你可以在
angular.io/cli找到。 -
GitHub 资源:本章相关代码可在
github.com/PacktPublishing/Angular-Projects-Third-Edition的Chapter06文件夹中找到。
使用 Ionic 创建移动应用程序
建立我们的应用程序的第一步是使用 Ionic 工具包创建一个新的移动应用程序。我们将从以下任务开始构建我们的应用程序:
-
应用程序脚手架
-
构建主菜单
Ionic 创建从零开始的新移动应用程序的过程非常直接,无需输入任何代码。
应用程序脚手架
创建新的 Ionic 应用程序,请完成以下步骤:
-
使用以下命令安装我们需要的 Ionic 工具:
npm install -g @ionic/cli native-run cordova-resIonic CLI 用于构建和运行 Ionic 移动应用程序。
native-run库用于在移动设备和模拟器上运行原生库。cordova-res库为我们生成原生移动设备的应用程序图标和启动画面。 -
运行以下命令以创建一个新的 Angular 应用程序,该应用程序使用 Ionic 的
sidemenu起始模板,并添加了 Capacitor:ionic start phototag sidemenu --type=angular --capacitor -
之前的命令将询问你是否想使用 Angular 模块或独立组件。选择
Standalone并按 Enter。
Ionic 将为我们创建一个包含一些现成数据的示例应用程序。在下一节中,我们将学习如何根据我们的需求对其进行修改。
构建主菜单
我们将根据我们的规格开始构建应用程序的主菜单:
-
在 VSCode 中加载我们在上一节中构建的 Ionic 项目,并打开应用程序的主 HTML 文件
index.html。 -
在
<title>标签中添加您应用程序的名称:<title>**Phototag** App</title> -
打开主组件的模板文件
app.component.html,并删除第二个<ion-list>元素。<ion-list>元素用于在列表视图中显示项目。 -
在
<ion-list-header>元素中添加您应用程序的名称,并相应地更改<ion-note>元素的文本:<ion-list-header>**Phototag**</ion-list-header> <ion-note>**Capture geotagged photos**</ion-note><ion-list-header>元素是列表的标题。<ion-note>元素是一个用于提供额外信息的文本元素,例如列表的副标题。 -
打开主组件的 TypeScript 文件
app.component.ts,并按如下方式修改AppComponent类:export class AppComponent { public appPages = [ { title: 'Take a photo', url: '/capture', icon: 'camera' }, { title: 'View gallery', url: '/view', icon: 'globe' } ]; constructor() {} }appPages属性包含我们应用程序的所有页面。每个页面都有一个title,一个可访问的url,以及一个icon。我们的应用程序将包含两个页面,一个用于使用相机拍照,另一个用于在地图上显示它们。 -
运行 Ionic CLI 的
serve命令以启动应用程序:ionic serve上述命令将构建您的应用程序并在默认浏览器中打开
http://localhost:8100。您应该在应用程序的侧边菜单中看到以下输出:
图 6.2 – 主菜单
假设您调整浏览器窗口大小以获得更真实的移动设备视图或使用模拟器,例如 Google Chrome 开发者工具中的设备工具栏。在这种情况下,您必须点击应用程序菜单按钮才能看到前面的图像。
我们已经学习了如何使用 Ionic CLI 创建新的 Ionic 应用程序并根据我们的需求进行修改。
如果我们尝试点击菜单项,我们会注意到没有任何反应,因为我们还没有为每种情况创建必要的页面。在下一节中,我们将学习如何通过构建第一页的功能来完成此任务。
使用 Capacitor 拍照
我们应用程序的第一页将允许用户使用相机拍照。我们将使用 Capacitor 运行时来获取对相机原生资源的访问权限。为了实现页面,我们需要采取以下行动:
-
创建用户界面。
-
与电容器交互。
让我们开始构建页面的用户界面。
创建用户界面
我们应用程序中的每个页面都是一个不同的 Angular 组件。要在 Ionic 中创建 Angular 组件,我们可以使用 Ionic CLI 的 generate 命令:
ionic generate page capture
之前的命令将执行以下操作:
-
创建一个名为
capture的 Angular 组件。 -
创建相关路由文件。
让我们现在开始构建新页面的逻辑:
-
首先,当用户打开应用程序时,使我们的页面成为默认页面。打开
app.routes.ts文件,并将routes属性的第一个条目更改为:{ path: '', redirectTo: '**capture**', pathMatch: 'full', }空路径称为 默认 路由路径,当我们的应用程序启动时被激活。
redirectTo属性告诉 Angular 重定向到capture路径,这将加载我们创建的页面。你也可以删除
folder/:id路径,因为它不再需要,并且从应用程序中删除整个src\app\folder目录,这是 Ionic 模板布局的一部分。 -
打开
capture.page.html文件并按照以下方式替换第一个<ion-toolbar>元素的全部内容:<ion-header [translucent]="true"> <ion-toolbar> **<****ion-buttons****slot****=****"start"****>** **<****ion-menu-button****color****=****"primary"****></****ion-menu-button****>** **</****ion-buttons****>** <ion-title>**Take a photo**</ion-title> </ion-toolbar> </ion-header><ion-toolbar>元素是<ion-header>元素的一部分,它是页面的顶部导航栏。它包含一个<ion-menu-button>元素,用于切换应用程序的主菜单,以及一个<ion-title>元素,描述页面的标题。 -
按照以下方式修改第二个
<ion-toolbar>元素的标题:<ion-title size="large">**Take a photo**</ion-title>当页面展开时,将显示第二个
<ion-header>元素,主菜单将显示在屏幕上。 -
在第二个标题之后立即添加以下 HTML 代码:
<div id="container"> <strong class="capitalize">Take a nice photo with your camera</strong> <ion-fab vertical="center" horizontal="center" slot="fixed"> <ion-fab-button> <ion-icon name="camera"></ion-icon> </ion-fab-button> </ion-fab> </div>它包含一个
<ion-fab-button>元素,当点击时,将打开设备的相机来拍照。 -
最后,让我们给我们的页面添加一些酷炫的样式。打开
capture.page.scss文件并输入以下 CSS 样式:#container { text-align: center; position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); } #container strong { font-size: 20px; line-height: 26px; } #container ion-fab { margin-top: 60px; }
让我们使用 ionic serve 运行应用程序,以快速预览我们迄今为止所构建的内容:
图 6.3 – 捕获页面
页面上的相机按钮需要打开相机来拍照。在以下部分,我们将学习如何使用 Capacitor 与相机交互。
与 Capacitor 交互
在我们的应用程序中拍照涉及使用 Capacitor 库中的两个 API。Camera API 将打开相机来拍照,而 Geolocation API 将从 GPS 读取当前位置。让我们看看我们如何在应用程序中使用这两个 API:
-
执行以下
npm命令来安装两个 API:npm install @capacitor/camera @capacitor/geolocation -
使用以下 Ionic CLI 命令创建 Angular 服务:
ionic generate service photo -
打开
photo.service.ts文件并添加以下import语句:import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Geolocation } from '@capacitor/geolocation'; -
在
PhotoService类中创建一个方法来从 GPS 设备读取当前位置:private async getLocation() { const location = await Geolocation.getCurrentPosition(); return location.coords; }Geolocation对象的getCurrentPosition方法包含一个coords属性,其中包含各种基于位置的数据,如纬度和经度。 -
创建另一个方法,该方法调用
getLocation方法并打开设备的相机来拍照:async takePhoto() { await this.getLocation(); await Camera.getPhoto({ resultType: CameraResultType.DataUrl, source: CameraSource.Camera, quality: 100 }); }我们使用
Camera对象的getPhoto方法并传递一个配置对象来定义每张照片的属性。resultType属性表示照片将以 data URL 格式保存,以便稍后轻松地将其保存到 Firebase。source属性表示我们将使用相机设备来获取照片,而quality属性定义了实际照片的质量。 -
打开
capture.page.ts文件并在CapturePage类的constructor中注入PhotoService:import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; **import** **{** **PhotoService** **}** **from****'../photo.service'****;** @Component({ selector: 'app-capture', templateUrl: './capture.page.html', styleUrls: ['./capture.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class CapturePage implements OnInit { constructor(**private** **photoService: PhotoService**) { } ngOnInit() { } } -
创建一个组件方法,该方法将调用
photoService变量的takePhoto方法:openCamera() { this.photoService.takePhoto(); } -
打开
capture.page.html文件,并将<ion-fab-button>元素的click事件绑定到openCamera组件方法:<ion-fab-button **(****click****)=****"openCamera()"**> <ion-icon name="camera"></ion-icon> </ion-fab-button>
我们现在已经添加了所有必要的组件来使用设备的相机拍照。让我们尝试在真实设备上运行应用程序以测试与相机的交互:
-
首先,我们需要使用以下 Ionic CLI 命令构建我们的应用程序:
ionic build
上述命令将在项目根目录中创建一个www文件夹,其中包含你的应用程序包。
-
运行以下命令以在所选平台的发展环境中打开应用程序:
ionic cap open <os>在上一个命令中,
<os>可以是android或ios。执行后,它将根据你针对的平台打开相应的本地移动项目,Android Studio 或 Xcode,具体取决于目标平台。然后必须使用 IDE 来运行本地应用程序。每次你想重新构建应用程序时,都必须运行
ionic cap copy命令,以将应用程序包从www文件夹复制到本地移动项目。 -
点击相机按钮。应用程序可能会要求你允许使用 GPS 和相机。或者,在继续之前,你可能需要在设备上启用位置设置。
你可能需要在开发环境的本地移动项目中添加额外的权限。检查 Capacitor 网站上 API 的相关文档。
我们应用程序的第一页现在有一个简洁的界面,允许用户与相机交互。我们还创建了一个 Angular 服务,确保与 Capacitor 的无缝交互以获取基于位置的数据并拍照。在下一节中,我们将看到如何使用 Firebase 将它们保存在云端。
在 Firebase 中存储数据
应用程序将能够将照片及其位置存储在 Firebase 中。我们将使用存储服务上传我们的照片,并使用 Cloud Firestore 数据库来保存它们的位置。我们将在以下任务中进一步扩展我们的应用程序:
-
创建 Firebase 项目
-
集成AngularFire库
首先,我们必须为我们的应用程序设置一个新的 Firebase 项目。
创建 Firebase 项目
我们可以使用位于console.firebase.google.com的Firebase 控制台设置和配置 Firebase 项目:
- 点击添加项目按钮以创建一个新的 Firebase 项目:
图 6.4 – 创建新的 Firebase 项目
-
为你的项目输入一个名称,然后点击继续按钮:
图 6.5 – 输入项目名称
Firebase 为你的项目生成一个唯一的标识符,它位于项目名称下方,并在各种 Firebase 服务中使用。
-
禁用此项目的Google Analytics并点击创建项目按钮:
图 6.6 – 禁用 Google Analytics
-
等待创建新项目并点击继续按钮。您将被重定向到您的新项目仪表板,其中包含一系列选项:
图 6.7 – 选择应用程序类型
点击带有代码图标的第三个选项,将 Firebase 添加到 Web 应用程序中。
-
在应用昵称字段中输入您的应用程序名称,然后点击注册应用按钮:
图 6.8 – 应用程序注册
-
Firebase 将生成一个配置,我们将在后面的移动应用程序中使用:
const firebaseConfig = { apiKey: "<Your API key>", authDomain: "<Your project auth domain>", projectId: "<Your project ID>", storageBucket: "<Your storage bucket>", messagingSenderId: "<Your messaging sender ID>", appId: "<Your application ID>" };复制
firebaseConfig对象并点击继续到控制台按钮。Firebase 配置也可以在
https://console.firebase.google.com/project/<project-id>/settings/general处稍后访问,其中project-id是您的 Firebase 项目 ID。 -
在仪表板控制台中,选择Cloud Firestore选项以在您的应用程序中启用 Cloud Firestore。
-
点击创建数据库按钮以创建一个新的 Cloud Firestore 数据库:
图 6.9 – 创建数据库
-
选择数据库的操作模式。为了开发目的,选择以测试模式启动并点击下一步按钮:
图 6.10 – 选择操作模式
选择模式相当于为您的数据库设置规则。测试模式允许快速设置,并保持您的数据公开 30 天。当您准备好将应用程序移入生产时,您可以相应地修改数据库规则以使您的数据私有。
-
根据您的区域设置选择数据库的位置,然后点击启用按钮。
恭喜!您已创建了一个新的 Cloud Firestore 数据库。在下一节中,我们将学习如何使用新的数据库通过我们的移动应用程序保存数据。
集成 AngularFire 库
AngularFire 库是一个 Angular 库,我们可以在 Angular 应用程序中使用它来与 Firebase 家族产品(如 Cloud Firestore 和存储服务)交互。要在我们的应用程序中安装它:
-
在终端窗口中运行以下命令以安装Firebase 工具:
npm install -g firebase-tools -
在相同的终端窗口中运行以下命令以使用 Firebase CLI 进行身份验证:
firebase login -
最后,运行以下 Angular CLI 命令以在您的 Angular CLI 项目中安装
@angular/firenpm 包:ng add @angular/fire上述命令将找到库的最新版本,并提示我们安装它。
-
首先,它会询问我们想要启用 Firebase 的哪些功能:
? What features would you like to setup?确保只选择 Firestore 选项并按 Enter。
-
然后,它将询问我们想要使用哪个 Firebase 账户:
? Which Firebase account would you like to use?确保选择你之前使用的账户,并按 Enter。
-
在下一个问题中,我们将选择我们将要使用 Firestore 的项目:
? Please select a project:选择我们之前创建的
phototag项目并按 Enter。 -
最后,我们必须选择已启用 Firestore 的应用程序:
? Please select an app:选择我们之前创建的
phototag应用程序并按 Enter。前面的命令可能会抛出一个错误,表明
app.module.ts文件不存在。请忽略它并继续下一步。 -
打开
main.ts文件并添加以下import语句:import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; import { getStorage, provideStorage } from '@angular/fire/storage'; -
最后,修改
bootstrapApplication方法中的providers数组如下:bootstrapApplication(AppComponent, { providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, importProvidersFrom(IonicModule.forRoot({})), provideRouter(routes), **importProvidersFrom(****provideFirebaseApp****(****() =>****initializeApp****(<firebaseConfig>))),** **importProvidersFrom(****provideFirestore****(****() =>****getFirestore****())),** **importProvidersFrom(****provideStorage****(****() =>****getStorage****()))** ] });
将 <firebaseConfig> 替换为你在上一节中复制的 Firebase 配置对象。
现在我们来看看我们如何在应用程序中使用 AngularFire 库:
-
打开
photo.service.ts文件并添加以下import语句:import { Firestore, collection, addDoc } from '@angular/fire/firestore'; import { Storage, ref, uploadString, getDownloadURL } from '@angular/fire/storage';Firestore服务包含我们与 Cloud Firestore 数据库交互所需的所有必要方法。Storage服务包含将文件上传到存储服务的方法。 -
将这两个服务注入到
PhotoService类的constructor中:constructor(**private** **firestore: Firestore,** **private** **storage: Storage**) {} -
创建以下方法以在 Firebase 中保存照片:
private async savePhoto(dataUrl: string, latitude: number, longitude: number) { const name = new Date().getUTCMilliseconds().toString(); const storageRef = ref(this.storage, name); await uploadString(storageRef, dataUrl, 'data_url'); const photoUrl = await getDownloadURL(storageRef); const photoCollection = collection(this.firestore, 'photos'); await addDoc(photoCollection, { url: photoUrl, lat: latitude, lng: longitude }) }首先,我们为我们的照片创建一个随机的
name,并使用uploadString方法将其上传到 Firebase 存储中。一旦上传完成,我们使用getDownloadURL方法获取可下载的 URL,该 URL 可用于访问该照片。最后,我们使用addDoc方法将新照片添加到 Firestore 数据库的photocollection属性中。 -
修改
takePhoto方法以调用我们在上一步中创建的savePhoto方法:async takePhoto() { **const** **{latitude, longitude} =** await this.getLocation(); **const** **cameraPhoto =** await Camera.getPhoto({ resultType: CameraResultType.DataUrl, source: CameraSource.Camera, quality: 100 }); **if** **(cameraPhoto.****dataUrl****) {** **await****this****.****savePhoto****(cameraPhoto.****dataUrl****, latitude, longitude);** **}** }
我们现在可以检查照片拍摄过程的完整功能:
-
运行以下 Capacitor 命令以将应用程序包复制到原生移动项目中:
ionic cap copy -
使用 Capacitor 的
open命令打开原生移动项目,并使用相应的 IDE 运行项目。 -
打开应用程序的 Firebase 控制台,并在 构建 部分中选择 存储 选项。点击 开始 按钮,选择 以测试模式开始 选项,然后点击 下一步。最后,点击 完成 以完成设置云存储的过程。
-
使用应用程序拍摄一张好照片。为了验证你的照片是否已成功上传到 Firebase,请刷新 Firebase 控制台中的页面。你应该会看到一个如下条目:
图 6.11 – Firebase 存储
- 类似地,在 构建 部分中选择 Firestore 数据库 选项,你应该会看到以下内容:
图 6.12 – Cloud Firestore
在前面的屏幕截图中,1oFxxWgQseIwqWUrYBkN条目是包含实际文件 URL 及其位置数据的照片的逻辑对象。
我们应用的第一页现在功能完整。我们已经完成了从拍摄和上传照片到 Firebase,包括其位置数据的全过程。我们首先设置和配置了 Firebase 项目,最后通过学习如何使用 AngularFire 库与该项目交互来完成。在下一节中,我们将通过实现应用的第二页来达到最终目标。
使用 CesiumJS 预览照片
我们应用的下个功能将是将我们用相机拍摄的所有照片显示在 3D 地图上。CesiumJS 库提供了一个带有 3D 地球仪的查看器,我们可以用它来可视化各种事物,例如特定位置上的图像。我们应用的新功能将包括以下内容:
-
配置 CesiumJS
-
在查看器上显示照片
我们将首先学习如何设置 CesiumJS 库。
配置 CesiumJS
CesiumJS 库是一个我们可以安装的 npm 包,用于开始使用 3D 地图和可视化:
-
运行以下
npm命令来安装 CesiumJS:npm install cesium -
打开
angular.json配置文件,并在build架构师选项的assets数组中添加以下条目:{ "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Workers", "output": "/assets/cesium/Workers" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/ThirdParty", "output": "/assets/cesium/ThirdParty" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Assets", "output": "/assets/cesium/Assets" }, { "glob": "**/*", "input": "node_modules/cesium/Build/Cesium/Widgets", "output": "/assets/cesium/Widgets" }上述条目将把所有 CesiumJS 源文件复制到应用
assets文件夹内的cesium文件夹中。 -
还要将 CesiumJS 小部件样式表文件添加到
build部分的styles数组中:"styles": [ **"node_modules/cesium/Build/Cesium/Widgets/widgets.css"****,** "src/theme/variables.scss", "src/global.scss" ]CesiumJS 的查看器包含一个带有小部件的工具栏,包括搜索栏和下拉菜单以选择特定类型的地图,例如 Bing Maps 或 Mapbox。
-
打开我们应用的主入口点文件
main.ts,并添加以下行:(window as Record<string, any>)['CESIUM_BASE_URL'] = '/assets/cesium/';CESIUM_BASE_URL全局变量指示 CesiumJS 源文件的位置。 -
使用以下
npm命令安装自定义 webpack 构建器:npm install -D @angular-builders/custom-webpack构建器是一个扩展 Angular CLI 默认功能的 Angular 库。
@angular-builders/custom-webpack构建器允许我们在构建应用时提供额外的 webpack 配置文件。在需要包含其他 webpack 插件或覆盖现有功能的情况下,这非常有用。 -
在项目的根文件夹中创建一个名为
extra-webpack.config.js的新 webpack 配置文件,并添加以下内容:module.exports = { resolve: { fallback: { "https": false, "zlib": false, "http": false, "url": false } }, module: { unknownContextCritical: false } };配置文件将确保 webpack 只会尝试加载它能够理解的 CesiumJS 代码。CesiumJS 使用一种格式,无法使用 webpack 进行静态分析。
-
打开
angular.json文件,并将build架构师部分的builder属性更改为使用自定义 webpack 构建器:"builder": "**@angular-builders/custom-webpack:browser**" -
在
build部分的options属性中定义自定义 webpack 配置文件的路径:"customWebpackConfig": { "path": "./extra-webpack.config.js" } -
还要配置
serve架构师部分以使用自定义 webpack 构建器:"serve": { "builder": "**@angular-builders/custom-webpack:dev-server**", "configurations": { "production": { "browserTarget": "app:build:production" }, "development": { "browserTarget": "app:build:development" }, "ci": { "progress": false } }, "defaultConfiguration": "development" }
现在我们已经完成了 CesiumJS 库的配置,我们可以开始创建我们功能的页面:
-
使用以下 Ionic CLI 命令创建一个新的页面:
ionic generate page view -
打开
view.page.html文件并修改第一个<ion-header>元素,使其包含一个菜单切换按钮:<ion-header [translucent]="true"> <ion-toolbar> **<****ion-buttons****slot****=****"start"****>** **<****ion-menu-button****color****=****"primary"****></****ion-menu-button****>** **</****ion-buttons****>** <ion-title>**View gallery**</ion-title> </ion-toolbar> </ion-header> -
修改
<ion-content>元素标题,并添加一个<div>元素作为我们的查看器容器:<ion-content [fullscreen]="true"> <ion-header collapse="condense"> <ion-toolbar> <ion-title size="large">**View gallery**</ion-title> </ion-toolbar> </ion-header> **<****div****#mapContainer****></****div****>** </ion-content>#mapContainer是我们用来在模板中声明元素别名的模板引用变量。 -
打开
view.page.scss文件并设置地图容器元素的大小:div { height: 100%; width: 100%; } -
让我们现在创建我们的查看器。打开
view.page.ts文件并按以下方式修改它:import { **AfterViewInit**, Component, **ElementRef**, OnInit, **ViewChild** } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; **import** **{** **Viewer** **}** **from****'cesium'****;** @Component({ selector: 'app-view', templateUrl: './view.page.html', styleUrls: ['./view.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class ViewPage implements OnInit, **AfterViewInit** { **@ViewChild****(****'mapContainer'****)** **content****:** **ElementRef** **|** **undefined****;** constructor() { } ngOnInit() { } **ngAfterViewInit****() {** **const** **viewer =** **new****Viewer****(****this****.****content****?.****nativeElement****);** **}** }我们在组件的
ngAfterViewInit方法内部创建一个新的Viewer对象。ngAfterViewInit方法在组件视图加载完成后被调用,它定义在AfterViewInit接口中。Viewer类的构造函数接受一个参数,即我们想要在上面创建查看器的原生 HTML 元素。在我们的情况下,我们想要将其附加到我们之前创建的地图容器元素上。因此,我们使用@ViewChild装饰器通过传递模板引用变量名称作为参数来引用该元素。 -
使用
ionic serve运行应用程序,并从主菜单点击查看相册选项。你应该看到以下输出:
图 6.13 – 查看相册页面
我们现在已成功在我们的应用程序中配置了 CesiumJS 库。在下一节中,我们将看到如何从中受益并在 CesiumJS 查看器的 3D 地球上显示我们的照片。
在查看器上显示照片
为了使我们的应用程序准备就绪,我们需要做的下一件事是在地图上显示我们的照片。我们将从 Firebase 获取所有照片并将它们添加到查看器中指定的位置。让我们看看我们如何实现这一点:
-
使用以下 Ionic CLI 命令创建一个新的 Angular 服务:
ionic generate service cesium -
打开
cesium.service.ts文件并添加以下import语句:import { Firestore, collection, getDocs } from '@angular/fire/firestore'; import { Cartesian3, Color, PinBuilder, Viewer } from 'cesium'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -
在
CesiumService类的constructor中注入Firestore服务并创建一个viewer属性,我们将使用它来存储我们的Viewer对象:export class CesiumService { **private****viewer****:** **Viewer** **|** **undefined****;** constructor(**private** **firestore: Firestore**) { } } -
创建一个
register方法来设置viewer属性:register(viewer: Viewer) { this.viewer = viewer; } -
创建一个方法来从 Cloud Firestore 获取
photos集合:private async getPhotos() { const photoCollection = collection(this.firestore, 'photos'); return await getDocs(photoCollection); }在前面的方法中,我们调用
getDocs方法来获取photos集合的数据。 -
为添加所有照片到查看器创建以下方法:
async addPhotos() { const pinBuilder = new PinBuilder(); const photos = await this.getPhotos(); photos.forEach(photo => { const entity = { position: Cartesian3.fromDegrees(photo.get('lng'), photo.get('lat')), billboard: { image: pinBuilder.fromColor(Color.fromCssColorString('#de6b45'), 48).toDataURL() }, description: `<img width="100%" style="margin:auto; display: block;" src="img/${photo.get('url')}" />` }; this.viewer?.entities.add(entity); }); }在查看器中,每张照片的位置将以标记的形式显示。因此,我们首先需要初始化一个
PinBuilder对象。前面的方法调用getPhotos方法从 Cloud Firestore 获取所有照片。对于每张照片,它创建一个包含position的entity对象,这是照片在度数中的位置,以及一个显示 48 像素大小标记的billboard属性。它还定义了一个description属性,当点击标记时将显示照片的实际图像。每个
entity对象都通过其add方法添加到查看器的entities集合中。 -
每张照片的描述都显示在信息框内。打开包含应用程序全局样式的
global.scss文件,并为信息框添加以下 CSS 样式:.cesium-infoBox, .cesium-infoBox-iframe { height: 100% !important; width: 100%; } -
现在,让我们使用页面中的
CesiumService。打开view.page.ts文件,并将CesiumService类注入到ViewPage类的constructor中:import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Viewer } from 'cesium'; **import** **{** **CesiumService** **}** **from****'../cesium.service'****;** @Component({ selector: 'app-view', templateUrl: './view.page.html', styleUrls: ['./view.page.scss'], standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) export class ViewPage implements OnInit, AfterViewInit { @ViewChild('mapContainer') content: ElementRef | undefined; constructor(**private** **cesiumService: CesiumService**) { } ngOnInit() { } ngAfterViewInit() { const viewer = new Viewer(this.content?.nativeElement); } } -
修改
ngAfterViewInit方法以注册查看器并添加照片:ngAfterViewInit() { **this****.****cesiumService****.****register****(****new****Viewer****(****this****.****content****?.****nativeElement****));** **this****.****cesiumService****.****addPhotos****();** }
我们现在可以查看地图上的照片了:
-
使用
ionic serve命令运行应用程序。 -
使用应用程序拍摄漂亮的照片,最好在不同的地点。
-
从主菜单中选择查看相册选项,你应该得到以下输出:
图 6.14 – 地图上的照片
- 点击地图上的一个标记,你应该能看到你的照片:
图 6.15 – 照片显示
现在我们有一个完整的移动应用程序,用于拍摄带有地理标记的照片并在地图上显示它们。我们看到了如何设置 CesiumJS 库并从 Cloud Firestore 获取我们的照片。CesiumJS 查看器的 API 为我们在地图上可视化照片和与之交互提供了简单的方法。
摘要
在本章中,我们构建了一个用于拍照、标记当前位置并在 3D 地图上显示照片的移动应用程序。最初,我们学习了如何使用 Ionic 框架创建新的移动应用程序。我们在本地构建了应用程序,并集成了 Capacitor 以与相机和 GPS 设备交互。相机用于拍照,GPS 用于标记位置。
之后,我们使用了 Firebase 服务将我们的照片文件和数据存储在云端。最后,我们学习了如何从 Firebase 检索存储的照片,并使用 CesiumJS 库在 3D 球上显示它们。
在下一章中,我们将探讨在 Angular 中预渲染内容的另一种方法。我们将使用服务器端渲染技术来创建一个 GitHub 站点。
实践问题
-
我们可以使用哪个工具包在 Capacitor 应用中创建 UI?
-
在 Capacitor 应用中,我们使用哪种方法用相机拍照?
-
在 Capacitor 应用中,我们如何读取当前的位置?
-
我们如何使用 Ionic 添加菜单切换按钮?
-
我们使用哪个 Capacitor 命令来同步应用程序包与原生移动项目?
-
在 Cloud Firestore 中,测试模式和发布模式有什么区别?
-
我们如何使用 AngularFire 库初始化应用程序?
-
我们使用哪种方法从 Cloud Firestore 集合中获取数据?
-
我们如何使用 CesiumJS 库创建一个标记?
-
我们如何使用 CesiumJS 将经纬度转换为度?
进一步阅读
-
开始使用 Capacitor:
capacitorjs.com/docs/getting-started -
Capacitor 的 Android 入门指南:
capacitorjs.com/docs/android#getting-started -
Capacitor 的 iOS 入门指南:
capacitorjs.com/docs/ios#getting-started -
使用 Ionic 进行 Angular 开发:
ionicframework.com/docs/angular/overview -
AngularFire 库文档:
firebaseopensource.com/projects/angular/angularfire2 -
CesiumJS 快速入门指南:
cesium.com/docs/tutorials/quick-start -
CesiumJS 和 Angular 文章:
cesium.com/blog/2018/03/12/cesium-and-angular
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论: