+6

Angular Bài 5 - Services và Dependency Injection

1. Services và vai trò của chúng trong Angular

1.1. Services là gì?

Services, là một khái niệm không thể thiếu khi nói về Angular. "Ồ, vậy nhưng Services là gì mà lại quan trọng đến thế?" - các bạn có thể thắc mắc. Đơn giản thôi, Services là những class có mục đích đặc biệt, chức năng chủ yếu là để thực hiện các nhiệm vụ cụ thể và có thể tái sử dụng trong toàn bộ ứng dụng.

Ví dụ, nếu mình có một Service tên là LoggerService, thì nhiệm vụ của nó có thể là ghi log ra console hoặc gửi lên server.

1.2. Vì sao Services quan trọng?

Đơn giản mà nói, Services giúp chúng ta giữ cho mã nguồn (code) gọn gàng, dễ bảo dưỡng và có thể tái sử dụng. Thay vì phải viết đi viết lại cùng một đoạn mã ở nhiều chỗ khác nhau, chúng ta có thể gói gọn nó vào trong một Service và tái sử dụng mỗi khi cần. Còn gì tuyệt vời hơn khi bạn chỉ cần viết một lần và sử dụng mãi mãi chứ! (Mà mãi mãi là bao lâu nhỉ?)

Vậy làm thế nào để tạo một Service trong Angular? Để giải đáp thắc mắc đó, mình sẽ đưa ra ví dụ về việc tạo một Service đơn giản.

1.3. Tạo một Service trong Angular

Để tạo một Service, trước hết, bạn cần import Injectable từ @angular/core. Injectable là một decorator dùng để đánh dấu class như là một class có thể inject (tiêm) vào các class khác.

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {

  log(message: string) {
    console.log('LoggerService: ' + message);
  }
}

Trong ví dụ trên, mình tạo một LoggerService đơn giản với phương thức log để ghi log ra console. Phần providedIn: 'root' trong decorator Injectable nghĩa là Service này có thể sử dụng ở bất kỳ đâu trong ứng dụng.

Để sử dụng LoggerService, bạn chỉ cần inject nó vào constructor của component hoặc Service khác.

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angular-services';

  constructor(private loggerService: LoggerService) {
    this.loggerService.log('Hello from AppComponent!');
  }
}

Trong ví dụ trên, mình đã inject LoggerService vào AppComponent và sử dụng phương thức log để ghi một đoạn text. Khi bạn chạy ứng dụng, bạn sẽ thấy đoạn text "LoggerService: Hello from AppComponent!" được ghi ra console.

Vậy là các bạn đã hiểu cơ bản về Services và cách tạo Services trong Angular rồi nhé. Nhưng câu chuyện chưa dừng ở đây, bây giờ mình sẽ đưa các bạn đi sâu hơn vào Dependency Injection - kỹ thuật mà Angular sử dụng để cung cấp Services cho các components và Services khác.

2. Dependency Injection (DI) và cách hoạt động

2.1. Dependency Injection là gì?

Dependency Injection (DI), hay còn gọi là "Tiêm phụ thuộc", là một kỹ thuật trong lập trình cho phép ta truyền một đối tượng (thường là một đối tượng Service) vào một đối tượng khác. "Tiêm" ở đây không phải là tiêm chích y tế đâu nhé, mà là việc "tiêm" một Service vào một class thông qua constructor.

"Vậy mà cũng có kỹ thuật à?" - Có chứ, nhưng các bạn đừng lo, DI không hề phức tạp như tưởng tượng đâu. Đơn giản thôi, DI giúp chúng ta giảm sự phụ thuộc giữa các class, giúp code dễ hiểu hơn, dễ kiểm tra (test) hơn.

2.2. Dependency Injection trong Angular

Angular cung cấp một hệ thống DI mạnh mẽ và linh hoạt, giúp chúng ta dễ dàng quản lý các dependencies (sự phụ thuộc) giữa các Services và components.

Để inject một Service vào component, chúng ta chỉ cần thêm nó vào constructor của component, giống như ví dụ ở phần trên. Angular sẽ tự động tạo một instance của Service và "tiêm" nó vào component.

2.3. Hierarchical Injector

Angular DI sử dụng một kỹ thuật gọi là "Hierarchical Injector". Cái này nghe có vẻ khó hiểu, nhưng thực ra cũng đơn giản lắm.

Tưởng tượng Angular ứng dụng của bạn như là một cây phả hệ, với module gốc (root module) ở trên cùng và các component, directive, pipe và Service như là các nhánh và lá cây. Angular tạo ra một injector cho mỗi nhánh, và injector này sẽ "thừa hưởng" các provider từ injector của nhánh cha.

Điều này có nghĩa là, nếu bạn provide một Service ở root module (thông qua providedIn: 'root'), thì tất cả các component và Service trong ứng dụng đều có thể sử dụng Service đó.

