三。Angular -- TS+服务+TODOS案例再次升级

Angular的开发模型更接近于传统强类型语言的模式,加上官方内建的组件和类库比较完整,学习曲线要低一些。

Posted by Azr on January 15, 2019

Angular 是一个开发平台。它能帮你更轻松的构建Web 应用。Angular 集声明式模板、依赖注入、端到端工具和一些最佳实践于一身,为你解决开发方面的各种挑战。

使用TS

使用TS的好处

  • Angular的最佳推荐
  • 增强了项目的可维护性
  • 有利于多人协作的开发
  • 降低了开发人员的沟通成本

在Angula中应用

类型的注解

—为函数/变量添加约束

todo.component.ts

....
// 父组件完成数据添加操作
  addTodo(todoName) {
    // 类型注解
    let id: number

    if (this.todos.length === 0) {
      id = 1
    } else {
      id = this.todos[this.todos.length - 1].id + 1
    }
    .....
  }
....

let id后面添加类型注解,如果下面的id=1写为id=‘1’就会得到一个错误不能将类型‘1’分配给数字‘Number’,在开发期间就可以通过这种方式来添加约束。

如果这个变量是有值的,就不需要添加类型注解了。

接口

—对值所具有的结构进行类型检查

接口规定了结构,达到了访问统一。

方法一

todo.component.ts

...
// 创建接口
interface Todo{
  id: number,
  name: string,
  done: boolean
}
...
export class TodoComponent implements OnInit {
  .... 
  // 记得是对象模式 Todo后面要加[]
  todos: Todo[] = [
    {id: 1, name: '吃饭', done: true},
    {id: 2, name: '睡觉', done: false},
    {id: 4, name: '1吃饭', done: true},
    {id: 5, name: '2睡觉', done: false},
    {id: 6, name: '3吃饭', done: true},
    {id: 7, name: '4睡觉', done: false}
  ]
  ....
}

方法二

todo.component.ts

import { Component, OnInit, OnChanges } from '@angular/core';
...

export class TodoComponent implements OnInit,OnChanges {
  .......
  
  ngOnInit() {
  }

  ngOnChanges() {

  }
}

OnInit一个生命周期钩子,它会在 Angular 初始化完了该指令的所有数据绑定属性之后调用。 定义 ngOnInit() 方法可以处理所有附加的初始化任务。

OnChanges当 Angular(重新)设置数据绑定输入属性时响应。首次调用一定会发生在 ngOnInit() 之前。

Angular 会找到并调用像 ngOnInit() 这样的钩子方法,有没有接口无所谓。

泛型

—泛型类EventEmitter约定参数类型

todo-header.component.ts

......
//  使用了泛型  <String>
  add = new EventEmitter<String>()
  addTodo() {
    if (this.todoName.trim() === '') {
        return
      }
    // 参数 this.todoName 就只能使用string类型的
    this.add.emit(this.todoName)

    this.todoName = ''
  }
......
}

类成员修饰符

—public公共(默认),private私有

代码

todo.component.ts

import { Component, OnInit } from '@angular/core';

// 创建接口
interface Todo{
  id: number,
  name: string,
  done: boolean
}

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {

  constructor() { }

  todos: Todo[] = [
    {id: 1, name: '吃饭', done: true},
    {id: 2, name: '睡觉', done: false},
    {id: 4, name: '1吃饭', done: true},
    {id: 5, name: '2睡觉', done: false},
    {id: 6, name: '3吃饭', done: true},
    {id: 7, name: '4睡觉', done: false}
  ]
  // private todo: any

 addTodo(todoName: string) {
   // id
   // 类型注解
   let id: number
   if (this.todos.length === 0) {
     return 1
   } else {
     id = this.todos[this.todos.length - 1 ].id - 1
   }

   // 放入todos数组
   this.todos.push({
     id,
     name: todoName,
     done: false,
   })

 }

  delTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id)
  }

  changeTodo(id: number) {
    const curTodo = this.todos.find(todo => todo.id === id)
    curTodo.done = !curTodo.done
  }
  
  ngOnInit() {
  }
}

todo-header.component.ts

import { Component, OnInit, Output, EventEmitter, OnChanges } from '@angular/core';

@Component({
  selector: 'app-todo-header',
  templateUrl: './todo-header.component.html',
  styleUrls: ['./todo-header.component.css']
})
export class TodoHeaderComponent implements OnInit {

  constructor() { }

