• 零、Angular安装
  • 一、项目起步
  • 二、编写路由组件
  • 三、编写页面组件
    • 1.编写单一组件
    • 2.模拟数据
    • 3.编写主从组件
  • 四、编写服务
    • 1.为什么需要服务
    • 2.编写服务
  • 五、引入RxJS改造项目
    • 1.关于RxJS
    • 2.引入RxJS
    • 3.改造数据获取方式
  • 六、改造组件
    • 1.添加历史记录组件
    • 2.添加增删改查功能
  • 七、HTTP改造
    • 1.引入HTTP
    • 2.通过HTTP请求数据
    • 3.通过HTTP修改数据
    • 4.通过HTTP增加数据
    • 5.通过HTTP删除数据
    • 6.通过HTTP查找数据
  • 八、结语

这个入门项目是我学习完Angular 英雄指南教程后,自己手写的一个练习项目,一步一步来,最终的项目源码可以这里查看,大佬们请指点啦。

推荐两个Angular学习网站:

  1. Angular 中文网
  2. Angular 修仙之路

还有呢,我没怎么关注到样式,所以样式会有点丑,主要都放在核心逻辑中了。
最终实现:

  • 首页书本列表数据展示
  • 各个页面静态/动态路由跳转
  • 本地模拟数据服务
  • 书本数据的增删改查
  • 父子组件通信
  • 常用指令使用和介绍

angular_books_result.png

后面我将把这个系列的文章,收录到我的【CuteJavaScript】中,里面有整理了ES6/7/8/9知识点重温JS基础系列文章。

那么,快跟我一步步来完成这个入门项目吧。

零、Angular安装

Angular 需要 Node.js8.x10.x 版本。
检查你的Node.js版本,请在终端/控制台窗口中运行 node -v 命令。
要想安装Node.js,请访问 nodejs.org。

  1. 安装Angular CLI
  1. npm install -g @angular/cli
  1. 常用命令

后续用到会详细介绍这些命令。

  • 启动服务,并打开新窗口
  1. ng serve --open
  2. # --open 可简写 -o
  • 创建新组件
  1. ng generate component books
  2. # generate 可简写 g
  • 创建新服务
  1. ng generate service books
  • 创建路由模块
  1. ng generate module app-routing --flat --module=app
  • 其他
    另外Angular CLI还有很多的命令提供,详细可以查阅官方文档 Angular CLI 命令

最后搭建完是这样:

Angular6 项目实战(个人书屋) - 图2

一、项目起步

  1. 创建项目
  1. ng new books
  2. cd books
  1. 创建所需的两个页面组件
  1. ng g component index
  2. ng g component detail

ggenerate的简写。

二、编写路由组件

这里为了项目结构先起来,所以先简单配置一下路由,后面路由会调整,如果遇到什么不懂,可以查看Angular 路由与导航

  1. 安装路由模块
  1. ng g module app-routing --flat --module=app

知识点:
--flat 把这个文件放进了 src/app 中,而不是单独的目录中。
--module=app 告诉 CLI 把它注册到 AppModuleimports 数组中。

  1. 引入路由模块
  1. // app-routing.module.ts
  2. import { RouterModule, Routes } from '@angular/router';
  1. 导出路由模块的指令

这里需要添加一个 @NgModule.exports 数组,并传入RouterModule,导出 RouterModule 让路由器的相关指令可以在 AppModule 中的组件中使用。

  1. // app-routing.module.ts
  2. @NgModule({
  3. imports: [CommonModule],
  4. declarations: [],
  5. exports: [RouterModule]
  6. })
  1. 添加定义路由

这里添加路由的时候,记得将所需要指向的组件也引入进来,这里我们需要引入两个页面的组件:

  1. // app-routing.module.ts
  2. import { IndexComponent } from './index/index.component';
  3. import { DetailComponent } from './detail/detail.component';

然后将我们所需要的路由定义在routes变量中,类型是我们引入的Routes

  1. // app-routing.module.ts
  2. const routes: Routes = [
  3. { path: '', redirectTo:'/index', pathMatch:'full' }, // 1
  4. { path: 'index', component: IndexComponent}, // 2
  5. { path: 'detail/:id', component: DetailComponent}, // 3
  6. ]

知识点
angular的路由接收两个参数:

  • path:用于匹配浏览器地址栏中 URL 的字符串。
  • component:当导航到此路由时,路由器展示的组件名称。

第1行代码
作为路由系统的默认路由,当所有路由都不匹配的话,就会重定向到这个路由,并展示对应的组件。
第2行代码
正常情况下的路由配置。
第3行代码
配置的是携带参数的路由,在路由/后,用 : 拼接参数名来实现,获取这个参数的值的方法后面会介绍

另外,我们还可以这么传递参数,直接将数据通过路由传入,后面还会介绍:

  1. { path: 'pathname', component: DemoComponent, data: { title: 'pingan8787' } },
  1. 添加路由监视

配置好路由还不能使用,需要一个监视路由变化的工具,这时候需要把RouterModule添加到 @NgModule.imports 数组中,并用 routes 来配置它。
这里只需要调用imports数组中的 RouterModule.forRoot() 函数就行了,就像这样:

  1. // app-routing.module.ts
  2. imports: [ RouterModule.forRoot(routes) ],
  1. 添加路由出口