Tuy nhiên, nếu bạn provide Service ở một component cụ thể, thì chỉ chính component đó và các component con của nó mới có thể sử dụng Service đó. Đây là cách chúng ta có thể giới hạn phạm vi sử dụng của một Service.

3. Sử dụng Dependency Injection để inject các Services

Mình đã giới thiệu về DI và cách nó hoạt động, bây giờ mình sẽ đưa ra một số ví dụ về cách sử dụng DI để inject các Services.

3.1. Inject một Service vào một Service khác

Không chỉ có thể inject Services vào components, chúng ta còn có thể inject Services vào các Services khác. Điều này rất hữu ích khi bạn muốn chia nhỏ một Service lớn thành nhiều Services nhỏ hơn, mỗi Service thực hiện một nhiệm vụ cụ thể.

Giả sử mình có một UserService cần sử dụng LoggerService để ghi log. Dưới đây là cách mình sẽ thực hiện:

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private loggerService: LoggerService) {}

  getUser() {
    // Get the user from the server
    const user = { name: 'John Doe', age: 30 };

    this.loggerService.log('Got user: ' + JSON.stringify(user));
    return user;
  }
}

Trong ví dụ này, mình đã inject LoggerService vào UserService và sử dụng nó để ghi log khi lấy dữ liệu người dùng.

3.2. Giải thích kỹ hơn về cú pháp Injection

Trong Angular, khi chúng ta viết constructor(private loggerService: LoggerService) {} trong một class, chúng ta đang thực hiện hai việc cùng một lúc:

  1. Khai báo một thuộc tính loggerService trong class đó. Thuộc tính này có kiểu LoggerService và có phạm vi truy cập là private, nghĩa là nó chỉ có thể truy cập trong class đó.

  2. Yêu cầu Angular Dependency Injection system cung cấp một instance của LoggerService và gán vào thuộc tính loggerService.

Đây là cách viết ngắn gọn (syntactic sugar) của TypeScript, giúp chúng ta tiết kiệm thời gian và làm cho code dễ đọc hơn. Nếu viết đầy đủ, cú pháp trên sẽ trở thành:

private loggerService: LoggerService;

constructor(loggerService: LoggerService) {
  this.loggerService = loggerService;
}

Trong ví dụ này, mình đã tách cú pháp trên thành hai bước: một bước khai báo thuộc tính, và một bước gán giá trị cho thuộc tính trong constructor.

3.3 Bonus 1 tý cho những bạn chưa hiểu về DI (Dependency Injection)

Hãy xem qua ví dụ bên dưới

class UserRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

class EmployeeRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

const userRepository = new UserRepository();
const employeeRepository = new EmployeeRepository();

Các bạn thấy trong 2 class repository ở trên mình thực hiện new Database() tận 2 lận. Vậy suy ra nếu có 100 chỗ dùng mình new Database() 100 lần -> mỗi class ở trên đều phụ thuộc vào class Database. Trong thực tế việc new 1 class có khi ko đơn giản chỉ là gọi new Database() mà còn phải truyển vào một đống thứ new Database(truyền vào rất nhiều arg ở đây: chẳng hạn như connectstring...bala bala). Mỗi class lại có một nhiệm vụ riêng của nó, thế mà nó còn phải đi lo tạo những class phụ thuộc thì khá là mệt mỏi. Đây là lúc DI phát huy sức mạnh. Nếu app dụng DI thì code trên sẽ trông như sau:

class UserRepository {
  // Cách inject ngắn gọn được Typescript hỗ trợ
  constructor(private db: Database) {}
}

class EmployeeRepository {
  // Viết đầy đủ nó sẽ là thế này
  private db: Database;
  constructor(db: Database) {
    this.db = db;
  }
}

// mình sẽ new 1 instance của class Database này bên ngoài
const root = {
  db: new Database();
}
const userRepository = new UserRepository(root.db);
const employeeRepository = new EmployeeRepository(root.db);

Như mình đã giải thích ở trên khi chúng ta decorator một class bằng @Injectable({providedIn: 'root'}) có nghĩa là minh đang ra lệnh cho: "root hãy tạo 1 instance cho class này và inject vào các class sẽ sử dụng nó...". root ở đây có thể chỉ đơn giản là một object như ví dụ trên và được chúng ta mô tả nó trong file module. (Về DI thì giải thích tới đây thôi ko các bạn loạn mất 😄 hẹn lần sau nói tiếp về cái này)

Đến đây thì các bạn đã hiểu cơ bản về Services và Dependency Injection trong Angular rồi nhé. Nhưng chưa hết, còn một cái rất thú vị đang chờ đón chúng ta, đó là RxJS và cách chúng ta có thể sử dụng nó cùng với Services và DI.

4. RxJS và ứng dụng trong Services

4.1. RxJS là gì?

RxJS (Reactive Extensions for JavaScript) là một thư viện dùng để lập trình reactive bằng JavaScript. Nó giúp chúng ta xử lý các sự kiện (events), các phép toán bất đồng bộ và các luồng dữ liệu (streams) một cách dễ dàng.

