美文网首页JS重温基础
【CuteJavaScript】Angular6入门项目(4.改

【CuteJavaScript】Angular6入门项目(4.改

作者: pingan8787 | 来源:发表于2019-02-23 11:12 被阅读9次

    本文目录

    本项目源码放在github

    六、改造组件

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

    1.添加历史记录组件

    • 创建HistoryComponent组件
    ng g component hostory
    

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

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

    2.添加增删改查功能

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

    • 创建HistoryService服务
    ng g service history
    

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

    // history.service.ts
    export class HistoryService {
        history: string[] = [];
        add(history: string){
            this.history.push(history);
        }
        clear(){
            this.history = [];
        }
    }
    
    • 使用HistoryService服务

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

    // books.service.ts
    import { HistoryService } from './history.service';
    constructor(
        private historyservice: HistoryService
    ) { }
    getBooks(): void{
        this.historyservice.add('请求书本数据')
        this.booksservice.getBookList()
            .subscribe(books => this.books = books);
    }
    

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

    // index.component.ts
    import { HistoryService } from '../history.service';
    constructor(
        private booksservice: BooksService,
        private historyservice: HistoryService
    ) { }
    getBooks(): void{
        this.historyservice.add('访问首页书本列表');
        this.booksservice.getBookList()
            .subscribe(books => this.books = books);
    }
    

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

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

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

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

    图片5-1

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

    // detail.component.ts
    import { HistoryService } from '../history.service';
    
    export class DetailComponent implements OnInit {
        constructor(
            private route: ActivatedRoute,
            private location: Location,
            private booksservice: BooksService,
            private historyservice: HistoryService
        ) { }
        //...
        getBooks(id: number): void {
            this.books = this.booksservice.getBook(id);
            this.historyservice.add(`查看书本${this.books.title},id为${this.books.id}`);
            console.log(this.books)
        }
    }
    
    图片5-2

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

    七、HTTP改造

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

    1.引入HTTP

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

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

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

    import { HttpClientModule } from '@angular/common/http';
    @NgModule({
        //...
        imports: [
            BrowserModule,
            AppRoutingModule,
            HttpClientModule
        ],
        //...
    })
    

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

    通过下面命令来安装:

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

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

    // app.module.ts
    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
    import { InMemoryDataService }  from './in-memory-data.service';
    @NgModule({
        // ...
        imports: [
            // ...
            HttpClientInMemoryWebApiModule.forRoot(
            InMemoryDataService, {dataEncapsulation:false}
            )
        ],
        // ...
    })
    export class AppModule { }
    

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

    然后我们要创建InMemoryDataService类:

    ng g service InMemoryData
    

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

    // in-memory-data.service.ts
    import { Injectable } from '@angular/core';
    import { InMemoryDbService } from 'angular-in-memory-web-api';
    import { Books } from './books';
    @Injectable({
      providedIn: 'root'
    })
    export class InMemoryDataService implements InMemoryDbService {
      createDb(){
        const books = [
          {
              id: 1, 
              url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg',
              title: '像火焰像灰烬',
              author: '程姬',
          },
          // 省略其他9条数据
        ];
        return {books};
      }
      constructor() { }
    }
    

    这里先总结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并改造:

    // books.service.ts
    import { HttpClient, HttpHeaders} from '@angular/common/http';
    // ...
    export class BooksService {
        constructor(
            private historyservice: HistoryService,
            private http: HttpClient
        ) { }
        private log(histories: string){
            this.historyservice.add(`正在执行:${histories}`)
        }
        private booksUrl = 'api/books'; // 提供一个API供调用
        // ...
    }
    

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

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

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

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

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

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

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

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

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

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

    3.通过HTTP修改数据

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

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

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

    // app.module.ts
    // ...
    import { FormsModule } from '@angular/forms'; 
    @NgModule({
        // ...
        imports: [
            // ...
            FormsModule
        ],
        // ...
    })
    

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

    // detail.component.ts
    save(): void {
        this.historyservice.updateBooks(this.books)
            .subscribe(() => this.goBack());
    }
    goBack(): void {
        this.location.back();
    }
    

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

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

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

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

    4.通过HTTP增加数据

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

    ng g component add
    

    添加路由:

    // app-routing.module.ts
    // ...
    import { AddComponent } from './add/add.component';
    
    const routes: Routes = [
      { path: '', redirectTo:'/index', pathMatch:'full' },
      { path: 'index', component: IndexComponent},
      { path: 'detail/:id', component: DetailComponent},
      { path: 'add', component: AddComponent},
    ]
    

    添加路由入口:

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

    编辑添加书本的页面:

    <!-- add.component.html -->
    <div class="add">
        <h2>添加书本:</h2>
        <label>标题:
            <input [(ngModel)]="books.title" placeholder="请输入标题">
        </label>
        <label>作者:
            <input [(ngModel)]="books.author" placeholder="请输入作者">
        </label>
        <label>书本id:
            <input [(ngModel)]="books.id" placeholder="请输入书本id">
        </label>
        <label>封面地址:
            <input [(ngModel)]="books.url" placeholder="请输入封面地址">
        </label>
        <div><button (click)="add(books)">添加</button></div>
    </div>
    

    初始化添加书本的数据:

    // add.component.ts
    // ...
    import { Books } from '../books';
    import { BooksService } from '../books.service';
    import { HistoryService } from '../history.service';
    import { Location } from '@angular/common';
    export class AddComponent implements OnInit {
        books: Books = {
            id: 0,
            url: '',
            title: '',
            author: ''
        }
        constructor(
            private location: Location,
            private booksservice: BooksService,
            private historyservice: HistoryService
        ) { }
        ngOnInit() {}
        add(books: Books): void{
            books.title = books.title.trim();
            books.author = books.author.trim();
            this.booksservice.addBooks(books)
            .subscribe( book => {
                this.historyservice.add(`新增书本${books.title},id为${books.id}`);
                this.location.back();
            });
        }
    }
    

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

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

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

    图片5-3

    5.通过HTTP删除数据

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

    <!-- books.component.html -->
    <!-- 省略一些代码 -->
    <span class="delete" (click)="delete(list)">X</span>
    
    // books.component.ts
    import { BooksService } from '../books.service';
    export class BooksComponent implements OnInit {
      @Input() list: Books;
      constructor(
        private booksservice: BooksService
      ) { }
      // ...
      delete(books: Books): void {
        this.booksservice.deleteBooks(books)
          .subscribe();
      }
    }
    

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

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

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

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

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

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

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

    import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
    export class BooksComponent implements OnInit {
        // ... 
        @Output()
        change = new EventEmitter()
        // ... 
        delete(books: Books): void {
            this.booksservice.deleteBooks(books)
            .subscribe(()=>{
                this.change.emit(books);
            });
        }
    }
    

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

    6.通过HTTP查找数据

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

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

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

    八、结语

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

    图片结果

    本部分内容到这结束

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

    相关文章

      网友评论

        本文标题:【CuteJavaScript】Angular6入门项目(4.改

        本文链接:https://www.haomeiwen.com/subject/spguyqtx.html