第一个 Angular 项目 - 添加服务

第一个 Angular 项目 - 添加服务

这里主要用到的内容就是 [Angular 基础] - service 服务 提到的

前置项目在 第一个 Angular 项目 - 动态页面 这里查看

想要实现的功能是简化 shopping-listrecipe 之间的跨组件交流

回顾一下项目的结构:

bash 复制代码
❯ 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:

bash 复制代码
src/app/
├── services
│   ├── ingredient.service.ts
│   └── recipe.service.ts

一个用来管理所有的 ingredients------这部分是放在 shopping-list 中进行展示的,另一个就是管理所有的 recipes

ingredient service

实现代码如下:

typescript 复制代码
@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

typescript 复制代码
@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 层修改:

    html 复制代码
    <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 层修改

    typescript 复制代码
    @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 层修改如下

    html 复制代码
    <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 层修改如下

    typescript 复制代码
    @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 层

    html 复制代码
    <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 层

    typescript 复制代码
    @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();
      }
    }

    这里变化稍微有一点多,主要也是针对 activeRecipeonSelectedRecipe 的修改。

    前者的判断我在 model 写了一个 isEqual 的方法用来判断名字、数量、图片等是否一样,当然只用这个方法的话还是有可能会出现数据碰撞的,因此写案例的时候我尽量不会用同一个名字去命名 ingredient。基于这个前提下,那么就可以判断当前的 recipe 是不是被选中的 recipe,同时添加 active 这一类名做更好的提示

    使用 subscribe 也是基于同样的理由,需要捕获 recipe 的变动

    onSelectedRecipe 的变化倒是没有太多,同样会触发一个事件,不过这个事件现在保存在 recipeService 中

    目前的实现是整个 recipe 都共享一个 service,因此这里 emit 的事件,在整个 recipe 组件下,只要 subscribe 了,就只会是同一个事件

recipe-detail 组件的修改

  • V 层

    html 复制代码
    <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 层

    typescript 复制代码
    @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 层

    html 复制代码
    <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 层

    typescript 复制代码
    @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 层

    html 复制代码
    <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 层

    typescript 复制代码
    @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 基本一致,也就不多赘述了

相关推荐
come1123410 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志32 分钟前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘1 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js