มีอะไรใหม่ใน Angular 17

10/11/2566

renaissance-art
Photo by Daniel Gregoire on Unsplash

ยุคเรเนซองส์ของ Angular มาถึงแล้ว! พบกับ Angular เวอร์ชั่นใหม่ล่าสุด ที่นอกจากจะมาพร้อมโลโก้ใหม่ไฉไลกว่าเดิมแล้ว ยังมาพร้อมฟีเจอร์คับคั่งที่เหมือนยกเครื่อง Angular ใหม่ให้แจ่มกว่าเดิมอีกด้วย


สารบัญ


ลุคใหม่ไฉไลกว่าเดิม

ในช่วงปีที่ผ่านมานี้ Angular ได้ออกฟีเจอร์ใหม่ๆมามากมายไม่ว่าจะเป็น Angular Signal, SSR Hydration, Standalone Component, Directive Composition และอื่นๆอีกนับไม่ถ้วน แต่ว่าโลโก้ Angular นั้นยังไม่แตกต่างจากยุค AngularJS ซักเท่าไหร่ เพื่อเป็นการสื่อถึงความเปลี่ยนแปลงของ Angular และนำไปสู่ความทันสมัยในอนาคต ทางทีมจึงได้เปลี่ยนดีไซน์ของโลโก้ Angular ใหม่มาเป็นโลโก้แบบด้านล่างครับ

new-angular-logo
โลโก้ใหม่ไฉไลกว่าเดิม

Doc ใหม่น่าใช้กว่าเดิม

นอกจากโลโก้ใหม่แล้ว ทางทีม Angular ยังได้ทำเว็บ document ขึ้นมาใหม่ซึ่งก็คือ angular.dev โดยเว็บใหม่นี้จะประกอบไปด้วยโครงสร้างใหม่ ไกด์ใหม่ คอนเทนต์ใหม่ และยังเป็นแพลตฟอร์มที่เป็น interactive ทำให้เราสามารถเรียนรู้ Angular ผ่านบราวเซอร์ได้เลย โดยทั้งหมดนี้สร้างมาจากขุมพลังของ WebContainers ครับ

โดยทางทีมวางแผนไว้ว่า angular.dev จะเป็น default เว็บของ Angular เริ่มตั้งแต่เวอร์ชั่น 18 เป็นต้นไปครับ


Built-in control flow

หลังจากที่ออก RFC มาเพื่อขอความคิดเห็นเกี่ยวกับ block template syntax แบบใหม่ ทางทีมได้เก็บ feedback และนำไปทบทวนและในที่สุดก็ออกมาเป็น control flow syntax ใหม่ที่ใช้ใน template ของ Angular ครับ โดย syntax ใหม่นี้มีข้อดีคือ

  • มีความใกล้เคียงกับ syntax ใน JavaScript ทำให้เราไม่ต้องไปดู doc เวลาจะใช้
  • Type checking ทำได้ดีขึ้น
  • เป็น concept ที่เกิดตอน build ทำให้ลดงานที่ต้องทำในตอน runtime และยังช่วยลดขนาดของ bundle ได้มากถึง 30KB นอกจากนี้ยังช่วยเพิ่มคะแนนของ Core Web Vital ได้อีกด้วย
  • สามารถใช้ใน Angular Template ได้เลยไม่ต้อง import ใดๆเพิ่ม
  • performance ดีขึ้นอย่างเห็นได้ชัด

Conditional statements

ลองเปรียบเทียบกันระหว่าง syntax แบบเก่ากับแบบใหม่ครับ

<!-- แบบเก่า -->
<div *ngIf="loggedIn; else anonymousUser">
  The user is logged in
</div>
<ng-template #anonymousUser>
  The user is not logged in
</ng-template>

<!-- แบบใหม่ -->
@if (loggedIn) {
  The user is logged in
} @else {
  The user is not logged in
}

จะเห็นได้ว่าแบบใหม่เข้าใจง่ายขึ้น และเคส else ก็เขียนได้ง่ายกว่ากว่าเดิม นอกจากนี้ยังสามารถใส่เคส else if ได้อีกด้วย

ลองมาดูเคส switch จะยิ่งเห็นว่าใช้งานง่ายกว่าเดิมครับ