所谓的路由出口,就是路由所对应的组件展示的地方,接下来我们在app.component.html内容中,添加<router-outlet></router-outlet>

  1. <!-- app.component.html -->
  2. <div>
  3. <h1> 欢迎来到我的个人书屋! </h1>
  4. <router-outlet></router-outlet>
  5. </div>

这里的<router-outlet></router-outlet>就是我们路由输出的地方,也是组件展示的地方,简单理解就是,它会告诉路由器要在哪里显示路由的视图。

  1. 添加路由链接

所谓的路由链接,就是出发路由跳转事件的地方,比如一个按钮,一张图片等,我们还是在app.component.html中,使用<a routerLink="/path"></a>添加3个按钮:

  1. <!-- app.component.html -->
  2. <div>
  3. <h1> 欢迎来到我的个人书屋! </h1>
  4. <a routerLink="">重定向</a> |
  5. <a routerLink="/index">打开首页</a> |
  6. <a routerLink="/detail/1">打开书本详情</a>
  7. <router-outlet></router-outlet>
  8. </div>

这边3个按钮的路由,我们将上面定义的3种路由,传入到routerLink参数中,现在就项目就可以实现页面跳转了。

另外,这里还可以传入一个可选参数routerLinkActive="className",表示当这个<a>标签激活的时候显示的样式,值是一个字符串,为样式的类名:

  1. <a routerLink="/index" routerLinkActive="activeClass">打开首页</a> |
  1. 获取带参数路由的参数

在第7步中,我们点击 打开书本详情 按钮中,在路由中带了参数,这时候我们需要这么来获取这个参数:

  • 先导出模块ActivatedRouteLocation
  1. // detail.component.ts
  2. import { ActivatedRoute } from '@angular/router';
  3. import { Location } from '@angular/common';
  • 再注入到构造函数中,并将值作为私有变量:
  1. // detail.component.ts
  2. export class DetailComponent implements OnInit {
  3. constructor(
  4. private route: ActivatedRoute,
  5. private location: Location
  6. ) { }
  7. ngOnInit() {}
  8. }

知识点:
ActivatedRoute 保存该 DetailComponent 实例的路由信息。可以从这个组件获取URL中的路由参数和其他数据。
Location 是一个 Angular 的服务,用来与浏览器打交道。后续会使用它来导航回上一个视图。

  • 提取路由参数:

这里声明getDetail方法,提取路由参数,并ngOnInit生命周期钩子方法在中执行。

  1. // detail.component.ts
  2. ngOnInit() {
  3. this.getDetail()
  4. }
  5. getDetail(): void{
  6. const id = +this.route.snapshot.paramMap.get('id');
  7. console.log(`此课本的id${id}`)
  8. }

知识点
route.snapshot 是一个路由信息的静态快照,抓取自组件刚刚创建完毕之后。
paramMap 是一个URL中路由所携带的参数值的对象。”id”对应的值就是要获取的书本的 id。
注意
路由参数总会是字符串。这里我们使用 (+) 操作符,将字符串转换成数字。

现在在浏览器上刷新下页面,再点击 打开书本详情 按钮,可以看到控制台输出了此课本的id是1的结果。
到这一步,我们算是把路由配置完成了,接下来可以开始做页面的逻辑了。

三、编写页面组件

接下来开始编写页面组件,这里我们挑重点来写,一些布局的样式,后面可以看源码。

1.编写单一组件

我们首先写一个书本信息的组件,代码如下:

  1. <!-- index.component.html -->
  2. <div class="content">
  3. <div class="books_box">
  4. <!-- 单个课本 -->
  5. <div class="books_item" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
  6. <img class="cover" src="https://img3.doubanio.com/view/subject/m/public/s29988481.jpg">
  7. <div class="title"><a>像火焰像灰烬</a></div>
  8. <div class="author">程姬</div>
  9. </div>
  10. </div>
  11. </div>

知识点
*ngFor 是一个 Angular 的复写器(repeater)指令,就像angular1中的ng-forvuejs中的v-for。 它会为列表中的每项数据复写它的宿主元素。
这时候可以看到页面变成下面这个样子:
Angular6 项目实战(个人书屋) - 图3

接下来我们要把写死在HTML上面的数据,抽到JS中:

现在先新建一个books.ts文件来定义一个Book类,并添加idurltitleauthor四个属性:

  1. // src/app/books.ts
  2. export class Book {
  3. id: number;
  4. url: string;
  5. title: string;
  6. author: string;
  7. }

然后回到index.component.ts文件去引入它,并定义一个books属性,使用导入进来的Book类作为类型:

  1. // index.component.ts
  2. import { Book } from '../books';
  3. export class IndexComponent implements OnInit {
  4. books: Book = {
  5. id: 1,
  6. url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg',
  7. title: '像火焰像灰烬',
  8. author: '程姬',
  9. }
  10. }

然后再改造前面的组件文件index.component.html:

  1. <!-- index.component.html -->
  2. <div class="books_item" *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
  3. <img class="cover" src="{{books.url}}" alt="{{books.id}}">
  4. <div class="title">
  5. <a>{{books.title}}</a>
  6. </div>
  7. <div class="author">{{books.author}}</div>
  8. </div>