RxJS đưa ra khái niệm về Observable, giúp chúng ta xử lý các hoạt động bất đồng bộ một cách dễ dàng và linh hoạt hơn so với việc sử dụng callback hoặc Promise.

Lý do mình luôn nhắc lại định nghĩa về RxJS mỗi khi nói về nó là vì RxJS có thể hơi phức tạp khi mới học nên nhắc đi nhắc lại định nghĩa cũng là một cách hay.

4.2. Sử dụng RxJS trong Services

Trong Angular, một trong những cách chúng ta sử dụng RxJS phổ biến nhất là trong Services để xử lý HTTP requests.

Ví dụ: giả sử chúng ta có một UserService cần lấy dữ liệu người dùng từ server. Mình sẽ sử dụng HttpClient (một Service được Angular cung cấp) để gửi HTTP request, và HttpClient sẽ trả về một Observable. Mình sẽ sử dụng phương thức subscribe của Observable để xử lý dữ liệu khi nó sẵn sàng.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private http: HttpClient) {}

  getUser(): Observable<any> {
    return this.http.get('/api/users/1');
  }
}

Trong ví dụ này, mình đã inject HttpClient vào UserService và sử dụng nó để gửi HTTP GET request đến server. Hàm getUser trả về một Observable, và mình có thể subscribe Observable này ở component để lấy dữ liệu người dùng khi nó sẵn sàng.

4.3. Ví dụ khác

Để làm rõ hơn về việc sử dụng RxJS trong Services, mình sẽ đưa ra một ví dụ mô phỏng việc tải danh sách người dùng từ server, với giả định server có hỗ trợ phân trang.

Đầu tiên, mình sẽ tạo một UserService sử dụng HttpClient để tải dữ liệu từ server:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, of } from 'rxjs';
import { catchError, tap, switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private pageNumber = new Subject<number>();
  users$ = this.pageNumber.pipe(
    switchMap(page => this.http.get(`/api/users?page=${page}`).pipe(
      catchError(error => {
        console.error('Get users failed', error);
        return of({ data: [] });
      })
    )),
    tap(users => console.log('Got users:', users))
  );

  constructor(private http: HttpClient) {}

  loadUsers(page: number) {
    this.pageNumber.next(page);
  }
}

Trong UserService, mình sử dụng một Subject để quản lý số trang hiện tại, và một users$ Observable để quản lý việc tải dữ liệu từ server. Mỗi khi số trang thay đổi (thông qua hàm loadUsers), users$ sẽ tự động tải lại dữ liệu.

Chú ý đến việc mình sử dụng switchMap để chuyển đổi từ số trang sang Observable của dữ liệu người dùng, và tap để log dữ liệu ra console. Đây là hai trong số những operator quan trọng của RxJS, giúp chúng ta kiểm soát và xử lý các tác vụ bất đồng bộ một cách linh hoạt. (switchMap, tap là gì? Về RxJS mình sẽ có 1 series riêng vì RxJS có thể hơi phức tạp khi mới học.)