<!-- แบบเก่า -->
<div [ngSwitch]="accessLevel">
  <admin-dashboard *ngSwitchCase="admin"/>
  <moderator-dashboard *ngSwitchCase="moderator"/>
  <user-dashboard *ngSwitchDefault/>
</div>

<!-- แบบใหม่ -->
@switch (accessLevel) {
  @case ('admin') { <admin-dashboard/> }
  @case ('moderator') { <moderator-dashboard/> }
  @default { <user-dashboard/> }
}

ด้วย syntax ใหม่จะทำเรื่อง type-narrowing ในแต่ละ case ได้ดีขึ้นซึ่งถ้าเป็น ngSwitch จะทำไม่ได้ครับ

Built-in for loop

สำหรับ for loop syntax ใหม่จะหน้าตาแบบนี้ครับ

@for (user of users; track user.id) {
  {{ user.name }}
} @empty {
  Empty list of users
}

ใน @for จะจำเป็นต้องใส่ track โดยจะตามด้วย expression ซึ่งจะช่วยเรื่อง performance ได้ดีขึ้น อีกทั้งถ้าใน collection นั้นมีสมาชิกเป็น 0 เรายังสามารถใช้ @empty เพื่อโชว์กรณีไม่มีสมาชิกใน collection ได้อีกด้วย

ด้านล่างจะเป็นตารางเปรียบเทียบประสิทธิภาพ @for แบบ syntax ใหม่กับ ngFor แบบเก่าครับ โดยประสิทธิภาพจะดีกว่าเดิมมากถึง 90% เลยครับ

for-loop-performance
เปรียบเทียบประสิทธิภาพระหว่าง @for แบบใหม่กับ ngFor แบบเก่า

control flow syntax ใหม่ออกมาเป็น developer preview แล้วใน Angular 17 นี้โดยเราสามารถทำการ migrate จาก syntax แบบเก่าโดยใช้คำสั่งด้านล่างนี้ครับ

ng generate @angular/core:control-flow

Deferrable view

ด้วย block syntax แบบใหม่ นอกจากเรื่องของ control flow แล้ว ทางทีมยังออก deferrable view เพื่อใช้ในการทำ lazy loading ซึ่งนอกจากจะทำให้ประสิทธิภาพของแอพดีขึ้นแล้ว ยังทำให้ developer experience ดีขึ้นอีกด้วยครับ

สมมุติว่าเรามี blog แล้วอยากจะ lazy load ส่วนที่เป็น user comment ถ้าก่อนหน้านี้เราจะต้องใช้ ViewContainerRef และต้องคอยจัดการ cleanup, error หรือ show placeholder ด้วยตัวเองทั้งหมด แต่ด้วยการมาของ Deferrable view จะช่วยทำให้เราทำเรื่องที่ว่ามาทั้งหมดได้ง่ายขึ้นเยอะเลย เพียงแค่เขียนแบบนี้

@defer {
  <comment-list />
}

โดยส่วนนี้ Angular จะทำตอน compile time ไม่ว่าจะเป็นการหา component directive หรือ pipe ที่อยู่ใน @defer block แล้วทำการสร้าง dynamic import และจัดการขั้นตอนการ loading และการเปลี่ยนระหว่าง state ให้เราทั้งหมด

ถ้าเราจะทำการ lazy load component ตอนที่ยังไม่โชว์ใน viewport เราอาจจะใช้ IntersectionObserver API ช่วยแต่ก็อาจจะทำได้ยาก แต่ด้วย @defer เราสามาราถทำได้ง่ายๆแบบนี้

@defer (on viewport) {
  <comment-list />
} @placeholder {
  <!-- A placeholder content to show until the comments load -->
  <img src="comments-placeholder.png">
}

จากตัวอย่างข้างบน Angular จะทำการ render placeholder ก่อนและรอจนกว่า viewport จะโชว์ เมื่อโชว์แล้วจึงจะทำการโหลด <comment-list/> และพอโหลดเสร็จก็จะแทนที่ placeholder ด้วย <comment-list/> ครับ

นอกจากนี้เรายังสามารถ handler loading state และ error state ได้ด้วยแบบนี้