  // 任务名称
  todoName = ''
  // 类型注解--有bug,当空白添加的时候没有默认值会进行报错
  // todoName: string

  @Output()
  add = new EventEmitter<String>()

  addTodo() {
    if (this.todoName.trim() === '') {
        return
      }
    this.add.emit(this.todoName)
    this.todoName = ''
  }

  ngOnInit() {
  }
}

todo-list.component.ts

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

// 创建接口
interface Todo{
  id: number,
  name: string,
  done: boolean
}

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {

  constructor() { }

  @Input()
  todos: Todo[]

  @Output()
  del = new EventEmitter<number>()

  @Output()
  change = new EventEmitter<number>()

  trackByTodo(index: number,todo: Todo) {
    return todo.id
  }
    
  // 删除
  delTodo(e, id: number) {
    e.preventDefault()
    this.del.emit(id)
  }

  // 任务完成状态切换
  changeTodo(id: number) {
    this.change.emit(id)
  }

  ngOnInit() {
  }
}

服务

作用

  1. 组件应该只提供用于数据绑定的属性和方法
  2. 组件不应该定义任何诸如从服务器获取数据、验证用户输入等操作
  3. 应该把各种处理任务定义到可注入的服务中
  4. 服务的作用:处理业务逻辑,供组件使用
  5. 服务和组件的关系:组件是服务的消费者

概念

  1. 通过 @Injectable()装饰器 来表示一个服务
  2. 服务需要注册提供商才可以使用
  3. Angular通过依赖注入(DI)来为组件提供服务
  4. DI使得在使用服务时,只提供要使用的服务即可。不需要手动创建服务实例
  5. 推荐在 constructor 中提供组件中用到的服务

使用

创建一个服务,该命令在todos的文件夹下创建了一个todos的服务。

$ ng g s todos/todos

todos.service.ts

// 导入Injectable  告诉Angular这个文件是一个服务
import { Injectable } from '@angular/core';

@Injectable({
  //  是一个服务中的提供商
  providedIn: 'root'
})
export class TodosService {

  constructor() { }
    
  // 服务的方法
  todotest(){
    console.log('TodoTest')
  }
}

todo.component.ts

import { Component, OnInit } from '@angular/core';

// 导入服务
import { TodosService } from '../todos.service';
...
export class TodoComponent implements OnInit {

  // 告诉组件: 组件中需要使用这个服务
  constructor(private TodosService: TodosService) {  }
    
  addTodo(todoName: string) {
   // 调用服务
   this.TodosService.todotest()
   .....
  }
}

todo.component.ts中通过TodosService来进行调用服务的方法TodoTest

如果没有服务注册提供商会进行以下报错

Error: StaticInjectorError(AppModule)[TodoComponent -> TodosService]: 
  StaticInjectorError(Platform: core)[TodoComponent -> TodosService]: 
    NullInjectorError: No provider for TodosService!
    .....

注册提供商

  1. 通过 @InjectableprovidedIn: 'root' 注册为根级提供商

    app.component.ts

    import { Component } from '@angular/core';
       
    // 导入服务
    import {TodosService} from './todos/todos.service';
       
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      // 告诉跟组件: 组件中需要使用这个服务
      constructor(private TodosService: TodosService) { }
    }
       
    
  2. 通过 @NgModuleproviders: [] 注册为模块内可用的提供商

    todos.module.ts

    // 导入服务
    import { TodosService} from './todos.service';
       
    @NgModule({
    ...
      // 在模块中注册可用的提供商
      providers: TodosService
    })
    export class TodosModule { }
    

    这样仅仅是在模块中使用,具有惰性。根注入器并不知道这些模块。

  3. 通过 @Componentproviders: [] 注册为组件的提供商

    todos.component.ts

    import { Component, OnInit } from '@angular/core';
       
    // 导入服务
    import { TodosService } from '../todos.service';
       
    @Component({
      selector: 'app-todo',
      templateUrl: './todo.component.html',
      styleUrls: ['./todo.component.css']
      // 在组件中注册可用的提供商
      providers: TodosService
    })
    export class TodoComponent implements OnInit {
       
      // 告诉组件: 组件中需要使用这个服务
      constructor(private TodosService: TodosService) { }
      addTodo(todoName: string) {
       // 调用服务
       this.TodosService.todotest()
       .....
      }
    }
    

    在父子组件中,是可以使用的,但是在兄弟组件中就必须注册才可以使用。