Giờ đây, mình có thể sử dụng UserService trong một component để hiển thị danh sách người dùng:

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users$ | async">
      {{user.name}}
    </div>
    <button (click)="loadMore()">Load more</button>
  `,
  styles: [`
    div {
      margin-bottom: 10px;
    }
    button {
      display: block;
      margin-top: 20px;
    }
  `]
})
export class UserListComponent implements OnInit {
  users$ = this.userService.users$;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.loadUsers(1);
  }

  loadMore() {
    this.userService.loadUsers(2); // Let's assume the next page is 2 for simplicity
  }
}

Trong UserListComponent, mình sử dụng async pipe để đăng ký lắng nghe users$ và tự động cập nhật giao diện mỗi khi dữ liệu thay đổi. Khi người dùng nhấn nút "Load more", mình sẽ tải thêm dữ liệu từ trang tiếp theo.

Chỉ với vài dòng code, nhờ vào RxJS, mình đã tạo ra một ứng dụng có khả năng tải dữ liệu từ server một cách linh hoạt và bền bỉ. Đây chỉ là một trong số vô số các tình huống mà RxJS có thể giúp chúng ta giải quyết một cách dễ dàng và hiệu quả.

5. Kết luận

Các bạn đã cùng mình tìm hiểu về Services và Dependency Injection trong Angular, và cách chúng ta có thể sử dụng RxJS để xử lý các tác vụ bất đồng bộ trong Services. Chắc chắn rằng sau bài viết này, các bạn đã hiểu rõ hơn về cách Angular hoạt động, và đã sẵn sàng để đi sâu hơn vào thế giới Angular.

Nhớ là lập trình không chỉ là công việc, mà còn là niềm vui và niềm đam mê nữa nhé! Hãy luôn giữ vững niềm đam mê và khát khao học hỏi để trở thành lập trình viên giỏi.

Câu hỏi ôn tập

  1. Cần phải tạo Service mới cho mỗi chức năng không? Không hẳn, tùy vào cách bạn tổ chức code của mình. Một số trường hợp, bạn có thể tạo một Service chung để xử lý các chức năng liên quan đến nhau.

  2. Tôi có thể sử dụng DI để inject các class không phải Service không? Chủ yếu, DI trong Angular được sử dụng để inject Services. Tuy nhiên, bạn cũng có thể inject các class khác, miễn là chúng đã được đăng ký với Angular DI system.

  3. RxJS có phức tạp không? Tôi có thể dùng Promise thay vì Observable không? RxJS có thể hơi phức tạp khi mới học, nhưng nó mang lại rất nhiều lợi ích, đặc biệt là khi xử lý các tác vụ bất đồng bộ. Tuy nhiên, nếu bạn thấy khó khăn, có thể sử dụng Promise. Nhưng hãy nhớ rằng Promise không mạnh mẽ bằng Observable.

  4. Tôi có thể inject một Service vào một pipe hoặc directive không? Câu trả lời là có. Bạn có thể inject Services vào pipes, directives, và hầu như bất kỳ class nào trong Angular.

  5. Tôi nên provide Service ở đâu? Đa số trường hợp, bạn nên provide Service ở root level (providedIn: 'root'). Tuy nhiên, nếu bạn muốn giới hạn phạm vi sử dụng của Service, bạn có thể provide nó ở một component cụ thể.


English Version

1. Services and their role in Angular

1.1. What are Services?

Services are an essential concept in Angular. "Oh, but what are Services and why are they important?" - you might wonder. It's simple, really. Services are special-purpose classes whose main function is to perform specific tasks and be reusable throughout the entire application.

For example, if I have a Service named LoggerService, its task could be logging messages to the console or sending them to a server.

1.2. Why are Services important?

Simply put, Services help us keep our source code clean, maintainable, and reusable. Instead of writing the same piece of code in multiple places, we can encapsulate it within a Service and reuse it whenever needed. Isn't it wonderful to write something once and use it forever? (But how long is "forever"?)

So, how do we create a Service in Angular? To answer that question, I will provide an example of creating a simple Service.

1.3. Creating a Service in Angular

To create a Service, first, you need to import Injectable from @angular/core. Injectable is a decorator used to mark a class as an injectable class that can be injected into other classes.

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {

  log(message: string) {
    console.log('LoggerService: ' + message);
  }
}

In the above example, I create a simple LoggerService with a log method that logs a message to the console. The providedIn: 'root' part in the Injectable decorator means that this Service can be used anywhere in the application.

To use the LoggerService, you simply need to inject it into the constructor of a component or another Service.

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angular-services';

  constructor(private loggerService: LoggerService) {
    this.loggerService.log('Hello from AppComponent!');
  }
}

In the above example, I have injected the LoggerService into AppComponent and used the log method to write a text message. When you run the application, you will see the text "LoggerService: Hello from AppComponent!" logged to the console.

Now you have a basic understanding of Services and how to create Services in Angular. But the story doesn't end here. I will now take you deeper into Dependency Injection - a technique that Angular uses to provide Services to components and other Services.

2. Dependency Injection (DI) and how it works

2.1. What is Dependency Injection?

Dependency Injection (DI) is a programming technique that allows us to pass an object (usually a Service object) to another object. "Injection" here doesn't mean medical injection, but rather "injecting" a Service into a class through its constructor.

"You mean there's a technique for that?" - Yes, there is. But don't worry, DI is not as complicated as you might imagine. It's simple. DI helps us reduce the dependencies between classes, making the code easier to understand and test.

2.2. Dependency Injection in Angular

Angular provides a powerful and flexible DI system that allows us to easily manage dependencies between Services and components.

To inject a Service into a component, all we need to do is add it to the constructor of the component, just like the example above. Angular will automatically create an instance of the Service and "inject" it into the component.

2.3. Hierarchical Injector

Angular DI uses a technique called "Hierarchical Injector." This may sound complicated, but it's actually quite simple.

Imagine your Angular application as a family tree, with the root module at the top and components, directives, pipes, and Services as branches and leaves. Angular creates an injector for each branch, and this injector "inherits" providers from its parent injector.

This means that if you provide a Service in the root module (using providedIn: 'root'), then all components and Services in the application can use that Service.

However, if you provide a Service in a specific component, only that component and its child components can use that Service. This is how we can limit the scope of a Service.

3. Using Dependency Injection to inject Services

I have introduced DI and how it works. Now I will provide some examples of using DI to inject Services.

3.1. Injecting a Service into another Service

We can not only inject Services into components but also inject Services into other Services. This is useful when you want to break down a large Service into smaller Services, each performing a specific task.

Let's say I have a UserService that needs to use the LoggerService to log messages. Here's how I would do it:

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private loggerService: LoggerService) {}

  getUser() {
    // Get the user from the server
    const user = { name: 'John Doe', age: 30 };

    this.loggerService.log('Got user: ' + JSON.stringify(user));
    return user;
  }
}

In this example, I have injected the LoggerService into UserService and used it to log a message when fetching user data.

3.2. A closer look at the Injection syntax

In Angular, when we write constructor(private loggerService: LoggerService) {} in a class, we are doing two things at once:

  1. Declaring a loggerService property in that class. This property has the type LoggerService and its access scope is private, meaning it can only be accessed within the class.

  2. Requesting the Angular Dependency Injection system to provide an instance of LoggerService and assign it to the loggerService property.

This is a shorthand syntax provided by TypeScript, which saves us time and makes the code more readable. If we were to write it out in full, the above syntax would become:

private loggerService: LoggerService;

constructor(loggerService: LoggerService) {
  this.loggerService = loggerService;
}

In this example, I have split the above syntax into two steps: declaring the property and assigning a value to it in the constructor.

3.3 A Little Bonus for Those Who Don't Understand DI (Dependency Injection)

Let's take a look at the example below:

class UserRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

class EmployeeRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

const userRepository = new UserRepository();
const employeeRepository = new EmployeeRepository();

As you can see in the above repository classes, I create a new Database() twice. So if there are 100 places where I new Database() 100 times, each class above depends on the Database class. In reality, creating a class might not be as simple as calling new Database(); it may require passing a bunch of arguments new Database(pass a lot of args here, like connectstring...blah blah). Each class has its own specific task, yet it has to take care of creating dependent classes, which can be quite exhausting.

This is where DI comes into play. If we apply DI, the code above will look like this:

class UserRepository {
  // Short inject syntax supported by TypeScript
  constructor(private db: Database) {}
}

class EmployeeRepository {
  // The full syntax would be like this
  private db: Database;
  constructor(db: Database) {
    this.db = db;
  }
}

// I will create an instance of the Database class outside
const root = {
  db: new Database();
}
const userRepository = new UserRepository(root.db);
const employeeRepository = new EmployeeRepository(root.db);

As I explained earlier, when we decorate a class with @Injectable({providedIn: 'root'}), it means we are instructing, "root, create an instance of this class and inject it into the classes that will use it...". The root here can simply be an object like the example above, described in the module file. (Regarding DI, I will stop explaining here; don't get overwhelmed 😄 Let's continue next time.)

Now you have a basic understanding of Services and Dependency Injection in Angular. But it's not over yet. There's something very exciting waiting for us: RxJS and how we can use it with Services and DI.

4. RxJS and its Application in Services

4.1. What is RxJS?

RxJS (Reactive Extensions for JavaScript) is a library for reactive programming with JavaScript. It helps us handle events, asynchronous operations, and data streams easily.

RxJS introduces the concept of Observables, which allows us to handle asynchronous operations more easily and flexibly compared to using callbacks or Promises.

I repeat the definition of RxJS every time I talk about it because RxJS can be a bit complex when you're first learning it. So it's helpful to repeat the definition.

4.2. Using RxJS in Services

In Angular, one of the most common ways we use RxJS is in Services to handle HTTP requests.

For example, let's say we have a UserService that needs to fetch user data from the server. I will use HttpClient (a Service provided by Angular) to send the HTTP request, and HttpClient will return an Observable. I will use the subscribe method of the Observable to handle the data when it's ready.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private http: HttpClient) {}

  getUser(): Observable<any> {
    return this.http.get('/api/users/1');
  }
}

In this example, I have injected HttpClient into UserService and used it to send an HTTP GET request to the server. The getUser function returns an Observable, and I can subscribe to this Observable in a component to fetch the user data when it's ready.

4.3. Another Example

To illustrate the use of RxJS in Services more clearly, I will provide an example that simulates loading a list of users from the server, assuming the server supports pagination.

First, I will create a UserService that uses HttpClient to load data from the server:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, of } from 'rxjs';
import { catchError, tap, switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private pageNumber = new Subject<number>();
  users$ = this.pageNumber.pipe(
    switchMap(page => this.http.get(`/api/users?page=${page}`).pipe(
      catchError(error => {
        console.error('Get users failed', error);
        return of({ data: [] });
      })
    )),
    tap(users => console.log('Got users:', users))
  );

  constructor(private http: HttpClient) {}

  loadUsers(page: number) {
    this.pageNumber.next(page);
  }
}

In the UserService, I use a Subject to manage the current page number and a users$ Observable to manage the loading of data from the server. Whenever the page number changes (via the loadUsers function), users$ will automatically reload the data.

Pay attention to the usage of switchMap to switch from the page number to the Observable of user data and tap to log the data to the console. These are two important operators in RxJS that help us control and handle asynchronous tasks flexibly. (What are switchMap and tap? I will have a separate series on RxJS because it can be a bit complex when you're first learning it.)

Now, I can use the UserService in a component to display the list of users:

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users$ | async">
      {{user.name}}
    </div>
    <button (click)="loadMore()">Load more</button>
  `,
  styles: [`
    div {
      margin-bottom: 10px;
    }
    button {
      display: block;
      margin-top: 20px;
    }
  `]
})
export class UserListComponent implements OnInit {
  users$ = this.userService.users$;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.loadUsers(1);
  }

  loadMore() {
    this.userService.loadUsers(2); // Let's assume the next page is 2 for simplicity
  }
}

