第一个 Angular 项目 - 添加服务
这里主要用到的内容就是 [Angular 基础] - service 服务 提到的
前置项目在 第一个 Angular 项目 - 动态页面 这里查看
想要实现的功能是简化 shopping-list
和 recipe
之间的跨组件交流
回顾一下项目的结构:
❯ tree src/app/
src/app/
├── directives
├── header
├── recipes
│ ├── recipe-detail
│ ├── recipe-list
│ │ ├── recipe-item
│ ├── recipe.model.ts
├── shared
│ └── ingredient.model.ts
└── shopping-list
├── shopping-edit
11 directories, 31 files
层级结构相对来说还是有一点点复杂的,所以如果在 app
层构建一个对应的变量和事件再一层层往下传,无疑是一件非常麻烦的事情(尤其 V 层和 VM 层都要进行事件传输的对应变化),而使用 service 就能相对而言比较简单的解决这个问题
创建新的 service
这里主要会创建两个 services:
src/app/
├── services
│ ├── ingredient.service.ts
│ └── recipe.service.ts
一个用来管理所有的 ingredients——这部分是放在 shopping-list
中进行展示的,另一个就是管理所有的 recipes
ingredient service
实现代码如下:
@Injectable({
providedIn: 'root',
})
export class IngredientService {
ingredientChanged = new EventEmitter<Ingredient[]>();
private ingredientList: Ingredient[] = [
new Ingredient('Apples', 5),
new Ingredient('Tomatoes', 10),
];
constructor() {}
get ingredients() {
return this.ingredientList.slice();
}
addIngredient(Ingredient: Ingredient) {
this.ingredientList.push(Ingredient);
this.ingredientChanged.emit(this.ingredients);
}
addIngredients(ingredients: Ingredient[]) {
this.ingredientList.push(...ingredients);
this.ingredientChanged.emit(this.ingredients);
}
}
代码分析如下:
-
Injectable
这里使用
providedIn: 'root'
是因为我想让所有的组件共享一个 service,这样可以满足当 ingredient 页面修改对应的食材,并且将其发送到shopping-list
的时候,数据可以进行同步渲染 -
ingredientChanged
这是一个 event emitter,主要的目的就是让其他的组件可以 subscribe 到事件的变更
subscribe 是之前的 service 笔记中没提到的内容,这里暂时不会细舅,不过会放一下用法
-
get ingredients()
一个语法糖,这里的
slice
会创造一个 shallow copy,防止意外对数组进行修改也可以用 lodash 的
cloneDeep
,或者单独创建一个函数去进行深拷贝 -
add 函数
向数组中添加元素,并向外发送数据变更的信号
recipe service
@Injectable()
export class RecipeService {
private recipeList: Recipe[] = [
new Recipe('Recipe 1', 'Description 1', 'http://picsum.photos/200/200', [
new Ingredient('Bread', 5),
new Ingredient('Ginger', 10),
]),
new Recipe('Recipe 2', 'Description 2', 'http://picsum.photos/200/200', [
new Ingredient('Chicken', 10),
new Ingredient('Bacon', 5),
]),
];
private currRecipe: Recipe;
recipeSelected = new EventEmitter<Recipe>();
get recipes() {
return this.recipeList.slice();
}
get selectedRecipe() {
return this.currRecipe;
}
}
这里主要讲一下 Injectable
,因为 recipe service 的部分应该被限制在 recipe
这个组件下,所以这里不会采用 singleton 的方式实现
其余的实现基本和上面一样
修改 recipe
这里依旧是具体业务具体分析:
-
recipe
这里需要获取
activeRecipe
+ngIf
去渲染recipe-detail
部分的内容,如:没有选中 recipe 选中了 recipe -
recipe-detail
这里需要
activeRecipe
去渲染对应的数据,如上图 -
recipe-list
这里需要
recipes
去完成循环,渲染对应的recipe-item
-
recipe-item
这里需要
activeRecipe
完成对active
这个 class 的添加
recipe 组件的修改
-
V 层修改:
<div class="row"> <div class="col-md-5"> <app-recipe-list></app-recipe-list> </div> <div class="col-md-7"> <app-recipe-detail [activeRecipe]="activeRecipe" *ngIf="activeRecipe; else noActiveRecipe" ></app-recipe-detail> <ng-template #noActiveRecipe> <p>Please select a recipe to view the detailed information</p> </ng-template> </div> </div>
-
VM 层修改
@Component({ selector: 'app-recipes', templateUrl: './recipes.component.html', providers: [RecipeService], }) export class RecipesComponent implements OnInit, OnDestroy { activeRecipe: Recipe; constructor(private recipeService: RecipeService) {} ngOnInit() { this.recipeService.recipeSelected.subscribe((recipe: Recipe) => { this.activeRecipe = recipe; }); } ngOnDestroy(): void { this.recipeService.recipeSelected.unsubscribe(); } }
这里主要是对 V 层进行了一些修改,减少了一些数据绑定。大多数的用法这里都是之前在 service 的笔记中提到的,除了这个 subscribe
的使用
简单的说,在 subscribe 之后,每一次 event 触发后,在这个 subscription 里,它都可以获取 event 中传来的信息,并进行对应的更新操作
recipe-list 组件的修改
-
V 层修改如下
<div class="row"> <div class="col-xs-12"> <button class="btn btn-success">New Recipe</button> </div> </div> <hr /> <div class="row"> <div class="col-xs-12"> <app-recipe-item *ngFor="let recipe of recipes" [recipe]="recipe" ></app-recipe-item> </div> </div>
-
VM 层修改如下
@Component({ selector: 'app-recipe-list', templateUrl: './recipe-list.component.html', styleUrl: './recipe-list.component.css', }) export class RecipeListComponent implements OnInit { recipes: Recipe[]; constructor(private recipeService: RecipeService) {} ngOnInit() { this.recipes = this.recipeService.recipes; } }
这里主要就是获取数据的方式变了,也不需要向下传递 @Input
,向上触发 @Output
了
reccipe-item 组件的修改
-
V 层
<a href="#" class="list-group-item clearfix" (click)="onSelectedRecipe()" [ngClass]="{ active: isActiveRecipe }" > <div class="pull-left"> <h4 class="list-group-item-heading">{{ recipe.name }}</h4> <p class="list-group-item-text">{{ recipe.description }}</p> </div> <span class="pull-right"> <img [src]="recipe.imagePath" [alt]="recipe.name" class="image-responsive" style="max-height: 50px" /> </span> </a>
这里做的另外一个修改就是把
a
标签移到了 list-item 去处理,这样语义化相对更好一些 -
VM 层
@Component({ selector: 'app-recipe-item', templateUrl: './recipe-item.component.html', styleUrl: './recipe-item.component.css', }) export class RecipeItemComponent implements OnInit, OnDestroy { @Input() recipe: Recipe; isActiveRecipe = false; constructor(private recipeService: RecipeService) {} ngOnInit() { this.recipeService.recipeSelected.subscribe((recipe: Recipe) => { this.isActiveRecipe = recipe.isEqual(this.recipe); }); } onSelectedRecipe() { this.recipeService.recipeSelected.emit(this.recipe); } ngOnDestroy(): void { this.recipeService.recipeSelected.unsubscribe(); } }
这里变化稍微有一点多,主要也是针对
activeRecipe
和onSelectedRecipe
的修改。前者的判断我在 model 写了一个
isEqual
的方法用来判断名字、数量、图片等是否一样,当然只用这个方法的话还是有可能会出现数据碰撞的,因此写案例的时候我尽量不会用同一个名字去命名 ingredient。基于这个前提下,那么就可以判断当前的 recipe 是不是被选中的 recipe,同时添加active
这一类名做更好的提示使用
subscribe
也是基于同样的理由,需要捕获 recipe 的变动onSelectedRecipe
的变化倒是没有太多,同样会触发一个事件,不过这个事件现在保存在 recipeService 中目前的实现是整个 recipe 都共享一个 service,因此这里 emit 的事件,在整个 recipe 组件下,只要 subscribe 了,就只会是同一个事件
recipe-detail 组件的修改
-
V 层
<div class="row"> <div class="col-xs-12"> <img src="{{ activeRecipe.imagePath }}" alt=" {{ activeRecipe.name }} " class="img-responsive" /> </div> </div> <div class="row"> <div class="col-xs-12"> <h1>{{ activeRecipe.name }}</h1> </div> </div> <div class="row"> <div class="col-xs-12"> <div class="btn-group" appDropdown> <button type="button" class="btn btn-primary dropdown-toggle"> Manage Recipe <span class="caret"></span> </button> <ul class="dropdown-menu"> <li> <a href="#" (click)="onAddToShoppingList()">To Shopping List</a> </li> <li><a href="#">Edit Recipe</a></li> <li><a href="#">Delete Recipe</a></li> </ul> </div> </div> </div> <div class="row"> <div class="col-xs-12">{{ activeRecipe.description }}</div> </div> <div class="row"> <div class="col-xs-12"> <ul class="list-group"> <li class="list-group-item" *ngFor="let ingredient of activeRecipe.ingredients" > {{ ingredient.name }} - {{ ingredient.amount }} </li> </ul> </div> </div>
-
VM 层
@Component({ selector: 'app-recipe-detail', templateUrl: './recipe-detail.component.html', styleUrl: './recipe-detail.component.css', }) export class RecipeDetailComponent { @Input() activeRecipe: Recipe; constructor(private ingredientService: IngredientService) {} onAddToShoppingList() { this.ingredientService.addIngredients(this.activeRecipe.ingredients); } }
这里通过调用 ingredient service 将当前 recipe 中的 ingredient 送到 shopping-list 的 view 下,效果如下:
这里没有做 unique key 的检查,而且实现是通过 Array.push
去做的,因此只会无限增加,而不是更新已有的元素。不过大致可以看到这个跨组件的交流是怎么实现的
修改 shopping-list
这里的实现和 recipe 差不多,就只贴代码了
shopping-list 组件的修改
-
V 层
<div class="row"> <div class="col-xs-10"> <app-shopping-edit></app-shopping-edit> <hr /> <ul class="list-group"> <a class="list-group-item" style="cursor: pointer" *ngFor="let ingredient of ingredients" > {{ ingredient.name }} ({{ ingredient.amount }}) </a> </ul> </div> </div>
-
VM 层
@Component({ selector: 'app-shopping-list', templateUrl: './shopping-list.component.html', styleUrl: './shopping-list.component.css', }) export class ShoppingListComponent implements OnInit, OnDestroy { ingredients: Ingredient[] = []; constructor(private ingredientService: IngredientService) {} ngOnInit(): void { this.ingredients = this.ingredientService.ingredients; this.ingredientService.ingredientChanged.subscribe( (ingredients: Ingredient[]) => { this.ingredients = ingredients; } ); } ngOnDestroy(): void { this.ingredientService.ingredientChanged.unsubscribe(); } }
同样也是一个 subscription 的实现去动态监听 ingredients
的变化
shopping-edit 组件的修改
-
V 层
<div class="row"> <div class="col-xs-12"> <form> <div class="row"> <div class="col-sm-5 form-group"> <label for="name">Name</label> <input type="text" id="name" class="form-control" #nameInput /> </div> <div class="col-sm-2 form-group"> <label for="amount">Amount</label> <input type="number" id="amount" class="form-control" #amountInput /> </div> </div> <div class="row"> <div class="col-xs-12"> <div class="btn-toolbar"> <button class="btn btn-success mr-2" type="submit" (click)="onAddIngredient(nameInput)" > Add </button> <button class="btn btn-danger mr-2" type="button">Delete</button> <button class="btn btn-primary" type="button">Edit</button> </div> </div> </div> </form> </div> </div>
这里添加了一个按钮的功能,实现添加 ingredient
-
VM 层
@Component({ selector: 'app-shopping-edit', templateUrl: './shopping-edit.component.html', styleUrl: './shopping-edit.component.css', }) export class ShoppingEditComponent { @ViewChild('amountInput', { static: true }) amountInput: ElementRef; constructor(private ingredientService: IngredientService) {} onAddIngredient(nameInput: HTMLInputElement) { this.ingredientService.addIngredient( new Ingredient(nameInput.value, this.amountInput.nativeElement.value) ); } }
这里的
onAddIngredient
实现方式和添加整个 list 基本一致,也就不多赘述了