Trong năm qua, Angular đã có thêm nhiều tính năng mới như tái tạo và thành phần hiển thị có thể trì hoãn để giúp nhà phát triển cải thiện Chỉ số quan trọng chính của trang web và đảm bảo trải nghiệm chất lượng cao cho người dùng cuối. Chúng tôi cũng đang nghiên cứu các tính năng khác liên quan đến việc kết xuất phía máy chủ dựa trên chức năng này, chẳng hạn như truyền trực tuyến và hydrat hoá một phần.
Rất tiếc, có một mẫu có thể ngăn ứng dụng hoặc thư viện của bạn tận dụng tối đa tất cả các tính năng mới và sắp ra mắt này: thao tác thủ công trên cấu trúc DOM cơ bản. Angular yêu cầu cấu trúc của DOM phải nhất quán từ thời điểm máy chủ chuyển đổi tuần tự một thành phần cho đến khi thành phần đó được làm mới trên trình duyệt. Việc sử dụng ElementRef
, Renderer2
hoặc API DOM để thêm, di chuyển hoặc xoá các nút khỏi DOM theo cách thủ công trước khi làm mới có thể gây ra sự không nhất quán khiến các tính năng này không hoạt động.
Tuy nhiên, không phải mọi thao tác truy cập và thao tác DOM theo cách thủ công đều gặp vấn đề, đôi khi điều này là cần thiết. Chìa khoá để sử dụng DOM một cách an toàn là giảm thiểu nhu cầu sử dụng DOM càng nhiều càng tốt, sau đó trì hoãn việc sử dụng DOM càng lâu càng tốt. Các nguyên tắc sau đây giải thích cách bạn có thể thực hiện việc này và xây dựng các thành phần Angular thực sự phổ quát và phù hợp với tương lai, có thể tận dụng tối đa tất cả các tính năng mới và sắp ra mắt của Angular.
Tránh thao tác DOM theo cách thủ công
Không có gì đáng ngạc nhiên khi cách tốt nhất để tránh các vấn đề do thao tác thủ công với DOM gây ra là tránh thao tác thủ công với DOM bất cứ khi nào có thể. Angular có các API và mẫu tích hợp có thể thao tác hầu hết các khía cạnh của DOM: bạn nên sử dụng các API và mẫu này thay vì truy cập trực tiếp vào DOM.
Biến đổi phần tử DOM của chính thành phần
Khi viết một thành phần hoặc lệnh, bạn có thể cần sửa đổi phần tử lưu trữ (tức là phần tử DOM khớp với bộ chọn của thành phần hoặc lệnh) để thêm một lớp, kiểu hoặc thuộc tính, thay vì nhắm mục tiêu hoặc giới thiệu một phần tử trình bao bọc. Bạn có thể chỉ cần sử dụng ElementRef
để thay đổi phần tử DOM cơ bản. Thay vào đó, bạn nên sử dụng liên kết máy chủ để khai báo liên kết các giá trị với một biểu thức:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
Cũng giống như với liên kết dữ liệu trong HTML, bạn cũng có thể liên kết với các thuộc tính và kiểu, đồng thời thay đổi 'true'
thành một biểu thức khác mà Angular sẽ sử dụng để tự động thêm hoặc xoá giá trị nếu cần.
Trong một số trường hợp, khoá sẽ cần được tính toán một cách linh động. Bạn cũng có thể liên kết với một tín hiệu hoặc hàm trả về một tập hợp hoặc bản đồ giá trị:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
Trong các ứng dụng phức tạp hơn, bạn có thể muốn thao tác DOM theo cách thủ công để tránh ExpressionChangedAfterItHasBeenCheckedError
. Thay vào đó, bạn có thể liên kết giá trị với một tín hiệu như trong ví dụ trước. Bạn có thể thực hiện việc này khi cần và không cần áp dụng tín hiệu trên toàn bộ cơ sở mã.
Biến đổi các phần tử DOM bên ngoài mẫu
Bạn có thể muốn sử dụng DOM để truy cập vào các phần tử thường không truy cập được, chẳng hạn như các phần tử thuộc về thành phần mẹ hoặc con khác. Tuy nhiên, cách này dễ gây lỗi, vi phạm tính đóng gói và khiến bạn khó thay đổi hoặc nâng cấp các thành phần đó trong tương lai.
Thay vào đó, thành phần của bạn nên coi mọi thành phần khác là một hộp đen. Hãy dành thời gian để xem xét thời điểm và vị trí các thành phần khác (ngay cả trong cùng một ứng dụng hoặc thư viện) có thể cần tương tác hoặc tuỳ chỉnh hành vi hoặc giao diện của thành phần, sau đó cho thấy cách an toàn và được ghi nhận để thực hiện việc này. Sử dụng các tính năng như chèn phần phụ thuộc phân cấp để cung cấp API cho một cây con khi các thuộc tính @Input
và @Output
đơn giản là không đủ.
Trước đây, thường thì các tính năng như hộp thoại phương thức hoặc chú giải công cụ được triển khai bằng cách thêm một phần tử vào cuối <body>
hoặc một số phần tử lưu trữ khác, sau đó di chuyển hoặc chiếu nội dung vào đó. Tuy nhiên, ngày nay, bạn có thể hiển thị một phần tử <dialog>
đơn giản trong mẫu:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
Tạm hoãn thao tác thủ công trên DOM
Sau khi áp dụng các nguyên tắc trước đó để giảm thiểu thao tác và quyền truy cập trực tiếp vào DOM nhiều nhất có thể, bạn vẫn có thể phải thực hiện một số thao tác không thể tránh khỏi. Trong những trường hợp như vậy, điều quan trọng là bạn phải trì hoãn việc này càng lâu càng tốt. Lệnh gọi lại afterRender
và afterNextRender
là một cách tuyệt vời để thực hiện việc này, vì các lệnh gọi lại này chỉ chạy trên trình duyệt, sau khi Angular đã kiểm tra mọi thay đổi và cam kết các thay đổi đó với DOM.
Chạy JavaScript chỉ dành cho trình duyệt
Trong một số trường hợp, bạn sẽ có một thư viện hoặc API chỉ hoạt động trong trình duyệt (ví dụ: thư viện biểu đồ, một số cách sử dụng IntersectionObserver
, v.v.). Thay vì kiểm tra có điều kiện xem bạn đang chạy trên trình duyệt hay không hoặc loại bỏ hành vi trên máy chủ, bạn chỉ cần sử dụng afterNextRender
:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Thực hiện bố cục tuỳ chỉnh
Đôi khi, bạn có thể cần đọc hoặc ghi vào DOM để thực hiện một số bố cục mà trình duyệt mục tiêu chưa hỗ trợ, chẳng hạn như định vị chú giải công cụ. afterRender
là một lựa chọn tuyệt vời cho việc này, vì bạn có thể chắc chắn rằng DOM đang ở trạng thái nhất quán. afterRender
và afterNextRender
chấp nhận giá trị phase
là EarlyRead
, Read
hoặc Write
. Việc đọc bố cục DOM sau khi ghi buộc trình duyệt phải đồng bộ tính toán lại bố cục, điều này có thể ảnh hưởng nghiêm trọng đến hiệu suất (xem: bố cục bị lỗi). Do đó, điều quan trọng là bạn phải chia logic của mình thành các giai đoạn chính xác.
Ví dụ: một thành phần chú giải công cụ muốn hiển thị chú giải công cụ liên quan đến một phần tử khác trên trang có thể sẽ sử dụng hai giai đoạn. Trước tiên, giai đoạn EarlyRead
sẽ được dùng để lấy kích thước và vị trí của các phần tử:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
Sau đó, giai đoạn Write
sẽ sử dụng giá trị đã đọc trước đó để thực sự định vị lại chú giải công cụ:
afterRender(() => {
tooltipEl.style.setProperty('left', `${targetRect.left + targetRect.width / 2 - tooltipRect.width / 2}px`);
tooltipEl.style.setProperty('top', `${targetRect.bottom - 4}px`);
}, { phase: AfterRenderPhase.Write },
);
Bằng cách chia logic của chúng ta thành các giai đoạn chính xác, Angular có thể thao tác hàng loạt DOM một cách hiệu quả trên mọi thành phần khác trong ứng dụng, đảm bảo tác động tối thiểu đến hiệu suất.
Kết luận
Sắp tới, chúng tôi sẽ có nhiều điểm cải tiến mới và thú vị cho tính năng kết xuất phía máy chủ của Angular, nhằm giúp bạn dễ dàng mang đến trải nghiệm tuyệt vời cho người dùng. Chúng tôi hy vọng các mẹo trước đó sẽ hữu ích trong việc giúp bạn khai thác tối đa các tính năng này trong ứng dụng và thư viện của mình!