In the UserListComponent, I use the async pipe to subscribe to users$ and automatically update the UI whenever the data changes. When the user clicks the "Load more" button, I will load more data from the next page.

With just a few lines of code, thanks to RxJS, I have created an application capable of loading data from the server flexibly and persistently. This is just one of countless situations where RxJS can help us solve tasks easily and effectively.

5. Conclusion

You have accompanied me in exploring Services and Dependency Injection in Angular and how we can use RxJS to handle asynchronous tasks in Services. I'm sure that after this article, you have a better understanding of how Angular works and are ready to delve deeper into the world of Angular.

Remember, programming is not just a job; it's also a joy and passion! Always maintain your passion and thirst for learning to become a great developer.

Review Questions

  1. Do I need to create a new Service for each functionality? Not necessarily. It depends on how you organize your code. In some cases, you can create a shared Service to handle related functionalities.

  2. Can I use DI to inject classes other than Services? DI in Angular is primarily used to inject Services. However, you can also inject other classes as long as they have been registered with the Angular DI system.

  3. Is RxJS complex? Can I use Promises instead of Observables? RxJS can be a bit complex when you're first learning it, but it offers many benefits, especially when dealing with asynchronous tasks. However, if you find it difficult, you can use Promises. But remember that Promises are not as powerful as Observables.

  4. Can I inject a Service into a pipe or directive? Yes, you can inject Services into pipes, directives, and almost any class in Angular.

  5. Where should I provide a Service? In most cases, you should provide a Service at the root level (providedIn: 'root'). However, if you want to limit the scope of a Service, you can provide it in a specific component.