接着,我们再为每个课本添加一个点击事件,来实现点击封面图能查看大图的效果,现在index.component.ts中定义一个getDetailImage方法,并在index.component.html中绑定该方法:

  1. // index.component.ts
  2. export class IndexComponent implements OnInit {
  3. getDetailImage(books){
  4. alert(`正在查看id${books.id}的大图!`);
  5. }
  6. }

这边方法的具体实现,不写,不是本文重点。下面是增加点击事件的绑定:

  1. <!-- index.component.html -->
  2. <img class="cover" src="{{books.url}}" alt="{{books.id}}" (click)="getDetailImage(books)">

知识点
(click)是Angular用来绑定事件,它会让 Angular 监听这个<img> 元素的 click 事件。 当用户点击 <img> 时,Angular 就会执行表达式 getDetailImage(books)

再来,我们引入前面学到的路由链接指令来改造HTML:

  1. <!-- index.component.html -->
  2. <a routerLink="/detail/{{books.id}}">{{books.title}}</a>

这时候,我们在点击书本的标题,发现页面跳转到URL地址为http://localhost:4200/detail/1的页面,这就说明,我们页面的路由跳转也成功了~

改造完成后,可以看到,页面显示的还是一样,接下来我们先这样放着,因为我们后面会进行数据模拟,和模拟服务器请求。

我们就这样写好第一个单一组件,并且数据是从JS中读取的。

2.模拟数据

这时候为了方便后面数据渲染,我们这里需要模拟一些本地数据,我们创建一个本地mock-books.ts文件来存放模拟的数据:

  1. // app/mock-books.ts
  2. import { Books } from './books';
  3. export const BookList: Books[] = [
  4. {
  5. id: 1,
  6. url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg',
  7. title: '像火焰像灰烬',
  8. author: '程姬',
  9. },
  10. // 省略其他9条
  11. ]

然后在index.component.ts中导入模拟的数据,并将原有的books值修改成导入的模拟数据BookList

  1. // index.component.ts
  2. import { BookList } from '../mock-books';
  3. books = BookList;

并将原本的*ngFor中修改成这样,绑定真正的数据:

  1. <!-- index.component.html -->
  2. <div class="books_item" *ngFor="let item of books">
  3. <img class="cover" src="{{item.url}}" alt="{{item.id}}">
  4. <div class="title">
  5. <a>{{item.title}}</a>
  6. </div>
  7. <div class="author">{{item.author}}</div>
  8. </div>

3.编写主从组件

当我们写完一个单一组件后,我们会发现,如果我们把每个组件都写到同一个HTML文件中,这是很糟糕的事情,这样做有缺点:

  • 代码复用性差;(导致每次相同功能要重新写)
  • 代码难维护;(因为一个文件会非常长)
  • 影响性能;(打开每个页面都要重复加载很多)

为了解决这个问题,我们这里就要开始使用真正的组件化思维,将通用常用组件抽离出来,通过参数传递来控制组件的不同业务形态。
这便是我们接下来要写的主从组件。

思考一下,我们这里现在能抽成组件作为公共代码的,就是这个单个书本的内容,因为每个书本的内容都一致,只是里面数据的差异,于是我们再新建一个组件:

  1. ng g component books

并将前面index.component.html中关于课本的代码剪切到books.component.html中来,然后删除掉*ngFor的内容,并将原本本地的变量books替换成list,这个变量我们等会会取到:

  1. <!-- books.component.html -->
  2. <div class="books_item">
  3. <img class="cover" src="{{list.url}}" alt="{{list.id}}" (click)="getDetailImage(list)">
  4. <div class="title">
  5. <a routerLink="/detail/{{list.id}}">{{list.title}}</a>
  6. </div>
  7. <div class="author">{{list.author}}</div>
  8. </div>

再将这个组件,引用到它的父组件中,这里是要引用到index.component.html的组件中,并将前面的*ngFor再次传入<app-books>

  1. <div class="content">
  2. <div class="books_box">
  3. <app-books *ngFor="let item of books"></app-books>
  4. </div>
  5. </div>

接下来要做的就是获取到list变量的值,显然这个值是要从外面组件传进来的,我们需要在books.component.ts引入前面定义的 Books类 和 @Input() 装饰器,还要添加一个带有 @Input() 装饰器list 属性,另外还要记得将getDetailImage方法也剪切过来:

  1. // books.component.ts
  2. import { Component, OnInit, Input } from '@angular/core';
  3. import { Books } from '../books';
  4. export class BooksComponent implements OnInit {
  5. @Input() list: Books;
  6. constructor() { }
  7. ngOnInit() {}
  8. getDetailImage(books){
  9. alert(`正在查看id${books.id}的大图!`);
  10. }
  11. }

@Input() 装饰器介绍具体可以查看 手册

我们要获取的 list 属性必须是一个带有@Input()装饰器的输入属性,因为外部的 IndexComponent 组件将会绑定到它。就像这样:

  1. <app-books *ngFor="let list of books" [list]="item"></app-books>