@defer (on viewport) {
  <comment-list/>
} @loading {
  Loading…
} @error {
  Loading failed :(
} @placeholder {
  <img src="comments-placeholder.png">
}

Deferrable view ยัง built-in มาด้วย trigger หลายตัวดังนี้ครับ

  • on idle จะทำการ lazy load เมื่อ browser ไม่ได้ทำงานหนักอะไร
  • on immediate ทำการ lazy load แบบอัตโนมัติโดยไม่ block browser
  • on timer(<time>) ทำการดีเลย์ด้วยเวลาที่กำหนด
  • on viewport และ on viewport(<ref>) สามารถกำหนด ref สำหรับ anchor element เมื่อ anchor element โชว์ก็จะทำการ lazy load component
  • on interaction และ on interaction(<ref>) ทำการ lazy load เมื่อ user มี interaction กับ element ที่กำหนด
  • on hover และ on hover(<ref>) lazy load เมื่อ user hover element ที่กำหนด
  • when <expr> เป็น custom trigger ทำให้เราสามารถ lazy load ตามเงื่อนไขที่กำหนดได้

Deferrable view ยังสามารถกำหนดให้ทำการ prefetch dependency ก่อนจะทำการ render component ได้ด้วยครับ โดยเราสามารถเพิ่ม prefetch ลงใน statement ของ @defer แบบนี้

@defer (on viewport; prefetch on idle) {
  <comment-list />
}

ปรับปรุง hybrid rendering

เมื่อเราใช้คำสั่ง ng new จะมี option ให้เราสามารถเพิ่ม hybrid rendering (SSR/SSG) ลงไปในแอพได้เลยแบบในรูปด้านล่างนี้ครับ

prompt ขึ้นให้เลือก option SSR/SSG ตอนสร้างแอพ Angular ใหม่

หรือเราจะเพิ่ม SSR ด้วยตัวเองโดยใช้คำสั่งนี้ครับ

ng new my-app --ssr

Hydration ออกจาก developer preview

หลังจากที่ออกฟีเจอร์ hydration มาเป็น developer preview ใน Angular 16 มาในเวอร์ชั่นนี้ตัว Hydration ออกจาก developer preview และจะถูกเปิดเป็น default เมื่อสร้างแอพใหม่ที่เป็น Server Side Rendering ครับ

package @angular/ssr ใหม่

ทางทีม Angular ได้ย้าย Angular Universal repo ไปที่ Angular CLI repo เพื่อทำให้ Server side rendering integrate กับ Angular CLI ได้ดีขึ้น

เราสามารถเพิ่ม hybrid rendering ลงไปในแอพที่มีอยู่แล้วได้ด้วยคำสั่ง

ng add @angular/ssr

โดยคำสั่งข้างบนจะทำการสร้าง server entry point, เพิ่มความสามารถในการ build แบบ SSR และ SSG และเปิด hydration เป็น default ให้เราเลยครับ ซึ่งตัว @angular/ssr เทียบเท่ากับ @nguniversal/express-engine ถ้าแอพไหนยังใช้ตัว express engine อยู่ พอใช้คำสั่งด้านบนก็จะทำการ migrate เป็น @angular/ssr ให้เราเลยครับ

Deploy app ด้วย SSR

ถ้าเราใช้ Firebase ในการ deploy ด้วยฟีเจอร์ใหม่ของ Firebase จะทำให้เราแทบไม่ต้อง config ก็สามารถ deploy ได้ง่ายๆแบบนี้ครับ

firebase experiments:enable webframeworks
firebase init hosting
firebase deploy

ส่วนถ้าใครใช้ monorepo ที่มีความซับซ้อนหรืออยากใช้ tool แบบ native ก็สามารถเพิ่ม AngularFire และ deploy ได้ด้วยคำสั่งนี้ครับ

ng add @angular/fire
ng deploy

สำหรับการ deploy ไปยัง edge worker ทางทีมได้เพิ่มการซัพพอร์ต ECMAScript module สำหรับ Angular SSR และเพิ่ม fetch backend สำหรับ HttpClient และยังร่วมมือกับทีม CloudFlare เพื่อเพิ่มขั้นตอน ในการช่วย deploy Angular แอพด้วยครับ

Lifecycle hook ใหม่

เพื่อปรับปรุง performance ของ Angular SSR และ SSG ทางทีมจึงต้องการหลีกเลี่ยงการทำ DOM emulation และ DOM manipulation โดยตรง ดังนั้นจึงจำเป็นต้องเพิ่ม lifecycle เพื่อใช้ในการ interact กับ element หรือเพื่อเริ่มการทำงานของ third-party library หรือวัดขนาด element เป็นต้น โดย lifecycle ใหม่จะมีดังนี้ครับ

  • afterRender ใช้ register callback ที่ถูกเรียกทุกครั้งที่แอพ render เสร็จ
  • afterNextRender ใช้ register callback ที่ถูกเรียกครั้งต่อไปที่แอพ render เสร็จ

เฉพาะ browser เท่านั้นที่จะเป็นคนเรียก lifecycle hook ทั้งสองอันนี้ทำให้เราสามารถใส่ custom DOM logic ใน component ได้อย่างปลอดภัยครับ ยกตัวอย่างถ้าเราอยากเริ่มต้นการทำงานของ chart library เราสามารถใช้ afterNextRender ได้แบบนี้

@Component({
  selector: 'my-chart-cmp',
  template: `<div #chart>{{ ... }}</div>`,
})
export class MyChartCmp {
  @ViewChild('chart') chartRef: ElementRef
  chart: MyChart | null

  constructor() {
    afterNextRender(
      () => {
        this.chart = new MyChart(this.chartRef.nativeElement)
      },
      { phase: AfterRenderPhase.Write }
    )
  }
}

แต่ละ lifecycle hooks สามารถกำหนด option phase (read, write) เพื่อที่ Angular จะใช้ลด layout trash และเพิ่ม performance ได้ครับ


Vite และ esbuild

ใน Angular 16 ทางทีมได้เปิดให้ใช้ Vite และ esbuild เป็น develop preview ให้ได้ลองใช้ทั้งสองตัวนี้ในการ build และพบว่ามีความเร็วในการ build ดีขึ้นถึง 67% ใน Angular 17 ทางทีมเลยเลยเปิดให้ใช้ Vite และ esbuild เป็น default ในการ build Angular แอพครับ

นอกจากนี้ถ้าเราใช้ hybrid rendering จะพบว่าความเร็วในการ build ดีขึ้นถึง 87% และความเร็วตอน edit แล้ว refresh เวลาใช้ ng serve เพิ่มขึ้น 80%

ิbuild-pipeline-compare-chart
เปรียบเทียบ build pipeline ระหว่าง vite + esbuild กับ webpack แบบเก่า

Dependency injection ใน DevTools

ด้วย debugging API ใหม่ ทางทีมได้สร้าง UI เพื่อใช้ทำการ inspect Angular แอพซึ่งจะทำให้เราสามารถ

  • ดู dependency ของ component ใน component inspector
  • ดู injector tree และ dependency resolution path
  • ดู provider ที่ถูกประกาศในแต่ละ injector

สามารถดูเกี่ยวกับ DevTool เพิ่มเติมได้ที่นี่ ครับ

ตัวอย่างการดู Dependency injection ใน DevTools

Standalone API ตั้งแต่ต้น

หลังจากที่เก็บ feedback เกี่ยวกับ standalone component มาประมาณปีครึ่ง ทางทีม Angular ก็ได้มั่นใจและทำการเปิดให้ใช้ Standalone API ตั้งแต่ต้นใน Angular 17 นี้ครับ โดยคำสั่ง ng generate จะทำการสร้าง standalone component, directive และ pipe ให้เราเลยครับ

ส่วน NgModules ก็ยังอยู่ครับไม่ได้ไปไหน แต่ทางทีมก็แนะนำให้เราเปลี่ยนไปใช้ standalone API แทน ซึ่งก็จะมี schematic ที่ใช้ migrate ให้เป็น standalone API ตามคำสั่งด้านล่างนี้ครับ

ng generate @angular/core:standalone

ขั้นต่อไปของ reactivity

หนึ่งในสิ่งที่เป็นการเปลี่ยนแปลงสำคัญของ Angular framework ก็คือการเปลี่ยนไปใช้ signal-based reactivity system ครับ ซึ่งทางทีมก็ทำงานหนักเพื่อที่จะให้ยังเป็น backward compatible และทำงานร่วมกับ Zone.js ได้

โดยใน Angular 17 นี้ตัว Angular Signal ก็ได้ออกจาก developer preview แล้ว ยกเว้นแต่ฟังก์ชั่น effect ที่ยังคงอยู่ใน developer preview ครับ

หลังจากนี้ทางทีมก็จะทำต่อในเรื่อง signal-based input, view query และอื่นๆอีกมากมายที่จะมาพร้อมกับ Angular 18 อย่างแน่นอน


ทดลองซัพพอร์ต View Transitions API

View Transitions API สามารถทำให้เราทำเรื่อง Transition ใน DOM ได้ง่ายขึ้น โดยใน Angular 17 ก็จะมีฟีเจอร์ที่ซัพพอร์ต View Transition API โดยตรง ซึ่งจะอยู่ใน Angular router โดยจะมีชื่อว่า withViewTransitions ครับ เราสามารถใช้ฟีเจอร์นี้ได้แบบนี้

bootstrapApplication(App, {
  providers: [provideRouter(routes, withViewTransitions())],
})

withViewTransitions รับ config option object ที่มี property onViewTransitionCreated ที่เป็น callback เพื่อใช้ควบคุม

  • ว่าจะ skip บาง animation
  • เพิ่ม class เพื่อ custom animation และเอา class เหล่านี้ออกเมื่อ animation เสร็จ

Input value transforms

สมมุติว่าเรามี component ที่รับ input เป็น boolean แบบนี้

@Component({
  standalone: true,
  selector: 'my-expander',
  template: `…`,
})
export class Expander {
  @Input() expanded: boolean = false
}

และอยากจะส่งค่าไปใน component แบบนี้

<my-expander expanded />

เราจะเจอ error ประมาณว่า string is not assignable to boolean ซึ่งตัว Input value transform ก็จะมาช่วยเราในจุดนี้ครับ โดยเราสามารถแก้เคสด้านบนได้แบบนี้

@Component({
  standalone: true,
  selector: 'my-expander',
  template: `…`,
})
export class Expander {
  @Input({ transform: booleanAttribute }) expanded: boolean = false
}

Style และ styleUrls เป็น string

ใน Angular component ตรงส่วนของ styles และ styleUrls ปกติรับแต่ array แต่ใน Angular 17 สามารถใส่เป็น string ได้เลยแบบนี้ครับ

@Component({
  styles: `
    ...
  `
})

// หรือแบบนี้
@Component({
  styleUrl: 'styles.css'
})

ซึ่งเราก็ยังสามารถใส่เป็น array ได้อยู่เหมือนเดิมครับ แต่กรณีที่มีแค่ไฟล์เดียวแบบใหม่จะสะดวกกว่า


ไฮไลต์อัพเดทจาก Community

  • HttpClient สามารถใช้ fetch ใน backend ได้แล้ว
  • custom HttpTransferCache
  • ซัพพอร์ต nameChunks ใน application builder

อัพเดทอื่นๆ

  • เพิ่ม preconnect ใน image directive อัติโนมัติ
  • Defer loading animations module

สรุป

ใน Angular 17 นี้ ทางทีม Angular ก็ได้เพิ่มฟีเจอร์ใหม่ๆ มากมายไม่ว่าจะเป็น control flow syntax แบบใหม่, deferrable view, ปรับปรุง hybrid rendering และอื่นๆอีกมากมาย เพื่อให้เข้ากับ Angular ในยุคเรเนซองส์ ทางทีมจึงมีการเปลี่ยนโลโก้ใหม่ให้ดูทันสมัยยิ่งขึ้น และยังมีการสร้างเว็บ document ใหม่ เพื่อรองรับการเปลี่ยนแปลงที่มี และอัพเดทใหม่ๆที่กำลังจะมาถึงด้วยครับ


Reference

Introducing Angular v17 Last month marked the 13th anniversary of Angular’s red shield. AngularJS was the starting point for a new wave of JavaScript frameworks emerging to support the increasing need for rich web experiences.