日本語版

1. Angularにおけるサービスとその役割

1.1. サービスとは何ですか?

サービスはAngularにおいて重要な概念です。「でも、サービスって何で、なぜ重要なの?」と疑問に思うかもしれませんね。実はとてもシンプルなんです。サービスは特定のタスクを実行し、アプリケーション全体で再利用可能な特別な目的のクラスです。

例えば、LoggerServiceというサービスがあったとします。このサービスの役割は、メッセージをコンソールにログしたり、サーバーに送信したりすることかもしれません。

1.2. サービスはなぜ重要なのですか?

言い換えれば、サービスはソースコードをきれいで保守可能で再利用可能にするのに役立ちます。同じコードを複数の場所に書く代わりに、サービス内にカプセル化して必要なときに再利用することができます。一度書いてずっと使えるって素晴らしいですよね?(でも、「ずっと」っていつまでのことなんでしょう?)

それでは、Angularでどのようにサービスを作成するかを説明します。その質問に答えるために、シンプルなサービスの作成例を示します。

1.3. Angularでのサービスの作成

サービスを作成するには、まず@angular/coreからInjectableをインポートする必要があります。Injectableは、他のクラスにインジェクト可能なクラスとしてマークするために使用されるデコレータです。

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {

  log(message: string) {
    console.log('LoggerService: ' + message);
  }
}

上記の例では、logメソッドでメッセージをコンソールにログするシンプルなLoggerServiceを作成しています。Injectableデコレータの中のprovidedIn: 'root'の部分は、このサービスがアプリケーションのどこでも使用できることを意味しています。

LoggerServiceを使用するには、単純にコンポーネントや他のサービスのコンストラクタにインジェクトするだけです。

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angular-services';

  constructor(private loggerService: LoggerService) {
    this.loggerService.log('AppComponentからこんにちは!');
  }
}

上記の例では、LoggerServiceAppComponentにインジェクトし、logメソッドを使用してテキストメッセージを書いています。アプリケーションを実行すると、「LoggerService: AppComponentからこんにちは!」というテキストがコンソールにログされます。

これで、サービスとサービスの作成方法の基本的な理解ができました。しかし、物語はここで終わりません。次は、Angularがコンポーネントや他のサービスにサービスを提供するために使用する依存性注入(DI)というテクニックについて詳しく説明します。

2. 依存性注入(DI)とその仕組み

2.1. 依存性注入とは何ですか?

依存性注入(DI)は、オブジェクト(通常はサービスオブジェクト)を別のオブジェクトに渡すことができるプログラミングのテクニックです。「注入」という言葉はここで医療的な注射を意味するのではなく、単に「注入」という意味で、コンストラクタを通じてクラスにサービスを「注入」することを指します。

「それってテクニックがあるの?」と思うかもしれませんが、心配しないでください。DIは想像しているほど複雑ではありません。シンプルです。DIはクラス間の依存関係を減らし、コードを理解しやすく、テストしやすくするのに役立ちます。

