ในช่วงปีที่ผ่านมา Angular ได้เพิ่มฟีเจอร์ใหม่ๆ มากมาย เช่น การไฮเดรตและมุมมองที่เลื่อนได้ เพื่อช่วยนักพัฒนาซอฟต์แวร์ปรับปรุง Core Web Vitals และมอบประสบการณ์การใช้งานที่ยอดเยี่ยมให้แก่ผู้ใช้ปลายทาง นอกจากนี้ เรายังกําลังวิจัยฟีเจอร์อื่นๆ ที่เกี่ยวข้องกับการแสดงผลฝั่งเซิร์ฟเวอร์ซึ่งสร้างขึ้นจากฟังก์ชันการทํางานนี้ด้วย เช่น สตรีมมิงและการเติมน้ำบางส่วน
แต่มีรูปแบบหนึ่งที่อาจทําให้แอปพลิเคชันหรือไลบรารีของคุณไม่สามารถใช้ประโยชน์จากฟีเจอร์ใหม่และฟีเจอร์ที่กําลังจะมีทั้งหมดเหล่านี้ได้อย่างเต็มที่ ซึ่งก็คือ การจัดการโครงสร้าง DOM พื้นฐานด้วยตนเอง Angular กําหนดให้โครงสร้าง DOM คงที่ตั้งแต่เวลาที่เซิร์ฟเวอร์จัดรูปแบบคอมโพเนนต์จนกว่าจะมีการเรียกข้อมูลในเบราว์เซอร์ การใช้ ElementRef
, Renderer2
หรือ DOM API เพื่อเพิ่ม ย้าย หรือนําโหนดออกจาก DOM ด้วยตนเองก่อนการไฮเดรตอาจทําให้เกิดความไม่สอดคล้องซึ่งทําให้ฟีเจอร์เหล่านี้ไม่ทํางาน
อย่างไรก็ตาม การจัดการและการเข้าถึง DOM ด้วยตนเองอาจไม่ก่อให้เกิดปัญหาเสมอไป และบางครั้งก็จําเป็น หัวใจสำคัญของการใช้ DOM อย่างปลอดภัยคือการลดความจำเป็นในการใช้ DOM ให้น้อยที่สุด แล้วเลื่อนการใช้ DOM ออกไปให้นานที่สุด หลักเกณฑ์ต่อไปนี้อธิบายวิธีทําให้บรรลุเป้าหมายนี้และสร้างคอมโพเนนต์ Angular ที่ใช้งานได้จริงกับทุกแพลตฟอร์มและใช้ได้ในอนาคต ซึ่งสามารถใช้ประโยชน์จากฟีเจอร์ใหม่และฟีเจอร์ที่กําลังจะมีให้บริการของ Angular ทั้งหมด
หลีกเลี่ยงการจัดการ DOM ด้วยตนเอง
วิธีที่ดีที่สุดในการหลีกเลี่ยงปัญหาที่เกิดจากการดําเนินการ DOM ด้วยตนเองคือหลีกเลี่ยงการดำเนินการดังกล่าวโดยสิ้นเชิงหากเป็นไปได้ Angular มี API และรูปแบบในตัวที่จัดการแง่มุมส่วนใหญ่ของ DOM ได้ คุณควรใช้ API และรูปแบบเหล่านี้แทนการเข้าถึง DOM โดยตรง
เปลี่ยนรูปแบบองค์ประกอบ DOM ของคอมโพเนนต์
เมื่อเขียนคอมโพเนนต์หรือไดเรกทีฟ คุณอาจต้องแก้ไของค์ประกอบโฮสต์ (นั่นคือองค์ประกอบ DOM ที่ตรงกับตัวเลือกของคอมโพเนนต์หรือไดเรกทีฟ) เช่น เพิ่มคลาส สไตล์ หรือแอตทริบิวต์ แทนการกำหนดเป้าหมายหรือการใช้องค์ประกอบ Wrapper คุณอาจอยากใช้ ElementRef
เพื่อเปลี่ยนรูปแบบองค์ประกอบ DOM ที่อยู่เบื้องหลัง แต่คุณควรใช้การเชื่อมโยงโฮสต์เพื่อประกาศการเชื่อมโยงค่ากับนิพจน์แทน ดังนี้
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
เช่นเดียวกับการเชื่อมโยงข้อมูลใน HTML คุณยังทำการเชื่อมโยงกับแอตทริบิวต์และสไตล์ รวมถึงเปลี่ยน 'true'
เป็นนิพจน์อื่นที่ Angular จะใช้เพื่อเพิ่มหรือนำค่าออกโดยอัตโนมัติตามต้องการได้ด้วย
ในบางกรณี ระบบจะต้องคํานวณคีย์แบบไดนามิก นอกจากนี้ คุณยังสามารถเชื่อมโยงกับสัญญาณหรือฟังก์ชันที่แสดงผลชุดหรือแผนที่ของค่าได้ ดังนี้
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
ในแอปพลิเคชันที่ซับซ้อนมากขึ้น คุณอาจต้องการใช้การจัดการ DOM ด้วยตนเองเพื่อหลีกเลี่ยง ExpressionChangedAfterItHasBeenCheckedError
แต่คุณสามารถเชื่อมโยงค่ากับสัญญาณได้ ดังตัวอย่างก่อนหน้านี้ ซึ่งทําได้ตามต้องการและไม่จำเป็นต้องใช้สัญญาณทั่วทั้งโค้ดเบส
เปลี่ยนองค์ประกอบ DOM นอกเทมเพลต
คุณอาจอยากลองใช้ DOM เพื่อเข้าถึงองค์ประกอบที่เข้าถึงไม่ได้ตามปกติ เช่น องค์ประกอบที่เป็นของคอมโพเนนต์หลักหรือคอมโพเนนต์ย่อยอื่นๆ อย่างไรก็ตาม วิธีนี้มักทำให้เกิดข้อผิดพลาด ละเมิดการรวม และทําให้เปลี่ยนแปลงหรืออัปเกรดคอมโพเนนต์เหล่านั้นได้ยากในอนาคต
แต่ควรถือว่าคอมโพเนนต์อื่นๆ ทั้งหมดเป็นกล่องดำ ใช้เวลาพิจารณาว่าคอมโพเนนต์อื่นๆ (แม้กระทั่งภายในแอปพลิเคชันหรือไลบรารีเดียวกัน) อาจต้องโต้ตอบหรือปรับแต่งลักษณะการทำงานหรือลักษณะที่ปรากฏของคอมโพเนนต์ของคุณเมื่อใดและที่ไหน จากนั้นแสดงวิธีทํางานที่ปลอดภัยและมีเอกสารประกอบไว้ให้ใช้งาน ใช้ฟีเจอร์ต่างๆ เช่น การฉีดข้อมูลตามลําดับชั้นเพื่อให้ API พร้อมใช้งานสําหรับซับต้นไม้เมื่อพร็อพเพอร์ตี้ @Input
และ @Output
ธรรมดาไม่เพียงพอ
ที่ผ่านมา การใช้ฟีเจอร์ต่างๆ เช่น กล่องโต้ตอบแบบโมดัลหรือเคล็ดลับเครื่องมือนั้นเป็นเรื่องปกติ โดยการเพิ่มองค์ประกอบลงท้าย <body>
หรือองค์ประกอบโฮสต์อื่นๆ แล้วย้ายหรือโปรเจ็กต์เนื้อหาไปยังตำแหน่งนั้น อย่างไรก็ตาม ปัจจุบันคุณอาจแสดงผลองค์ประกอบ <dialog>
ง่ายๆ ในเทมเพลตแทนได้ ดังนี้
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
เลื่อนการดําเนินการ DOM ด้วยตนเอง
หลังจากใช้หลักเกณฑ์ก่อนหน้านี้เพื่อลดการจัดการ DOM โดยตรงและการเข้าถึงให้น้อยที่สุดเท่าที่จะทำได้ คุณอาจยังมีการดำเนินการบางอย่างที่เหลืออยู่ซึ่งหลีกเลี่ยงไม่ได้ ในกรณีเช่นนี้ คุณควรเลื่อนการอัปเดตไว้ให้นานที่สุด การใช้การเรียกกลับ afterRender
และ afterNextRender
เป็นวิธีที่ยอดเยี่ยมในการดำเนินการนี้ เนื่องจากจะทํางานในเบราว์เซอร์เท่านั้น หลังจากที่ Angular ตรวจสอบการเปลี่ยนแปลงและบันทึกการเปลี่ยนแปลงไปยัง DOM แล้ว
เรียกใช้ JavaScript ในเบราว์เซอร์เท่านั้น
ในบางกรณี คุณจะมีไลบรารีหรือ API ที่ใช้งานได้ในเบราว์เซอร์เท่านั้น (เช่น ไลบรารีแผนภูมิ การใช้งาน IntersectionObserver
บางรายการ เป็นต้น) คุณใช้ afterNextRender
แทนการตรวจสอบแบบมีเงื่อนไขว่าคุณกำลังใช้งานในเบราว์เซอร์หรือกำลังจำลองลักษณะการทำงานในเซิร์ฟเวอร์ได้
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
ดำเนินการกับเลย์เอาต์ที่กำหนดเอง
บางครั้งคุณอาจต้องอ่านหรือเขียนลงใน DOM เพื่อแสดงเลย์เอาต์บางอย่างที่เบราว์เซอร์เป้าหมายยังไม่รองรับ เช่น การวางตำแหน่งเคล็ดลับเครื่องมือ afterRender
เป็นตัวเลือกที่ยอดเยี่ยมสำหรับกรณีนี้ เนื่องจากคุณมั่นใจได้ว่า DOM อยู่ในสถานะที่สอดคล้องกัน afterRender
และ afterNextRender
ยอมรับค่า phase
เป็น EarlyRead
, Read
หรือ Write
การอ่านเลย์เอาต์ DOM หลังจากเขียนจะบังคับให้เบราว์เซอร์คำนวณเลย์เอาต์แบบซิงค์ ซึ่งอาจส่งผลเสียต่อประสิทธิภาพอย่างร้ายแรง (ดูการประมวลผลเลย์เอาต์มากเกินไป) ดังนั้น คุณจึงต้องแยกตรรกะออกเป็นระยะๆ อย่างระมัดระวัง
ตัวอย่างเช่น คอมโพเนนต์เคล็ดลับเครื่องมือที่ต้องการแสดงเคล็ดลับเครื่องมือซึ่งสัมพันธ์กับองค์ประกอบอื่นในหน้าเว็บมีแนวโน้มที่จะใช้ 2 ระยะ ระบบจะใช้ระยะ EarlyRead
ก่อนเพื่อรับขนาดและตําแหน่งขององค์ประกอบ
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
จากนั้นระยะ Write
จะใช้ค่าที่อ่านก่อนหน้านี้เพื่อจัดตำแหน่งเคล็ดลับเครื่องมือใหม่
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 },
);
การแยกตรรกะออกเป็นระยะการทำงานที่เหมาะสมช่วยให้ Angular สามารถจัดกลุ่มการจัดการ DOM กับคอมโพเนนต์อื่นๆ ทั้งหมดในแอปพลิเคชันได้อย่างมีประสิทธิภาพ ซึ่งจะช่วยลดผลกระทบต่อประสิทธิภาพให้เหลือน้อยที่สุด
บทสรุป
เรามีแผนที่จะปรับปรุงการเรนเดอร์ฝั่งเซิร์ฟเวอร์ของ Angular ใหม่และน่าสนใจหลายอย่าง โดยมีเป้าหมายเพื่อให้คุณมอบประสบการณ์การใช้งานที่ยอดเยี่ยมแก่ผู้ใช้ได้ง่ายขึ้น เราหวังว่าเคล็ดลับก่อนหน้านี้จะเป็นประโยชน์ในการช่วยให้คุณใช้ประโยชน์จากแอปพลิเคชันและไลบรารีได้อย่างเต็มที่