第一个 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-edit11 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 层
<ahref="#"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"><imgsrc="{{ 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"><liclass="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"><aclass="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><inputtype="number"id="amount"class="form-control"#amountInput/></div></div><div class="row"><div class="col-xs-12"><div class="btn-toolbar"><buttonclass="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 基本一致,也就不多赘述了