知识点
[list]="item"Angular属性绑定语法。这是一种单向数据绑定。从 IndexComponentitem 属性绑定到目标元素的 list 属性,并映射到了 BooksComponentlist 属性。

做到这里,我们已经将BooksComponent作为IndexComponent的子组件来引用了,在实际开发过程中,这样的父子组件关系,会用的非常多。

写到这里,看看我们项目,还是一样正常在运行,只是现在项目中组件分工更加明确了。

现在的效果图:
Angular6 项目实战(个人书屋) - 图4

四、编写服务

截止到这部分,我们的BooksComponent组件获取和显示的都是本地模拟的数据。
接下来我们要开始对这些进行重构,让聚焦于为它的视图提供支持,这也让它更容易使用模拟服务进行单元测试。

1.为什么需要服务

我们不应该让组件来直接获取或保存数据,它们应该聚焦于展示数据,而数据访问的工作交给其他服务来做。
这里我们需要创建一个名为BooksService的服务,让我们应用中所有的类都使用它来获取书本列表的数据,使用的时候,只需要将它通过Angular的依赖注入机制注入到需要用的组件的构造函数中。

知识点:
服务可以实现多个不同组件之间信息共享,后面我们还会将它注入到两个地方:
BooksService中,使用该服务发送消息。
IndexService中,使用该服务来展示消息。

接下来我们使用命令行,创建BooksService

  1. ng g service books

在生成的books.service.ts文件中:

  1. // books.service.ts
  2. import { Injectable } from '@angular/core';
  3. @Injectable({
  4. providedIn: 'root'
  5. })

新导入了@Injectable装饰器,是为了让BooksService提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖,简单理解就是如果你的服务需要依赖,那么你就需要导入它
并且它接收该服务的元数据对象。

2.编写服务

接下来我们开始编写books.service.ts服务。

  • 导入服务所需组件

这里我们导入BooksBookList,并添加一个getBooks方法来返回所有书本的数据,并且还需要添加一个getBooks方法来返回指定id的书本信息:

  1. // index.component.ts
  2. import { Books } from './books';
  3. import { BookList } from './mock-books';
  4. @Injectable({
  5. providedIn: 'root'
  6. })
  7. export class BooksService {
  8. constructor() { }
  9. getBookList(): Books[] {
  10. return BookList;
  11. }
  12. getBook(id: number): Books{
  13. return BookList.find(book => book.id === id)
  14. }
  15. }

在我们使用这个服务之前,需要先注册该服务,因为我们在使用ng g service books命令创建服务时,CLI已经默认为我们添加了注册了,这是方法就是上面代码中的:

  1. providedIn: 'root'

表示将我们的服务注册在根注入器上,这样我们就可以把这个服务注入到任何享用的类上了。

  • 修改IndexComponent

先删除BookList的引入,并修改books属性的定义:

  1. // index.component.ts
  2. import { BooksService } from '../books.service';
  3. export class IndexComponent implements OnInit {
  4. books : Books[];
  5. ngOnInit() {}
  6. }

然后注入我们的BooksService服务,需要先往构造函数中添加一个私有的booksservice,使用注入的BooksService作为类型,理解成一个注入点:

  1. // index.component.ts
  2. constructor(private booksservice: BooksService) { }

之后我们需要添加一个getBooks方法来获取这些书本数据,并在生命周期函数ngOnInit中调用:

  1. export class IndexComponent implements OnInit {
  2. ngOnInit() {
  3. this.getBooks();
  4. }
  5. getBooks(): void{
  6. this.books = this.booksservice.getBookList();
  7. }
  8. }
  • 修改DetailComponent
    我们先改造书本详情页的HTML结构:
  1. <!-- detail.component.html -->
  2. <div *ngIf="books" class="detail">
  3. <h3>《{{books.title}}》介绍</h3>
  4. <div>
  5. <img src="{{books.url}}">
  6. </div>
  7. <p>书本标题: {{books.title}}</p>
  8. <p>书本作者: {{books.author}}</p>
  9. <p>书本id: {{books.id}}</p>
  10. </div>
  11. <div *ngIf="!books" class="detail">
  12. <h3>暂无信息</h3>
  13. </div>

知识点
这里使用了*ngIf指令,当条件为true则显示其HTML内容。

  1. // detail.component.ts
  2. import { Books } from '../books';
  3. import { BooksService } from '../books.service';
  4. export class DetailComponent implements OnInit {
  5. constructor(
  6. private route: ActivatedRoute,
  7. private location: Location,
  8. private booksservice: BooksService // 引入BooksService服务
  9. ) { }
  10. books: Books; // 定义books类型
  11. ngOnInit() {
  12. this.getDetail()
  13. }
  14. getDetail(): void{
  15. const id = +this.route.snapshot.paramMap.get('id');
  16. this.getBooks(id);
  17. }
  18. getBooks(id: number): void {
  19. this.books = this.booksservice.getBook(id);
  20. }
  21. }

这段代码,主要定义了getBooks方法,当刚进入页面时,将书本id传入getBooks方法,去BooksService去获取对应id的书本信息,并复制给变量books,然后展示到页面。

改造之后,我们的页面显示依旧正常。

