第一個 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 基本一致,也就不多贅述了