2.2. Angularにおける依存性注入

Angularは、サービスとコンポーネントの間の依存関係を簡単に管理できる強力で柔軟なDIシステムを提供しています。

サービスをコンポーネントに注入するには、先ほどの例のようにコンポーネントのコンストラクタに追加するだけです。Angularは自動的にサービスのインスタンスを作成し、コンポーネントに「注入」します。

2.3. 階層的インジェクタ

AngularのDIは、「階層的インジェクタ」と呼ばれるテクニックを使用しています。これは複雑そうに聞こえるかもしれませんが、実際には非常にシンプルです。

Angularアプリケーションをルートモジュールをトップとした家族の木と考えてみてください。コンポーネント、ディレクティブ、パイプ、およびサービスは枝や葉として存在します。Angularは各枝に対してインジェクタを作成し、このインジェクタは親のインジェクタからプロバイダを「継承」します。

つまり、ルートモジュールでサービスを提供すると(providedIn: 'root'を使用して)、アプリケーションのすべてのコンポーネントとサービスがそのサービスを使用できるようになります。

ただし、特定のコンポーネントでサービスを提供する場合は、そのコンポーネントとその子コンポーネントのみがそのサービスを使用できるようになります。これにより、サービスのスコープを制限できます。

3. サービスを注入するための依存性注入の使用

DIとその仕組みを紹介しました。次に、DIを使用してサービスを注入するいくつかの例を示します。

3.1. サービスを別のサービスに注入する

サービスはコンポーネントだけでなく、他のサービスにも注入することができます。これは、大きなサービスを複数のタスクを実行する小さなサービスに分割したい場合に便利です。

例えば、UserServiceLoggerServiceを使用してメッセージをログに記録する必要があるとします。次のように行います:

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private loggerService: LoggerService) {}

  getUser() {
    // サーバーからユーザーを取得
    const user = { name: 'John Doe', age: 30 };

    this.loggerService.log('ユーザーを取得しました:' + JSON.stringify(user));
    return user;
  }
}

この例では、LoggerServiceUserServiceに注入し、ユーザーデータを取得する際にメッセージをログに記録しています。

3.2. 注入構文の詳細

Angularでは、クラス内でconstructor(private loggerService: LoggerService) {}と記述することで、2つのことを同時に行っています:

  1. そのクラス内でloggerServiceプロパティを宣言しています。このプロパティの型はLoggerServiceであり、アクセス範囲はprivateとなっているため、クラス内でのみアクセス可能です。

  2. Angularの依存性注入システムに対して、LoggerServiceのインスタンスを提供してもらい、それをloggerServiceプロパティに割り当てるように要求しています。

これはTypeScriptが提供する省略記法であり、時間を節約し、コードをより読みやすくします。完全な形で書くと、上記の構文は次のようになります:

private loggerService: LoggerService;

constructor(loggerService: LoggerService) {
  this.loggerService = loggerService;
}

この例では、上記の構文を2つのステップに分割しています。まずプロパティを宣言し、次にコンストラクタで値を割り当てています。

3.3 DI(依存性注入)が理解できない人へのおまけ

以下の例を見てみましょう:

class UserRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

class EmployeeRepository {
  private db: Database;

  constructor() {
    this.db = new Database();
  }
}

const userRepository = new UserRepository();
const employeeRepository = new EmployeeRepository();

上記のリポジトリクラスでは、new Database()を2回作成しています。そのため、100か所で100回new Database()を呼び出す場合、上記の各クラスはDatabaseクラスに依存しています。実際のところ、クラスを作成することはnew Database()を呼び出すだけではなく、たくさんの引数を渡す必要があるかもしれません(例えば、new Database(接続文字列などの多くの引数を渡す...ぶらぶら))。それぞれのクラスは独自の特定のタスクを持っていますが、依存するクラスの作成にも注意が必要で、かなり大変かもしれません。

ここでDIが登場します。もしDIを適用すると、上記のコードは次のようになります:

class UserRepository {
  // TypeScriptがサポートする短いインジェクト構文
  constructor(private db: Database) {}
}

class EmployeeRepository {
  // 完全な構文は以下のようになります
  private db: Database;
  constructor(db: Database) {
    this.db = db;
  }
}

// Databaseクラスのインスタンスを外部で作成します
const root = {
  db: new Database();
}
const userRepository = new UserRepository(root.db);
const employeeRepository = new EmployeeRepository(root.db);

先ほど説明したように、クラスに@Injectable({providedIn: 'root'})とデコレートすると、指示していることは「root、このクラスのインスタンスを作成し、それを使用するクラスに「注入」してください...」ということです。ここでのrootは、上記の例のようにモジュールファイルで説明されるようなオブジェクトである場合があります。(DIに関しては、ここで説明を終了します。困惑しないでください 😄 次回に続けましょう。)