TODOS使用服务

使用服务修改todos

  1. 把组件中的业务逻辑抽离到服务中
  2. 在组件中调用服务中对应的方法

数据

  1. 在组件todos.component.ts中导入服务

  2. constructor中告诉组件要使用服务

    不要在constructor中放入业务逻辑,只能去提供一些属性/服务

  3. 将数据抽离到服务中

  4. ngOnInit可以放入一些初始化的代码

  5. 记得在服务中添加数据的接口类型约束

    服务中提供的数据类型和组件中使用的是一样的

    抽离为单独的文件,进行导入

todos/todo/todos.component.ts

import { Component, OnInit } from '@angular/core';

// 导入服务
import { TodosService } from '../todos.service';

// 接口
import { Todo } from "../todo";

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})

export class TodoComponent implements OnInit {

  // 告诉组件: 组件中需要使用这个服务
  constructor(private TodosService: TodosService) { }
  todos
    
	.....
  
  // ngOnInit可以放入一些初始化的代码
  ngOnInit() {
    this.todos = this.TodosService.getTodos()
  }
}

todos/todos.service.ts

import { Injectable } from '@angular/core';
// 接口
import { Todo }  from './todo';

@Injectable({
  providedIn: 'root'
})
export class TodosService {

  constructor() { }

  // 提供数据
  getTodos() {
    const todos: Todo[] = [
      {id: 1, name: '1', done: true},
      {id: 2, name: '2', done: false}
    ]
    return todos
  }
}

todos/todo.ts

export interface Todo{
  id: number,
  name: string,
  done: boolean
}

添加任务

  1. 从组件中复制添加任务到服务中
  2. 服务中没有todos的数据,声明todos,在getTodos中添加初始化数据
  3. 组件直接调用服务中的方法

todos/todo/todos.component.ts

....
export class TodoComponent implements OnInit {

  // 告诉组件: 组件中需要使用这个服务
  constructor(private TodosService: TodosService) { }
  todos

  addTodo(todoName: string) {
    this.TodosService.addTodo(todoName)
  }
	....   
  ngOnInit() {
    this.todos = this.TodosService.getTodos()
  }
}

todos/todos.service.ts

import { Injectable } from '@angular/core';
// 接口
import { Todo }  from './todo';

@Injectable({
  providedIn: 'root'
})
export class TodosService {

  constructor() { }
  todos: Todo[]

  // 提供数据
  getTodos() {
    const todos: Todo[] = [
      {id: 1, name: '1', done: true},
      {id: 2, name: '2', done: false}
    ]
    // 添加一个初终数据
    this.todos = todos
    return todos
  }
  // 添加任务
  addTodo(todoName: string) {
    let id: number
    if (this.todos.length === 0) {
      return 1
    } else {
      id = this.todos[this.todos.length - 1 ].id - 1
    }

    this.todos.push({
      id,
      name: todoName,
      done: false,
    })

 }
}

删除和切换任务

  1. 先进行切换任务的处理,在进行删除任务的处理

  2. 从组件中复制切换任务到服务中

    todos/todo/todos.component.ts

    ....
    export class TodoComponent implements OnInit {
      ..
      changeTodo(id: number) {
        this.TodosService.changeTodo(id)
      }
    ...
    }
       
    

    todos/todos.service.ts

    ... 
    export class TodosService {
    ... 
     // 切换任务
     changeTodo(id: number) {
      const curTodo = this.todos.find(todo => todo.id === id)
      curTodo.done = !curTodo.done
     }
    }
       
    
  3. 从组件中复制删除任务到服务中,会发现无法进行删除

    在删除操作中,改变了this.todos的指向

  4. 更改删除任务的业务逻辑

    todos/todo/todos.component.ts

    ....
    export class TodoComponent implements OnInit {
      ..
      changeTodo(id: number) {
        this.TodosService.delTodo(id)
      }
    ...
    }
       
    

    todos/todos.service.ts

    ... 
    export class TodosService {
    ... 
      // 删除任务
     delTodo(id: number) {
      // this.todos = this.todos.filter(todo => todo.id !== id)
      const curIndex = this.todos.findIndex(todo => todo.id === id)
      this.todos.splice(curIndex,1)
     }
    }
       
    

完成

本文首次发布于 Azr的博客, 作者 @azrrrrr ,转载请保留原文链接.

原文链接: http://amor9.cn/2019/01/15/ng3/