Angular 踩坑之一:Service 的使用和作用范围

文中所用环境:

Angular-Cli: 12.0.3

Angular: 12.0.3

Node: 12.16.0

Npm: 6.13.4

文中所展示的例子是运行ng new [projectName] 命令后所生成的标准Angular demo

基础知识:

装饰器:

首先装饰器是什么?

从JS技术上来说它是一个高阶函数

高阶函数英文叫 Higher-order function。高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者返回它们。简单总结为高阶函数是一个接收函数作为参数或者将函数作为返回输出的函数。

看上去不太好理解,那我们就上代码:

function Driver(target: Function) {
    target.prototype.run = function() {
        console.log('我开动了这辆汽车');
    };
}

@Driver
class Car {
    constructor() {
        //
    }
}

const myCar = new Car();
myCar.run(); // console output: 我开动了这辆汽车

在上面的例子中Driver 接收了Car作为参数,在Car的原型上添加了一个方法。在经过Driver的装饰之后,Car的实例化对象就可以使用方法run了。

也可以直白的理解为:函数是JS中的高等公民,而处理这类高等公民的函数就叫高阶函数

Injectable 类装饰器的使用

在理解了高阶函数之后,我们再来看看Injectable

首先创建Service

import { Injectable } from '@angular/core';

@Injectable()
export class MyService {}

然后在app.module.ts中注册,并在Component中引入

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MyService } from './myService.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [MyService],
  bootstrap: [AppComponent]
})
export class AppModule { }

import { Component } from '@angular/core';
import { MyService } from './myService.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {
  constructor(private myService: MyService) {}
  …
}

使用ng serve启动项目

编译完成之后如下所示

((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  /* harmony export */   "MyService": () => (/* binding */ MyService)
  /* harmony export */ });
  /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ 7716);
  
  class MyService {
      constructor() { }
  }
  
  MyService.ɵfac = function MyService_Factory(t) { return new (t || MyService)(); };
  MyService.ɵprov = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineInjectable"]({ token: MyService, factory: MyService.ɵfac });
  

  /***/ })
MyService.ɵfac = function MyService_Factory(t) { return new (t || MyService)(); };
  MyService.ɵprov = /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineInjectable"]({ token: MyService, factory: MyService.ɵfac });

这一部分就是Injectable被编译之后的主要内容。实际上它将MyService作为一个单例(单例模式)存储在了你所注册的Module里面。在这个Module内部MyService只有一个实例,它可以保存属性,提供函数方法。也就是说Injectable的作用就是以单例模式对Service类进行改造,在所注册的Module上保存它的实例化对象,并允许以参数的形式传入Component,Directive和其它Service中进行操作。

当然如果你的Service中不需要入参也可以不使用Injectable

Service的使用和作用范围

继承上面的例子创建MyService,这一次我们在里面添加上一个name属性和set/get方法

import { Injectable } from '@angular/core';

@Injectable()
export class MyService {
  name = 'MyService';
  constructor() {}

  setName(name: string) {
    this.name = name;
  }

  getName() {
    console.log('My name is ' + this.name);
  }

在app.module.ts中注册它

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MyService } from './myService.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [MyService],
  bootstrap: [AppComponent]
})
export class AppModule { }

现在创建两个Component,注册并在AppComponent中使用它们

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ChangeNameComponent } from './components/changeName/changeName.component';
import { ConsoleNameComponent } from './components/consoleName/consoleName.component';
import { MyService } from './myService.service';

@NgModule({
  declarations: [
    AppComponent,
    ChangeNameComponent,
    ConsoleNameComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [MyService],
  bootstrap: [
    AppComponent,
    ChangeNameComponent,
    ConsoleNameComponent
  ]
})
export class AppModule { }

ChangeNameComponent

import { Component } from '@angular/core';
import { MyService } from '../../myService.service';

@Component({
    selector: 'change-name',
    template: `<button class="change-name" (click)="changeName()">Change Name</button>`,
    styleUrls: ['./changeName.component.less']
})

export class ChangeNameComponent {
    constructor(private myService: MyService) {
    }

    changeName() {
      this.myService.setName('ChangeNameComponent');
    }
}

ConsoleNameComponent 

import { Component } from '@angular/core';
import { MyService } from '../../myService.service';

@Component({
    selector: 'console-name',
    template: `<button class="console-name" (click)="consoleName()">Console Name</button>`,
    styleUrls: ['./consoleName.component.less']
})

export class ConsoleNameComponent {
    constructor(private myService: MyService) {
    }

    consoleName() {
      this.myService.getName();
    }
}

AppComponent View

<div class="container">
  <change-name></change-name>
  <console-name></console-name>
</div>

ng serve 运行项目可以看到 

 点击Console Name打印出了name,’MyService’

当点击了Change Name再点击Console Name时name就变为’ ChangeNameComponent’

Service的使用实际上很简单。常见的方式就是创建一个Service类,添加Injectable装饰器,然后再放入NgModule的providers属性中就能正常使用了。关键在于它的作用范围。

上面的例子证明两个Component所引用的MyService实际上是同一个实例化对象。我们在注册Service时需要将它放入NgModule的providers属性中。在该Module实例化的时候providers中的Service会依次被实例化并被存储进Module的作用域中。同样在这个Module中注册的 组件、指令、服务 都可以共享这个Service的实例化对象。

听上去有点像Redux对吧?如果你使用过Redux那么你应该会明白,使用Redux的一个问题就是要手动规范作用域。即规定key:value中的key来保证value的值不会相互污染。

比如说我想让上面例子中的两个组件都有自己的name,那么我就需要让数据被保存成这种形式:

{
changeNameComponent: {
	name: ‘changeNameComponent’
},
consoleNameComponent: {
	name: ‘consoleNameComponent’
}
}

Service的作用范围主要是由它所注册的作用域决定的

不同的Service注册方式,其作用域也不同

  1. NgModule providers: 可以用于当前Module的组件、指令、服务
  2. Component providers: 可以用于当前component和其子component
  3. Injectable 中的providedIn

说一下第3种注册方式:

@ Injectable({
	providedIn: 可选值 ‘root’ || Module
})

这种注册方式主要是用来替代第一种方式的,我们不需要在NgModule中写providers,而且在代码编译打包时,可以执行优化,可以自动移除所有没在应用中使用过的服务。Angular也是推荐使用这种方式的。但是这种方式不支持Component。

Service的作用范围,根据其选择的Component和Module的不同来划分作用域。它需要我们能合理的拆分模块或组件。Angular 一直提倡模块化和组件化,目的是更好的拆分和抽象项目的业务逻辑,降低模块与模块之间的耦合度。

附加内容:

在Service中保存属性值不是一个好的选择。尤其是多个组件公用一个Service时,极容易出现问题。例如同时发起的请求,异步函数被同时调用但是返回的顺序是无法预测的。那么每个组件是否能拿到自己对应的数据就也是不确定的了。如果设置多个属性存放不同的值又会增加维护的成本,并且有可能会与Component中的Data Model发生冲突。

推荐做法如下:

Service只提供函数方法,而不保存任何属性值

也就是说将Service当作是类似于lodash的工具类,而且Service里面的方法也尽量保证是纯函数

简单说一下纯函数:

对于一个函数来说,使用相同的输入始终会得到相同的输出,而且没有可观察到的副作用

简单来说能满足以下三个要求的就是纯函数:

  1. 传入相同的参数始终返回相同的值。不管调用该函数多少次,无论何时调用它。
  2. 函数中不直接使用全局变量(需要用到全局变量时可以将它作为参数传入)
  3. 函数中没有修改全局变量

另外,如果函数中有用到类似Math.random() 的也不算是纯函数,因为相同的参数返回的结果不相同

这种做法将Service变成了带有业务逻辑的工具类,所有的数据处理都会放入Service中。Component只负责存储数据以及View层的事件监听。每个Component有自己独特的业务逻辑,你可以将它放入同名的Service中。而相同的数据处理逻辑可以放入共享的Service中。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值