これで、Angularにおけるサービスと依存性注入の基本的な理解ができました。しかし、まだ終わりではありません。とてもワクワクすることが待っています:RxJSと、それをサービスとDIでどのように使用するかです。

4. RxJSとそのサービスでの適用

4.1. RxJSとは何ですか?

RxJS(Reactive Extensions for JavaScript)は、JavaScriptでリアクティブなプログラミングを行うためのライブラリです。イベント、非同期操作、データストリームを簡単に扱うことができます。

RxJSは、コールバックやプロミスを使用する場合に比べて、非同期操作をより簡単かつ柔軟に扱うための概念であるObservableを導入しています。

私がRxJSについて話すたびに、定義を繰り返しているのは、初めて学ぶときにRxJSは少し複雑に感じることがあるからです。そのため、定義を繰り返すことは役に立ちます。

4.2. サービスでのRxJSの使用

Angularでは、サービスでRxJSを使用する方法の1つとして、主にHTTPリクエストの処理に使用します。

例えば、サーバーからユーザーデータを取得する必要があるUserServiceがあるとします。Angularが提供するHttpClient(サービス)を使用してHTTPリクエストを送信し、HttpClientはObservableを返します。データが準備できたときには、Observableのsubscribeメソッドを使用してデータを処理します。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private http: HttpClient) {}

  getUser(): Observable<any> {
    return this.http.get('/api/users/1');
  }
}

この例では、UserServiceHttpClientを注入し、それを使用してサーバーに対してHTTP GETリクエストを送信しています。getUser関数はObservableを返し、コンポーネントでこのObservableにsubscribeすることでユーザーデータを取得できます。

4.3. 別の例

サービスでRxJSの使用方法をより明確に説明するために、ページネーションをサポートしていると想定して、サーバーからユーザーリストを読み込む例を示します。

まず、サーバーからデータを読み込むためにHttpClientを使用するUserServiceを作成します。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, of } from 'rxjs';
import { catchError, tap, switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private pageNumber = new Subject<number>();
  users$ = this.pageNumber

.pipe(
    switchMap(page => this.http.get(`/api/users?page=${page}`).pipe(
      catchError(error => {
        console.error('ユーザーの取得に失敗しました', error);
        return of({ data: [] });
      })
    )),
    tap(users => console.log('ユーザーを取得しました:', users))
  );

  constructor(private http: HttpClient) {}

  loadUsers(page: number) {
    this.pageNumber.next(page);
  }
}

UserServiceでは、現在のページ番号を管理するためにSubjectを使用し、データの読み込みを管理するためにusers$というObservableを使用しています。ページ番号が変更されるたびに(loadUsers関数を介して)、users$は自動的にデータを再読み込みします。

switchMapを使用してページ番号からユーザーデータのObservableに切り替え、tapを使用してデータをコンソールにログします。これらはRxJSの中で重要なオペレータであり、非同期タスクを柔軟に制御し処理するのに役立ちます。(switchMaptapは何ですか?RxJSは最初に学ぶと少し複雑なので、別のシリーズで説明します。)

これで、たった数行のコードで、RxJSのおかげで柔軟かつ持続的にサーバーからデータを読み込むアプリケーションを作成しました。これは、RxJSが私たちが簡単かつ効果的にタスクを解決するのに役立つ無数の状況のうちの1つです。

5. 結論

Angularにおけるサービスと依存性注入、およびRxJSを使用して非同期タスクを処理する方法を一緒に探求しました。この記事を読んだ後、Angularの動作についてより良い理解が得られ、Angularの世界により深く入り込む準備ができたことでしょう。

忘れないでください、プログラミングは単なる仕事だけでなく、喜びと情熱でもあります!常に情熱と学びの欲求を持ち続け、優れた開発者になるよう努力しましょう。

レビューの質問

  1. 機能ごとに新しいサービスを作成する必要がありますか? 必ずしもそうではありません。コードの組織方法によります。場合によっては、関連する機能を処理する共有サービスを作成することができます。

  2. サービス以外のクラスにもDIを使用できますか? AngularのDIは主にサービスを注入するために使用されます。ただし、AngularのDIシステムに登録されている限り、他のクラスも注入することができます。

  3. RxJSは複雑ですか?プロミスの代わりにObservableを使用できますか? RxJSは最初に学ぶと少し複雑かもしれませんが、コールバックやプロミスよりも多くの利点があります。ただし、難しいと感じる場合はプロミスを使用することもできます。ただし、プロミスはObservableほど強力ではありません。

  4. パイプやディレクティブにサービスを注入できますか? はい、パイプ、ディレクティブ、そしてほとんどのAngularのクラスにサービスを注入することができます。

  5. どこにサービスを提供すればよいですか? ほとんどの場合、サービスはルートレベル(providedIn: 'root')で提供する必要があります。ただし、サービスのスコープを制限したい場合は、特定のコンポーネントで提供することもできます。

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí