+1

Biến đổi ứng dụng Angular với tính năng SIgnals

Angular là một framework nổi tiếng để xây dựng các ứng dụng doanh nghiệp mạnh mẽ và phức tạp. Nó được sử dụng rộng rãi bởi các công ty lớn. Do đó, việc sở hữu kỹ năng xây dựng một ứng dụng hiệu suất cao bằng Angular là một trong những kỹ năng hàng đầu cho các lập trình viên.

Sự nổi tiếng của Angular có thể được quy cho một tính năng đặc biệt được gọi là phản ứng (reactivity). Phản ứng là khả năng của framework thay đổi giao diện người dùng (UI) khi dữ liệu cơ bản hoặc trạng thái của ứng dụng thay đổi.

Sự thay đổi này có thể là do các sự kiện không đồng bộ như nhận phản hồi từ một lệnh gọi API hoặc từ một hành động của người dùng như nhấp vào nút. Để đạt được khả năng phản ứng này, Angular triển khai một cơ chế được gọi là phát hiện thay đổi. Tuy nhiên, khả năng phản ứng là con dao hai lưỡi và thường có thể dẫn đến các vấn đề về hiệu suất do các cập nhật không mong muốn cho UI.

Angular gần đây đã phát hành một tính năng mới được gọi là Signals (tín hiệu), cho phép các nhà phát triển cải thiện hiệu suất của các ứng dụng hiện có được xây dựng bằng Angular, cũng như xây dựng các ứng dụng mới có hiệu suất vượt trội so với các ứng dụng Angular truyền thống.

Signals cung cấp cho bạn quyền kiểm soát việc phát hiện thay đổi và ngăn chặn các bản cập nhật không mong muốn cho UI. Việc chuyển đổi các ứng dụng hiện có để sử dụng signals có thể rất khó khăn, và hướng dẫn này nhằm mục đích hướng dẫn bạn bắt đầu với nó. Trong hướng dẫn này, một ứng dụng Angular hiện có, được xây dựng theo cách truyền thống, sẽ được chuyển đổi từng bước để sử dụng signals.

Trước khi bắt đầu, bạn cần đáp ứng được các điều kiện sau đây:

  • Có kiến thức về JavaScript, TypeScript và framework Angular.
  • Node.js và NPM được cài đặt trên máy của bạn. Bạn có thể xác minh điều này bằng cách sử dụng các lệnh sau: node -vnpm --version.
  • Git được cài đặt trên máy của bạn. Bạn có thể xác minh điều này bằng cách sử dụng lệnh git --version.
  • Trình soạn thảo mã được cài đặt trên máy của bạn. Hướng dẫn này được phát triển bằng Visual Studio Code, nhưng bất kỳ trình soạn thảo mã nào cũng sẽ hoạt động.

Bắt đầu khởi chạy một ứng dụng mà bạn có

Chúng ta sẽ chuyển đổi một ứng dụng hiện có bằng cách sử dụng Angular signal. Như trong bài viết này, tôi sẽ sử dụng một ứng dụng quản lý bảng chấm công, trong đó trưởng nhóm / quản lý ca có thể xem số giờ mỗi nhân viên đã làm việc mỗi tuần, cũng như tổng số giờ của nhóm. Ngoài ra, trưởng nhóm cũng có thể cập nhật số giờ của một nhân viên. image.png

Trước khi mở ứng dụng của bạn, hãy đảm bảo rằng bạn đã cài đặt các dependencies của ứng dụng bằng cách sử dụng lệnh npm install.

Sau khi các dependencies đã được cài đặt, hãy mở ứng dụng bằng cách sử dụng npm start. Khi ứng dụng đã khởi động, hãy điều hướng đến http://localhost:4200/ và bạn sẽ thấy ứng dụng của mình đã được hiển thị.

Hiểu rõ ứng dụng hiện có

Quay trở lại với ví dụ về ứng dụng của tôi, điều quan trọng là phải hiểu mã và kiến trúc thành phần hiển thị ứng dụng. Mở ứng dụng trong trình soạn thảo mã bằng cách điều hướng đến src -> app. Khi này nó sẽ hiển thị ra cấu trúc như sau. image.png

Ứng dụng này của tôi có hai thành phần chính, bao gồm thành phần cha và thành phần con. Thành phần cha hiển thị tổng số giờ tích lũy của nhóm cũng như danh sách nhân viên báo cáo cho trưởng nhóm và tổng số giờ và số giờ làm thêm của mỗi nhân viên. Thành phần cha có mã cần thiết để hiển thị danh sách cũng như tính toán tổng số giờ tích lũy của nhóm từ danh sách này. Hơn nữa, thành phần cha cũng cung cấp chi tiết của nhân viên được chọn cho thành phần con.

Thành phần con nhận chi tiết nhân viên được chọn làm đầu vào của nó và cho phép chỉnh sửa số giờ thông thường cũng như số giờ làm thêm. Khi trưởng nhóm đã hài lòng, một hành động lưu sẽ được bắt đầu. Hành động lưu phát ra một sự kiện từ thành phần con trở lại thành phần cha. Thành phần cha hoạt động trên sự kiện này và thực hiện các cập nhật cần thiết cho số giờ của nhân viên được chọn. Bản cập nhật này kích hoạt tính toán lại số giờ tích lũy.

Chuyển đổi ứng dụng hiện có sang Angular Signals

Việc chuyển đổi mã có thể bắt đầu bằng mã thành phần cha. Thay đổi biến managerName để sử dụng signals. Điều này nêu bật cách tạo một biến signal mới. Một signal có thể được khởi tạo bằng từ khóa signal và định nghĩa kiểu tùy chọn cùng với giá trị ban đầu cho signal. Mã bên dưới giới thiệu cách một signal mới được gọi là managerName có kiểu string có thể được khởi tạo với giá trị ban đầu là John Doe.

// parent-component.ts
managerName = signal<string>('John Doe');

Bạn sẽ quan sát thấy các vấn đề về hiển thị sau khi bạn cập nhật managerName trong tệp thành phần để sử dụng signal. Điều này là do, để đọc giá trị của một signal, nó cần được thực thi. Cập nhật mã HTML của thành phần thành bên dưới để đọc giá trị của signal một cách chính xác.

<!-- parent-component.html -->
<h1 class="text-3xl font-bold mb-6">Hello {{managerName()}}!</h1>

Danh sách nhân viên bên trong thành phần cha là một mảng đơn giản, hãy chuyển đổi nó thành signal.

// parent-component.ts
 employees = signal<Employee[]>([
    {
      id: 1,
      name: 'Jon Snow',
      regularHours: 40,
      overtimeHours: 5
    },
    ...restOfEmployees
    }
  ])

Ngay sau khi mảng nhân viên được chuyển đổi thành signal, thành phần cha sẽ quan sát thấy một số lỗi. Tại thời điểm này, hãy nhận xét mã bên trong các phương thức getTeamRegularHoursTotal, getTeamOvertimeHoursTotalemployeeChange như được đánh dấu trong hình minh họa được đưa ra bên dưới.

// parent-component.ts
  getTeamRegularHoursTotal() {
    let total = 0;
    // this.employees.forEach(employee => total += employee.regularHours);
    return total;
  }

  getTeamOvertimeHoursTotal() {
    let total = 0;
    // this.employees.forEach(employee => total += employee.overtimeHours);
    return total;
  }

  employeeChange(updatedEmployee: Employee | null) {
    // if (updatedEmployee) {
    //   const index = this.employees.findIndex(emp => emp.id === updatedEmployee.id);
    //   if (index !== -1) {
    //     this.employees[index] = updatedEmployee;
    //   }
    // }
    // this.selectedEmployee = null;
  }

Để hiển thị lại ứng dụng, hãy cập nhật HTML mẫu của thành phần cha và thực thi signal nhân viên theo mã được cung cấp bên dưới.

<!-- parent-component.html -->
<div class="flex items-center py-4 space-x-4 group border-b cursor-pointer hover:bg-gray-50" *ngFor="let employee of employees()" (click)="selectEmployee(employee)">

Đã đến lúc chuyển đổi logic để tính toán giá trị của số giờ thông thường hiện đang hiển thị là 0. Điều này dẫn đến việc kiểm tra một loại signal quan trọng khác được gọi là "computed" (tính toán).

Sử dụng Computed Signals

Computed signals, như tên gọi của nó, dựa vào các signal khác để lấy giá trị của chúng. Giá trị của chúng được cập nhật ngay sau khi các signal cơ bản thay đổi, mà không cần bất kỳ mã bổ sung nào để xử lý thay đổi.

Tạo một computed signal teamRegularHoursTotal như được hiển thị trong mã bên dưới, signal này phụ thuộc trực tiếp vào signal employees. Do đó, bất cứ khi nào signal employees thay đổi, giá trị của teamRegularHoursTotal sẽ tự động được cập nhật.

Xác định computed signal như được hiển thị trong mã bên dưới và đảm bảo rằng computed dependency được nhập từ gói lõi của Angular. Ngoài ra, hãy xóa hoặc nhận xét hoàn toàn phương thức getTeamRegularHoursTotal.

// parent-component.ts
 teamRegularHoursTotal = computed(() => {
    let total = 0;
    this.employees().forEach(employee => total += employee.regularHours);
    return total;
  })

Cập nhật HTML mẫu của thành phần cha để phản ánh thay đổi này và hiển thị giá trị tổng số giờ.