Angular6 项目实战(个人书屋) - 图5

但是我们要知道,这背后的逻辑已经改变了。

五、引入RxJS改造项目

1.关于RxJS

这里简单介绍关键概念,具体可以查看 RxJS 官网,也可以参考 浅析Angular之RxJS

什么是RxJS

RxJS全称Reactive Extensions for JavaScript,中文意思: JavaScript的响应式扩展。
RxJS主要是提供一种更加强大和优雅的方式,来利用响应式编程的模式,实现JavaScript的异步编程。

RxJS优点

  • 纯净性;
  • 流动性;

RxJS核心概念

RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。RxJS 中含有两个基本概念:ObservablesObserver
Observables 作为被观察者,是一个值或事件的流集合;而 Observer 则作为观察者,根据 Observables 进行处理。它们之间的订阅发布关系(观察者模式) 如下:
订阅Observer 通过 Observable 提供的 subscribe() 方法订阅 Observable
发布Observable 通过回调 next 方法向 Observer 发布事件。

———— 来源Angular修仙之路 RxJS Observable

另外这里列出来一些核心,具体还是看官网咯,并且下面使用到的时候会具体介绍。

  • Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer(观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
  • Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 mapfilterconcatflatMap 等这样的操作符来处理集合。
  • Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
  • Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeoutrequestAnimationFrame或其他。

2.引入RxJS

在我们的真实应用中,我们必须要等到服务器响应后,我们才能获取到数据,因此这天生就需要用异步思维来操作。

由于Angular中已经自带RxJS,所以我们只要在需要使用的时候,引入即可使用:

3.改造数据获取方式

了解完RxJS的一些概念后,我们开始改造下这些书本的数据获取方式。

  • 改造BooksService

首先我们从 RxJS 中导入 Observableof 符号:

  1. // books.service.ts
  2. import { Observable, of } from 'rxjs';

知识点
Observable: 观察者模式中的观察者,具体可以参考 Angular修仙之路 RxJS Observable
of: 用来获取观察者拿到的数据,通常是一个Observable

然后修改getBookList方法

  1. // books.service.ts
  2. getBookList(): Observable<Books[]> {
  3. return of(BookList);
  4. }

这里 of(BookList) 返回一个Observable<Books[]>,它会发出单个值,这个值就是这些模拟书本的数组。

  • 改造IndexComponent

这里也要修改getBooks方法,使用subscribe去订阅服务返回回来的值:

  1. // index.component.ts
  2. getBooks(): void{
  3. this.booksservice.getBookList()
  4. .subscribe(books => this.books = books);
  5. }

由于原本直接赋值数据,在实际场景中是不可能这样同步的,所以这里subscribe函数,会在Observable发出数据以后,再把书本列表传到里面的回调函数,再复制给books属性。
使用这种异步方式,当 BooksService 从远端服务器获取英雄数据时,不用担心还没拿到数据就执行后面。

下一步,我们就要改造一下项目了。

六、改造组件

从这里开始,我们要使用RxJS来改造组件和添加新功能了,让整个项目更加完善。

1.添加历史记录组件

  • 创建HistoryComponent组件
  1. ng g component hostory

然后在app.component.html文件夹中添加组件:

  1. <!-- app.component.html -->
  2. <app-history></app-history>

2.添加增删改查功能

这里我们要开始做书本的增删改查功能,需要先创建一个HistoryService服务,方便我们实现这几个功能:

  • 创建HistoryService服务
  1. ng g service history

然后在生成的ts文件中,增加addclear方法,add方法用来添加历史记录到history数组中,clear方法则是清空history数组:

  1. // history.service.ts
  2. export class HistoryService {
  3. history: string[] = [];
  4. add(history: string){
  5. this.history.push(history);
  6. }
  7. clear(){
  8. this.history = [];
  9. }
  10. }
  • 使用HistoryService服务

在将这个服务,注入到BooksService中,并改造getBooks方法:

  1. // books.service.ts
  2. import { HistoryService } from './history.service';
  3. constructor(
  4. private historyservice: HistoryService
  5. ) { }
  6. getBooks(): void{
  7. this.historyservice.add('请求书本数据')
  8. this.booksservice.getBookList()
  9. .subscribe(books => this.books = books);
  10. }

也可以用相同方法,在IndexComponent中添加访问首页书本列表的记录。

  1. // index.component.ts
  2. import { HistoryService } from '../history.service';
  3. constructor(
  4. private booksservice: BooksService,
  5. private historyservice: HistoryService
  6. ) { }
  7. getBooks(): void{
  8. this.historyservice.add('访问首页书本列表');
  9. this.booksservice.getBookList()
  10. .subscribe(books => this.books = books);
  11. }

接下来,将我们的HistoryService注入到HistoryComponent中,然后才能将历史数据显示到页面上:

  1. // history.component.ts
  2. import { HistoryService } from '../history.service';
  3. export class HistoryComponent implements OnInit {
  4. constructor(private historyservice: HistoryService) { }
  5. ngOnInit() {}
  6. }
  1. <!-- history.component.html -->
  2. <div *ngIf="historyservice.history.length">
  3. <h2>操作历史:</h2>
  4. <div>
  5. <button class="clear"
  6. (click)="historyservice.clear()"
  7. >清除</button>
  8. <div *ngFor="let item of historyservice.history">{{item}}</div>
  9. </div>
  10. </div>

代码解释
*ngIf="historyservice.history.length",是为了防止还没有拿到历史数据,导致后面的报错。
(click)="historyservice.clear()", 绑定我们服务中的clear事件,实现清除缓存。
*ngFor="let item of historyservice.history",将我们的历史数据渲染到页面上。

到了这一步,就能看到历史数据了,每次也换到首页,都会增加一条。

Angular6 项目实战(个人书屋) - 图6

接下来,我们要在书本详情页也加上历史记录的统计,导入文件,注入服务,然后改造getBooks方法,实现历史记录的统计:

  1. // detail.component.ts
  2. import { HistoryService } from '../history.service';
  3. export class DetailComponent implements OnInit {
  4. constructor(
  5. private route: ActivatedRoute,
  6. private location: Location,
  7. private booksservice: BooksService,
  8. private historyservice: HistoryService
  9. ) { }
  10. //...
  11. getBooks(id: number): void {
  12. this.books = this.booksservice.getBook(id);
  13. this.historyservice.add(`查看书本${this.books.title},id${this.books.id}`);
  14. console.log(this.books)
  15. }
  16. }

Angular6 项目实战(个人书屋) - 图7

这时候就可以在历史记录中,看到这些操作的记录了,并且清除按钮也正常使用。

七、HTTP改造

原本我只想写到上一章,但是想到,我们实际开发中,哪有什么本地数据,基本上数据都是要从服务端去请求,所以这边也有必要引入这一张,模拟实际的HTTP请求。

1.引入HTTP

在这一章,我们使用Angular提供的 HttpClient 来添加一些数据持久化特性。
然后实现对书本数据进行获取,增加,修改,删除和查找功能。

HttpClient是Angular通过 HTTP 与远程服务器通讯的机制。

这里我们为了让HttpClient在整个应用全局使用,所以将HttpClient导入到根模块app.module.ts中,然后把它加入 @NgModule.imports 数组:

  1. import { HttpClientModule } from '@angular/common/http';
  2. @NgModule({
  3. //...
  4. imports: [
  5. BrowserModule,
  6. AppRoutingModule,
  7. HttpClientModule
  8. ],
  9. //...
  10. })

这边我们使用 内存 Web API(In-memory Web API) 模拟出的远程数据服务器通讯。
注意: 这个内存 Web API 模块与 Angular 中的 HTTP 模块无关。

通过下面命令来安装:

  1. npm install angular-in-memory-web-api --save

然后在app.module.ts中导入 HttpClientInMemoryWebApiModuleInMemoryDataService 类(后面创建):

  1. // app.module.ts
  2. import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
  3. import { InMemoryDataService } from './in-memory-data.service';
  4. @NgModule({
  5. // ...
  6. imports: [
  7. // ...
  8. HttpClientInMemoryWebApiModule.forRoot(
  9. InMemoryDataService, {dataEncapsulation:false}
  10. )
  11. ],
  12. // ...
  13. })
  14. export class AppModule { }

知识点:
forRoot() 配置方法接受一个 InMemoryDataService 类(初期的内存数据库)作为参数。

然后我们要创建InMemoryDataService类:

  1. ng g service InMemoryData

并将生成的in-memory-data.service.ts修改为:

  1. // in-memory-data.service.ts
  2. import { Injectable } from '@angular/core';
  3. import { InMemoryDbService } from 'angular-in-memory-web-api';
  4. import { Books } from './books';
  5. @Injectable({
  6. providedIn: 'root'
  7. })
  8. export class InMemoryDataService implements InMemoryDbService {
  9. createDb(){
  10. const books = [
  11. {
  12. id: 1,
  13. url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg',
  14. title: '像火焰像灰烬',
  15. author: '程姬',
  16. },
  17. // 省略其他9条数据
  18. ];
  19. return {books};
  20. }
  21. constructor() { }
  22. }

这里先总结InMemoryDbService所提供的RESTful API,后面都要用到:
例如如果urlapi/books,那么

  • 查询所有成员:以GET方法访问api/books
  • 查询某个成员:以GET方法访问api/books/id,比如id1,那么访问api/books/1
  • 更新某个成员:以PUT方法访问api/books/id
  • 删除某个成员:以DELETE方法访问api/books/id
  • 增加一个成员:以POST方法访问api/books

2.通过HTTP请求数据

现在要为接下来的网络请求做一些准备,先在books.service.ts中引入HTTP符号,然后注入HttpClient并改造:

  1. // books.service.ts
  2. import { HttpClient, HttpHeaders} from '@angular/common/http';
  3. // ...
  4. export class BooksService {
  5. constructor(
  6. private historyservice: HistoryService,
  7. private http: HttpClient
  8. ) { }
  9. private log(histories: string){
  10. this.historyservice.add(`正在执行:${histories}`)
  11. }
  12. private booksUrl = 'api/books'; // 提供一个API供调用
  13. // ...
  14. }

这里我们还新增一个私有方法log和一个私有变量booksUrl

接下来我们要开始发起http请求数据,开始改造getBookList方法:

  1. // books.service.ts
  2. // ...
  3. getBookList(): Observable<Books[]> {
  4. this.historyservice.add('请求书本数据')
  5. return this.http.get<Books[]>(this.booksUrl);
  6. }
  7. // ...

这里我们使用 http.get 替换了 of,其它没修改,但是应用仍然在正常工作,这是因为这两个函数都返回了 Observable<Hero[]>

实际开发中,我们还需要考虑到请求的错误处理,要捕获错误,我们就要使用 RxJS 的 catchError() 操作符来建立对 Observable 结果的处理管道(pipe)。

我们引入catchError并改造原本getBookList方法:

  1. // books.service.ts
  2. getBookList(): Observable<Books[]> {
  3. this.historyservice.add('请求书本数据')
  4. return this.http.get<Books[]>(this.booksUrl).pipe(
  5. catchError(this.handleError<Books[]>('getHeroes', []))
  6. );
  7. }
  8. private handleError<T> (operation = 'operation', result?: T) {
  9. return (error: any): Observable<T> => {
  10. this.log(`${operation} 失败: ${error.message}`); // 发出错误通知
  11. return of(result as T); // 返回空结果避免程序出错
  12. };
  13. }

知识点
.pipe() 方法用来扩展 Observable 的结果。
catchError() 操作符会拦截失败的 Observable。并把错误对象传给错误处理器,错误处理器会处理这个错误。
handleError() 错误处理函数做了两件事,发出错误通知和返回空结果避免程序出错。

这里还需要使用tap操作符改造getBookList方法,来窥探Observable数据流,它会查看Observable的值,然后我们使用log方法,记录一条历史记录。
tap 回调不会改变这些值本身。

  1. // books.service.ts
  2. getBookList(): Observable<Books[]> {
  3. return this.http.get<Books[]>(this.booksUrl)
  4. .pipe(
  5. tap( _ => this.log('请求书本数据')),
  6. catchError(this.handleError<Books[]>('getHeroes', []))
  7. );
  8. }

3.通过HTTP修改数据

这里我们需要在原来DetailComponent上面,添加一个输入框、保存按钮和返回按钮,就像这样:

  1. <!-- detail.component.html -->
  2. <!-- 前面代码省略 -->
  3. <div>
  4. <h2>修改信息:</h2>
  5. <label>新标题:
  6. <input [(ngModel)]="books.title" placeholder="请输入新标题">
  7. </label>
  8. <button (click)="save()">保存</button>
  9. <button (click)="goBack()">返回</button>
  10. </div>

这边切记一点,一定要在app.module.ts中引入 FormsModule模块,并在@NgModuleimports中引入,不然要报错了。

  1. // app.module.ts
  2. // ...
  3. import { FormsModule } from '@angular/forms';
  4. @NgModule({
  5. // ...
  6. imports: [
  7. // ...
  8. FormsModule
  9. ],
  10. // ...
  11. })

input框绑定书本的标题books.title,而保存按钮绑定一个save()方法,这里还要实现这个方法:

  1. // detail.component.ts
  2. save(): void {
  3. this.historyservice.updateBooks(this.books)
  4. .subscribe(() => this.goBack());
  5. }
  6. goBack(): void {
  7. this.location.back();
  8. }

这里通过调用BooksServiceupdateBooks方法,将当前修改后的书本信息修改到源数据中,这里我们需要去books.service.ts中添加updateBooks方法:

  1. // books.service.ts
  2. // ...
  3. updateBooks(books: Books): Observable<any>{
  4. return this.http.put(this.booksUrl, books, httpOptions).pipe(
  5. tap(_ => this.log(`修改书本的id${books.id}`)),
  6. catchError(this.handleError<Books>(`getBooks请求是id${books.id}`))
  7. )
  8. }
  9. // ...

知识点
HttpClient.put() 方法接受三个参数:URL 地址要修改的数据其他选项
httpOptions 常量需要定义在@Injectable修饰器之前。

现在,我们点击首页,选择一本书进入详情,修改标题然后保存,会发现,首页上这本书的名称也会跟着改变呢。这算是好了。

4.通过HTTP增加数据

我们可以新增一个页面,并添加上路由和按钮:

  1. ng g component add

添加路由:

  1. // app-routing.module.ts
  2. // ...
  3. import { AddComponent } from './add/add.component';
  4. const routes: Routes = [
  5. { path: '', redirectTo:'/index', pathMatch:'full' },
  6. { path: 'index', component: IndexComponent},
  7. { path: 'detail/:id', component: DetailComponent},
  8. { path: 'add', component: AddComponent},
  9. ]

添加路由入口:

  1. <!-- app.component.html -->
  2. <!-- 省略一些代码 -->
  3. <a routerLink="/add">添加书本</a>

编辑添加书本的页面:

  1. <!-- add.component.html -->
  2. <div class="add">
  3. <h2>添加书本:</h2>
  4. <label>标题:
  5. <input [(ngModel)]="books.title" placeholder="请输入标题">
  6. </label>
  7. <label>作者:
  8. <input [(ngModel)]="books.author" placeholder="请输入作者">
  9. </label>
  10. <label>书本id:
  11. <input [(ngModel)]="books.id" placeholder="请输入书本id">
  12. </label>
  13. <label>封面地址:
  14. <input [(ngModel)]="books.url" placeholder="请输入封面地址">
  15. </label>
  16. <div><button (click)="add(books)">添加</button></div>
  17. </div>

初始化添加书本的数据:

  1. // add.component.ts
  2. // ...
  3. import { Books } from '../books';
  4. import { BooksService } from '../books.service';
  5. import { HistoryService } from '../history.service';
  6. import { Location } from '@angular/common';
  7. export class AddComponent implements OnInit {
  8. books: Books = {
  9. id: 0,
  10. url: '',
  11. title: '',
  12. author: ''
  13. }
  14. constructor(
  15. private location: Location,
  16. private booksservice: BooksService,
  17. private historyservice: HistoryService
  18. ) { }
  19. ngOnInit() {}
  20. add(books: Books): void{
  21. books.title = books.title.trim();
  22. books.author = books.author.trim();
  23. this.booksservice.addBooks(books)
  24. .subscribe( book => {
  25. this.historyservice.add(`新增书本${books.title},id${books.id}`);
  26. this.location.back();
  27. });
  28. }
  29. }

然后在books.service.ts中添加addBooks方法,来添加一本书本的数据:

  1. // books.service.ts
  2. addBooks(books: Books): Observable<Books>{
  3. return this.http.post<Books>(this.booksUrl, books, httpOptions).pipe(
  4. tap((newBook: Books) => this.log(`新增书本的id${newBook.id}`)),
  5. catchError(this.handleError<Books>('添加新书'))
  6. );
  7. }

现在就可以正常添加书本啦。

Angular6 项目实战(个人书屋) - 图8

5.通过HTTP删除数据

这里我们先为每个书本后面添加一个删除按钮,并绑定删除事件delete

  1. <!-- books.component.html -->
  2. <!-- 省略一些代码 -->
  3. <span class="delete" (click)="delete(list)">X</span>
  1. // books.component.ts
  2. import { BooksService } from '../books.service';
  3. export class BooksComponent implements OnInit {
  4. @Input() list: Books;
  5. constructor(
  6. private booksservice: BooksService
  7. ) { }
  8. // ...
  9. delete(books: Books): void {
  10. this.booksservice.deleteBooks(books)
  11. .subscribe();
  12. }
  13. }

然后还要再books.service.ts中添加deleteBooks方法来删除:

  1. // books.service.ts
  2. deleteBooks(books: Books): Observable<Books>{
  3. const id = books.id;
  4. const url = `${this.booksUrl}/${id}`;
  5. return this.http.delete<Books>(url, httpOptions).pipe(
  6. tap(_ => this.log(`删除书本${books.title},id${books.id}`)),
  7. catchError(this.handleError<Books>('删除书本'))
  8. );
  9. }

这里需要在删除书本结束后,通知IndexComponent将数据列表中的这条数据删除,这里还需要再了解一下Angular 父子组件数据通信
然后我们在父组件IndexComponent上添加change事件监听,并传入本地的funChange

  1. <!-- index.component.html -->
  2. <app-books *ngFor="let item of books" [list]="item"
  3. (change) = "funChange(item, $event)"
  4. ></app-books>

在对应的index.component.ts中添加funChange方法:

  1. // index.component.ts
  2. funChange(books, $event){
  3. this.books = this.books.filter(h => h.id !== books.id);
  4. }

再来,我们在子组件BooksComponent上多导入OutputEventEmitter,并添加@Output()修饰器和调用emit

  1. import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
  2. export class BooksComponent implements OnInit {
  3. // ...
  4. @Output()
  5. change = new EventEmitter()
  6. // ...
  7. delete(books: Books): void {
  8. this.booksservice.deleteBooks(books)
  9. .subscribe(()=>{
  10. this.change.emit(books);
  11. });
  12. }
  13. }

这样就实现了我们父子组件之间的事件传递啦,现在我们的页面还是正常运行,并且删除一条数据后,页面数据会更新。

6.通过HTTP查找数据

还是在books.service.ts,我们添加一个方法getBooks,来实现通过ID来查找指定书本,因为我们是通过ID查找,所以返回的是单个数据,这里就是Observable<Books>类型:

  1. // books.service.ts
  2. getBooks(id: number): Observable<Books>{
  3. const url = `${this.booksUrl}/${id}`;
  4. return this.http.get<Books>(url).pipe(
  5. tap( _ => this.log(`请求书本的id${id}`)),
  6. catchError(this.handleError<Books>(`getBooks请求是id${id}`))
  7. )
  8. }

注意,这里 getBooks 会返回 Observable<Books>,是一个可观察的单个对象,而不是一个可观察的对象数组。

八、结语

这个项目其实很简单,但是我还是一步一步的写下来,一方面让自己更熟悉Angular,另一方面也是希望能帮助到更多朋友哈~
最终效果:

Angular6 项目实战(个人书屋) - 图9

本部分内容到这结束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues
JS小册 js.pingan8787.com
微信公众号 前端自习课

Angular6 项目实战(个人书屋) - 图10