<!-- parent-component.html -->
<p class="text-lg font-medium text-gray-700">Regular Hours: <span class="font-bold">
{{teamRegularHoursTotal() }}</span></p>

Tương tự, số giờ làm thêm giờ cũng có thể được chuyển đổi thành computed signal. Tham khảo mã bên dưới để cập nhật cả mã thành phần và HTML mẫu của nó. Ngoài ra, hãy nhận xét hoặc xóa hoàn toàn phương thức getTeamOvertimeHoursTotal.

// parent-component.ts
 teamOvertimeHoursTotal = computed(() => {
    let total = 0;
    this.employees().forEach(employee => total += employee.overtimeHours);
    return total;
  })
<!-- parent-component.html -->
<p class="text-lg font-medium text-gray-700">Overtime Hours: <span class="font-bold">
{{teamOvertimeHoursTotal() }}</span></p>

Đã đến lúc chuyển đổi biến selectedEmployee trong thành phần cha thành signal. Chuyển đổi nó bằng cách sử dụng mã bên dưới:

// parent-component.ts
selectedEmployee = signal<Employee | null>(null);

Ngay sau khi thực hiện thay đổi này, phương thức selectEmployee trong thành phần cha sẽ gặp lỗi. Những lỗi này là một khởi đầu tuyệt vời cho một chủ đề quan trọng: thay đổi giá trị của một signal. Các signal của Angular có thể được cập nhật bằng cách sử dụng API set hoặc update từ signals.

Như tên gọi của nó, phương thức set gán một giá trị mới cho một signal và phương thức update cập nhật giá trị của một signal. Sử dụng phương thức set để thay đổi giá trị signal selectedEmployee. Bạn sẽ sớm thấy phương thức update trong hành động.

// parent-component.ts
selectEmployee(employee: Employee): void {
    this.selectedEmployee.set(employee);
}

Cùng với thay đổi này, mẫu của thành phần cha cần được cập nhật. Cập nhật mã chứa thành phần con như được hiển thị trong mã bên dưới.

Sau thay đổi này, hãy xác minh rằng ứng dụng đang hiển thị đúng cách và bạn có thể chọn một nhân viên và xem chi tiết trên màn hình. Điều này rất quan trọng vì chúng ta sẽ thực hiện hành trình chuyển đổi signal này sang thành phần con.

<!-- parent-component.html -->
<section class="w-full md:w-1/2 bg-white p-6 rounded-lg shadow-lg border border-gray-200 order-1 md:order-2 mb-4 md:mb-0"
      *ngIf="selectedEmployee() !== null">
    <app-child-component [employee]="selectedEmployee()" (employeeChange)="employeeChange($event)"></app-child-component>
</section>

Chuyển đổi thành phần con

Thành phần con nhận giá trị của nhân viên được chọn thông qua đầu vào của nó, hiện đang được xác định bằng cách sử dụng trình trang trí @Input. Điều này có thể được chuyển đổi bằng cách sử dụng một signal được gọi một cách thuận tiện là input. Thay đổi biến employee thành kiểu input như được hiển thị trong mã bên dưới. Nhận xét mã được hiển thị trong phương thức saveChangesresetForm như được đánh dấu.

// child-component.ts
employee  = input<Employee | null>();
saveChanges() {
    // this.employee.regularHours = this.editedRegularHours;
    // this.employee.overtimeHours = this.editedOvertimeHours;
    // this.employeeChange.emit(this.employee);
}

private resetForm() {
    // this.editedRegularHours = this.employee.regularHours;
    // this.editedOvertimeHours = this.employee.overtimeHours;
}

Biểu mẫu bên trong thành phần con hiển thị số giờ thông thường và số giờ làm thêm giờ của nhân viên được chọn bằng cách sử dụng hai biến mô hình: editedRegularHourseditedOvertimeHours.

Những biến này không còn cần thiết nữa và các đầu vào bên trong biểu mẫu của các thành phần con có thể được cập nhật để sử dụng trực tiếp giá trị của signal.

<!-- child-component.html --> 
<input 
     type="number" 
     id="regularHours" 
     [ngModel]="editedRegularHours"
     (ngModelChange)="onRegularHoursChange($event)"
      class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" 
 />
 <input 
      type="number" 
      id="overtimeHours" 
      [ngModel]="editedOvertimeHours"
      (ngModelChange)="onOvertimeHoursChange($event)"
      class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" 
  />

Logic để nắm bắt các giá trị được cập nhật cho số giờ thông thường và làm thêm giờ cũng có thể được chuyển đổi. Lý tưởng nhất là việc cập nhật mã bên dưới sẽ hoạt động nhưng bạn sẽ gặp lỗi như được đánh dấu bên dưới. image.png

Lý do đằng sau lỗi là signal kiểu input không thể bị biến đổi bằng các phương thức setupdate. Angular cung cấp một loại signal khác được gọi là model để nhận đầu vào cho các thành phần có thể bị biến đổi bên trong chính thành phần đó. Thay đổi employee thành kiểu model như được thấy bên dưới.

// child-component.ts
employee = model<Employee | null>();

Cập nhật phương thức cập nhật làm thêm giờ như được hiển thị trong mã bên dưới. Với điều này, thành phần con sẽ lấy lại khả năng cập nhật các giá trị số giờ thông thường và làm thêm giờ của nhân viên nhận được từ thành phần cha.

// child-component.ts
  onOvertimeHoursChange(event: number) {
    this.employee.set({
      ...this.employee(),
      overtimeHours: event
    })
  }

Hiện tại, thành phần con đang liên lạc lại các thay đổi cho thành phần cha bằng cách sử dụng trình trang trí @Output. Tương tự như inputmodel, Angular có một signal kiểu output để cho phép giao tiếp hai chiều giữa các thành phần con và cha. Cập nhật sự kiện employeeChange thành kiểu output như được thấy bên dưới.

// child-component.ts 
employeeChange = output<Employee | null>();

Cập nhật phương thức saveChanges và phát ra một đối tượng nhân viên được cập nhật.

// child-component.ts 
 saveChanges() {
    this.employeeChange.emit(this.employee());
 }

Đối với bước cuối cùng trong thành phần con, hãy cập nhật các phương thức resetFormcancelChanges để phản ánh các thay đổi đã thực hiện như được hiển thị trong mã bên dưới.

// child-component.ts   
cancelChanges() {
    this.employeeChange.emit(null);
 }
resetForm() {
    this.employee.set(null);
}

Quay trở lại thành phần cha ngay bây giờ, điều quan trọng là phải thực hiện các thay đổi đối với mẫu để đảm bảo giao tiếp suôn sẻ giữa cả hai thành phần.

Đầu vào employee trong thành phần con đã được thay đổi thành đầu vào kiểu model. Một model có hai cách liên kết dữ liệu. Thực hiện cập nhật cho mã như được thấy bên dưới để sử dụng banana in the box (cú pháp đặc biệt [( )], là viết tắt của liên kết dữ liệu hai chiều) cho đầu vào.

<!-- parent-component.html -->
<section class="w-full md:w-1/2 bg-white p-6 rounded-lg shadow-lg border border-gray-200 order-1 md:order-2 mb-4 md:mb-0"
    *ngIf="selectedEmployee() !== null">
    <app-child-component [(employee)]="selectedEmployee" (employeeChange)="employeeChange($event)"></app-child-component>
</section>

Đã đến lúc cập nhật phương thức employeeChange trong thành phần cha để các giá trị giờ được cập nhật của nhân viên được chọn có thể được phản ánh trở lại màn hình.

Để đạt được điều này, một phương thức quan trọng khác được sử dụng để biến đổi giá trị có thể được tận dụng. Đây là phương thức update, nhận một hàm làm đối số thay vì giá trị đầy đủ. Hàm này truyền giá trị hiện tại của signal làm tham số đầu tiên và trả về giá trị được cập nhật của signal.

Tham khảo mã được cập nhật cho phương thức employeeChange như được thấy bên dưới để hiểu rõ hơn về điều này.

// parent-component.ts  
employeeChange() {
    if (this.selectedEmployee()) {
      this.employees.update(employees => employees.map(employee => {
        if (employee.id === this.selectedEmployee().id) {
          employee.regularHours = this.selectedEmployee().regularHours;
          employee.overtimeHours = this.selectedEmployee().overtimeHours;
        }
        return employee;
      }))
    }
    this.selectedEmployee.set(null);
  }

Với thay đổi này, toàn bộ quá trình chuyển đổi ứng dụng để sử dụng signals đã hoàn tất. Đảm bảo mọi thứ tải đúng cách và tất cả các chức năng đều hoạt động như mong đợi.

Kết luận

Chúc mừng bạn đã hoàn thành hướng dẫn này! Trong hành trình này, bạn đã tạo một signal thông thường và cập nhật giá trị của nó bằng cách sử dụng các phương thức setupdate.

Bạn cũng đã học cách các computed signal được xác định và sử dụng như thế nào. Ngoài ra, bạn đã kích hoạt giao tiếp giữa hai thành phần bằng cách sử dụng các signal input, modeloutput.

Nếu bạn gặp phải bất kỳ sự cố nào trong hành trình chuyển đổi này, bạn có thể kiểm tra mã trong nhánh feature/signals của cùng một kho lưu trữ mà bạn đã sao chép trước đó. Bạn nên làm theo thay vì sao chép giải pháp từ nhánh tính năng này.

Cảm ơn các bạn đã theo dõi.


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í