มีอะไรใหม่บ้างใน Angular 5

Nuttavut Thongjor

ผ่านมาแล้วสามวันกับการมาของ Angular เวอร์ชัน 5 ตัวเต็ม ที่มาพร้อมกับประสิทธิภาพที่ดีขึ้น และคุณสมบัติใหม่ๆที่คุณไม่ได้ขอ แต่ทีมงานก็ใส่มาให้คุณ

ผมพึ่งสอนคอร์ส Angular เวอร์ชัน 4 ที่บริษัทแห่งหนึ่งเสร็จเมื่อวันพุธที่ผ่านมา เราย้ำเสมอในคอร์สว่าอย่าเจ็บปวดกับ Angular ด้วยการใช้ API ลึกๆ แม้พี่แกจะไม่เปลี่ยนทั้งกะบิเหมือนย้ายจากเวอร์ชันแรกมาเวอร์ชันสอง แต่ด้วยความเป็น Angular ที่ร่านเงียบเสมอ มันก็ต้องมี API บางอย่างที่เปลี่ยนบ้างหละ บางตัวอาจไม่ Depreceted ทันที บางตัวอาจประหารทิ้งด้วยการไม่ให้ใช้ แต่ท้ายสุดเปลี่ยนก็คือเปลี่ยน

บทความนี้เราจะดูกันซิว่า Angular 5 นั้นมีอะไรใหม่บ้าง อัพเกรดแล้วชีวิตจะง่ายขึ้นไหม ตลาดวายจนตาม React ทันรึเปล่า หรือ Angular ตอนนี้จะไปเทียบท่าแถวฝั่งจีน ด้วยเทคโนโลยีเสินเจิ้นแบบ Vue...

Angular Universal กับการทำ SSR ที่ไฉไลขึ้น

Angular Universal ถือเป็นโปรเจคที่ชุบชีวิตให้ Angular สามารถทำ Server-Side Rendering (SSR) ได้ หากใครได้มีโอกาสทำ SSR ด้วยไลบรารี่ดังกล่าว จะพบว่าหนึ่งในปัญหาของการใช้งานคือบางสถานะเกิดขึ้นแล้วบนฝั่งเซิฟเวอร์ แต่ยังไปเกิดซ้ำอีกครั้งบนฝั่งเบราเซอร์

SSR ก่อน Angular 5

จากรูปข้างต้น เรามี API Server สำหรับให้บริการร้องขอข้อมูลเพื่อใช้ในการแสดงผล เมื่อเราทำ SSR เราจัดเตรียมเนื้อหาให้เสร็จตั้งแต่ขั้นตอนทางฝั่งเซิฟเวอร์ ไม่ว่าจะเป็นการร้องขอข้อมูลจาก API เพื่อสร้างผลลัพธ์เป็นก้อน HTML ก่อนส่งกลับมาที่ฝั่งเบราเซอร์ เป็นข้อมูลที่สมบูรณ์พร้อมในการแสดงผล โดยไม่ต้องผ่านกลไกของ JavaScript อีก

ทว่าเมื่อข้อมูลส่งกลับมาถึงเบราเซอร์ ข้อมูลนั้นแสดงผลได้อย่างถูกต้อง แต่เมื่อ Angular และโค้ดที่เราเขียนถูกโหลดขึ้นมา เรากลับพบว่ายังคงมีการส่งการร้องขอไปที่ /articles?page=1 อีกครั้ง จังหวะนี้อาจทำให้หน้าเพจกระพริบจากการแสดงผลซ้ำอีกครั้งได้

ปัญหาดังกล่าวเกิดจากการทำ SSR ทำให้เรานำ HTML ที่ได้รับจากฝั่งเซิฟเวอร์มาใช้แสดงผลได้ก็จริง แต่เมื่อ Angular ถูกโหลด ตัวมันเองก็ต้องการที่ยืน มันจึงเริ่มทำการติดตั้งตัวเองบนหน้าเพจของเรา ในจังหวะที่ Angular กำลังทำงานตอนนั้นยังไม่มีข้อมูลบทความให้แสดงผล มันจึงแทนที่ทับ HTML ก้อนเดิมด้วยข้อมูลว่างทำให้ตอนนี้หน้าเพจเราโล่งโจ้ง ต่อมาเมื่อกระบวนการร้องขอข้อมูลจาก API เสร็จสมบูรณ์ Angular จึงนำข้อมูลดังกล่าวมาใช้แสดงผลอีกที นั่นจึงเป็นเหตุผลที่ว่าทำไมหน้าเพจของเราจึงกระพริบ

เพื่อป้องกันปัญหาดังกล่าว วิธีการของเราจึงหน่วย Angular ไว้ด้วยการให้มันจัดการสถานะภายใน ไม่ว่าจะเป็นการร้องขอข้อมูลจนกระทั่งการแสดงผลข้อมูลของมันให้เสร็จสิ้นก่อน แล้วจึงนำเจ้าตัว Angular มาแทนที่ทับ HTML จากเซิฟเวอร์อีกที เมื่อทำเช่นนี้เราก็จะไม่เห็นหน้าเพจกระพริบอีกต่อไป นี่คือวิธีการของการใช้งานไลบรารี่ชื่อ Preboot

สำหรับ Angular 5 นั้นจะทำให้ชีวิตเราดีขึ้น (มั้งนะ) ด้วย TransferState API ผลลัพธ์จากการใช้ API ตัวนี้ จะไม่ปรากฎการร้องขอข้อมูลเป็นครั้งที่สองจากหน้าเบราเซอร์อีกต่อไป

Transition API

เราร้องขอข้อมูลจาก API เพื่อได้ข้อมูลกลับมาแสดงผลบนหน้าเพจ แต่เรารู้อยู่แล้วว่าการทำ SSR นั้นเราร้องขอข้อมูลตั้งแต่การทำงานของเซิฟเวอร์ เมื่อเป็นเช่นนี้เซิฟเวอร์ก็แค่ฝังข้อมูลมาในหน้าเพจ ว่าไอ้เจ้าก้อนข้อมูลที่ต้องใช้ในการแสดงผลแท้จริงนั้นคืออะไร ในจังหวะที่ Angular ถูกปลุกขึ้นมาทำงานบนหน้าเบราเซอร์ Angular ไม่จำเป็นต้องส่งรีเควสไปร้องขอข้อมูลจาก API อีกต่อไป เพียงแค่ใช้ข้อมูลที่ SSR Server แปะมาให้ เพียงแค่นี้ก็เสร็จสิ้น นั่นหละฮะคือความไฉไลระดับสูงของ Angular 5 ในการทำ SSR (ทำไมรู้สึกคุ้นๆเหมือนฝั่ง React เลยแฮะ~)

RxJS 5.5 และ Lettable Operators

Angular 5 มาพร้อมกับ RxJS 5.5.2+ ที่มีคุณสมบัติในการจัดการกับ Operators ได้ดีขึ้นทั้งในเรื่องขนาดไฟล์และการใช้งาน

สมมติเราได้รับ articles เป็น Observable ผลลัพธ์จากการร้องขอข้อมูลจาก API Server เราสามารถสั่ง filter เพื่อกรองเฉพาะบทความที่เผยแพร่แล้ว และใช้ map เพื่อดึงเฉพาะผู้เขียนบทความออกมาได้

JavaScript
1import { Observable } from 'rxjs/Observable'
2import 'rxjs/add/operator/map'
3import 'rxjs/add/operator/filter'
4
5const authors = articles
6 .filter((article) => article.published)
7 .map((article) => article.author)

กระบวนการดังกล่าวเราต้องนำเข้า Operators ต่างๆ เพื่อเสริมให้ Observable ของเรารู้จักและใช้งานสิ่งเหล่านั้นได้

เมื่อเราเขียนประโยค import 'rxjs/add/operator/map' เบื้องหลังการถ่ายทำคือการเพิ่ม map เข้าไปยัง protptype ของ Observable เป็นผลให้เกิดการขยายขีดความสามารถให้ Observable ของเราเรียกใช้ map ได้นั่นเอง

JavaScript
1Observable.prototype.map...

วิธีการเช่นนี้ก็มีข้อจำกัดอยู่นั่นคือ Observable ของเราจะมี prototype ที่บวมขึ้นเรื่อยๆ แม้เราจะทำการเพิ่ม Operators เหล่านี้จากไลบรารี่อื่น หากแต่เรียกใช้ Observable ตัวนั้นจากในไฟล์เรา อันความอวบนั้นก็จะปรากฎชัดในไฟล์เราตามไปด้วย แม้เราจะไม่ต้องการอ้างถึงมันในภายหลังเลยก็ตาม

เบื้องหลังความอ้วนยังมียันฮี... ไขมันตัวดียังถูกดูดได้ แต่ไม่ใช่กับเมธอดที่เพิ่มเข้าไปใน prototype แม้เราจะไม่ใช้ก็มิอาจกำจัดได้จากชัวิตเรา

Tree Shaking เป็นหนึ่งในคุณสมบัติของ ES6 ที่ Webpack รองรับการทำงาน ด้วยคุณสมบัตินี้จะช่วยกำจัดส่วนเกินที่เราไม่ได้ใช้ออกไปจากไฟล์ผลลัพธ์ของเรา ทำให้ขนาดไฟล์ก่อนใช้งานมีขนาดเล็กลง

ทว่าการเพิ่มเมธอดเข้าไปใน prototype ทุกครั้งที่มีการ import เช่นนี้ มิอาจเติมเต็มให้การทำงานของ Tree Shaking สมบูรณ์ได้ RxJS ทราบถึงจุดนี้จึงได้เพิ่ม Lettable Operators เข้ามาเพื่อแก้ปัญหานี้

Letterable Operators เป็นฟังก์ชันที่คืนค่ากลับเป็นอีกฟังก์ชัน เราจึงสามารถเรียก Operators เหล่านี้ต่อเนื่องกันเรื่อยๆได้

Observable ได้เตรียมเมธอดชื่อ pipe สำหรับการขยายความสามารถของตัวเองด้วยการบอกว่า Observable นั้นต้องประกอบด้วย Operators อะไรบ้าง

JavaScript
1import { Observable } from 'rxjs/Observable'
2import { map, filter } from 'rxjs/operators'
3
4const authors = articles.pipe(
5 filter((article) => article.published),
6 map((article) => article.author)
7)

เราสามารถนำเข้า Operators กลุ่มนี้ได้จาก rxjs/operators โดยใช้คุณสมบัติของ Composition ผ่าน pipe ดังตัวอย่างข้างต้น

HttpClient และ Interceptor

ความเปลี่ยนแปลงนี้ไม่ได้เกิดขึ้นซะทีเดียวใน Angular 5 ครับ หากแต่มีมาตั้งแต่ Angular 4.3 แล้วหละ

Angular 4.3 ได้ออก HttpClient ตัวใหม่ที่มีความสามารถมากกว่าเดิม

โดยปกติเรามักนิยมออกแบบ RESTful API ของเราด้วยการส่งข้อมูลไปกลับในรูปแบบ JSON Angular รู้ดีถึงความจริงข้อนี้ HttpClient ตัวใหม่จึงจัดการแปลงข้อมูลจากเซิฟเวอร์ให้เป็น JSON ให้กับเราทันที โดยที่เราไม่ต้องเรียก response.json() เฉกเช่นการใช้งานตัวก่อนหน้า สิ่งที่เราต้องทำเพิ่มเติมมีแค่บอกใบ้ให้ HttpClient ทราบว่าหน้าตาของก้อนข้อมูล JSON นี้ควรมีลักษณะเช่นไร

TypeScript
1// ส่วนกำหนดรูปร่างของ JSON ที่คืนกลับจากเซิฟเวอร์
2export interface ArticleResponse {
3 article: Article;
4}
5
6// HttpClient ตัวใหม่อยู่ใต้แพคเกจ @angular/common/http
7import { HttpClient } from '@angular/common/http';
8
9// โดยเราต้อง inject มันผ่าน constructor ดังนี้
10constructor(private http: HttpClient)
11
12// ส่วนของการเรียกใช้งาน
13this.http
14 .get<ArticlesResponse>(`/api/articles?page=${page}`);

นอกจากคุณสมบัติข้างต้น HttpClient ตัวใหม่นี้ยังสามารถเป็น Interceptor ได้อีกด้วย

จินตนาการถึงแอพพลิเคชันที่ต้องมีระบบผู้ใช้งาน เราจำเป็นต้องส่งค่า token กลับไปยังฝั่งเซิฟเวอร์เพื่อเป็นการบ่งบอกว่าเราคือใคร ด้วยเหตุนี้ทุกๆรีเควสที่เรียกผ่าน HttpClient จะต้องมีการตั้งค่า token ทั้งหมด

แน่นอนว่าการตั้งค่า token เราสามารถทำได้โดยง่าย เพียงแค่เซ็ต HTTP Headers ก่อนทำการส่งรีเควสก็เป็นอันจบสิ้น หากแต่การทำเช่นนี้จะไม่ง่ายอีกต่อไปเมื่อการส่งรีเควสมีมากกว่าหนึ่งครั้ง

การต้องมานั่งตั้งค่า HTTP Headers ทุกครั้งก่อนส่งรีเควสเป็นเรื่องเสียเวลา Angular จึงได้เตรียม Interceptor ไว้ให้ใช้ เพื่อเป็นการแก้ไข HTTP Request ก่อนทำการส่งออกจริงไปยังเซิฟเวอร์ เมื่อเป็นเช่นนี้เราจึงสามารถเขียน Interceptor ขึ้นมาเพื่อทำการใส่ส่วนของ HTTP Headers เข้าไป โดยไม่ต้องไปนั่งใส่ในทุกๆรีเควส

JavaScript
1@Injectable()
2export class AuthInterceptor implements HttpInterceptor {
3 intercept(
4 req: HttpRequest<any>,
5 next: HttpHandler
6 ): Observable<HttpEvent<any>> {
7 const token = localStorage.getItem('access-token')
8 const authReq = req.clone({
9 headers: req.headers.set('Authorization', `Bearer ${token}`),
10 })
11
12 return next.handle(authReq)
13 }
14}

ประสิทธิภาพของคอมไพเลอร์

เป็นที่ทราบดีว่าคอมไพเลอร์ของ Angular มีอยู่ด้วยกันสองตัวคือ Just-in-Time (JiT) และ Ahead-of-Time (AoT) โดย JiT นั้นมีข้อดีคือคอมไพล์ความเปลี่ยนแปลงได้เร็วกว่า AoT มาก จึงเหมาะสมกับการพัฒนา ในขณะที่ AoT จะได้ไฟล์ผลลัพธ์ที่เล็ก ทั้งยังคอมไพล์ให้เสร็จตั้งแต่แรก จึงไม่จำเป็นต้องแบกตัวคอมไพเลอร์ไปใช้งานด้วยแบบ JiT เหตุนี้ AoT จึงเหมาะสมกว่าในการใช้งานกับ Production

การใช้ JiT บน development ก็ไม่น่าจะมีปัญหาอะไรทุกอย่างก็ดูเหมือนจะราบลื่นดี แต่ความเป็นจริงไม่เป็นเช่นนั้น การใช้งานบางอย่างของเราไม่เป็นปัญหาบน JiT แต่ดันไปเกิดข้อผิดพลาดบน AoT ซะงั้น เช่น เราสามารถประกาศพรอพเพอร์ตี้แบบ private แล้วนำตัวแปรนี้ไปใช้บน template ได้สำหรับการใช้งานคู่กับ JiT แต่สิ่งนี้จะทำให้เกิดข้อผิดพลาดขึ้นทันทีเมื่อคอมไพล์กับ AoT

เมื่อ AoT คือคอมไพเลอร์ตัวจริงที่เราต้องใช้เพื่อผลิตผลลัพธ์ก่อนนำขึ้น Production มันจะดีกว่าไหมหละถ้าเราจะใช้ AoT ทั้งใน development และ production ซะเลย?

เป็นความคิดที่ดีครับ แต่ช่างน่าเศร้าที่ AoT มันช่าง build ช้าซะเหลือเกิน...

Angular 5 ได้ปรับปรุงประสิทฑิภาพของ AoT การทดสอบพบว่าจากการลองคอมไพล์ด้วย AoT จากเดิมที่ใช้เวลา 40 วินาที เมื่อใช้ AoT ตัวใหม่พบว่าเหลือเพียง 2 วินาทีเท่านั้น คุณพระ!

เมื่อ AoT มีพัฒนาการด้านความเร็วที่ดีขึ้นเช่นนี้ จึงมิอาจห้ามใจให้ลองใช้มันกับ development ซะแล้ว ออกคำสั่งนี้กันเลย~

Code
1ng serve --aot

Forms และ Validation

แทบไม่มีแอพพลิเคชันไหนที่ไม่มีฟอร์ม ทุกแอพพลิเคชันที่มีฟอร์มก็น้อยนักที่จะไม่มีการตรวจสอบข้อมูล

การตรวจสอบข้อมูลหรือ validation นั้น บางครั้งเราก็จำเป็นต้องทำการตรวจสอบจากฝั่งเซิฟเวอร์ เช่น การตรวจสอบว่าอีเมล์ที่กรอกตอนสมัครสมาชิกมีผู้อื่นใช้หรือยัง เหล่านี้ล้วนไม่ควรตรวจสอบทุกครั้งที่เราพิมพ์ค่าข้อมูลลงไปในช่องรับข้อมูล เพราะนั่นจะทำให้เกิดการยิงรีเควสไปตรวจสอบกับเซิฟเวอร์มากมายไปหมด

Angular 5 ได้เพิ่ม updateOn เพื่อใช้งานคู่กับฟอร์มในกรณีต้องการบอกว่า FormControl ตัวนี้ให้ทำการตรวจสอบข้อมูลเฉพาะเหตุการณ์ blur หรือ submit เท่านั้น

HTML
1<!-- กรณีของ Template Driven Form --!>
2<input name="firstName" ngModel [ngModelOptions]="{updateOn: 'blur'}">
3
4<!-- หรือ --!>
5<form [ngFormOptions]="{updateOn: 'submit'}">
6
7<!-- กรณีของ Reactive Forms --!>
8new FormGroup(value, {updateOn: 'blur'}));

อื่นๆ

นอกเหนือจากสิ่งที่ผมกล่าวไปแล้วข้างต้น ยังมีอีกหลายสิ่งที่ได้รับการเปลี่ยนแปลงและปรับปรุงใน Angular 5เช่น การเพิ่ม Router Lifecycle Events ตัวใหม่ การเปลี่ยนแปลงของ Pipes บางตัว เป็นต้น ผู้อ่านสามารถศึกษาเพิ่มเติมได้จาก Version 5.0.0 of Angular Now Available

วิธีการอัพเกรดสู่ Angular 5

การเปลี่ยนเวอร์ชันหมายถึงบางสิ่งที่เปลี่ยนไป Angular จึงเตรียมเว็บสำหรับการแนะนำว่าหากคุณจะเปลี่ยนเวอร์ชันสิ่งใดที่คุณควรทำบ้าง เว็บดังกล่าวนั่นก็คือ Angular Update Guide

How to Upgrade

สรุป

ถามว่าการอัพเกรด Angular รอบนี้เจ็บปวดหรือไม่ บอกเลยว่าไม่ เพราะมันเจ็บปวดมาตั้งแต่ยังไม่อัพไปสู่เวอร์ชัน 5 แล้วไงหละ ฮาๆ ตั้งแต่การมาของ HttpClient ในเวอร์ชัน 4.3 หรือการ deprecate OpaqueToken และอื่นๆ

เอกสารอ้างอิง

Éverton Roberto Auler (2017). Angular 5 universal with Transfer State using @angular/cli. Retrieved November, 4, 2017, from https://medium.com/@evertonrobertoauler/angular-5-universal-with-transfer-state-using-angular-cli-19fe1e1d352c

Stephen Fluin (2017). ES8 was Released and here are its Main New Features. Retrieved November, 4, 2017, from https://blog.angular.io/version-5-0-0-of-angular-now-available-37e414935ced

Lettable Operators. Retrieved November, 4, 2017, from https://github.com/ReactiveX/rxjs/blob/master/doc/lettable-operators.md

linkTransferState. Retrieved November, 4, 2017, from https://next.angular.io/api/platform-browser/TransferState

Philippe Martin (2017). Using TransferState API in an Angular v5 Universal App. Retrieved November, 4, 2017, from https://blog.angularindepth.com/using-transferstate-api-in-an-angular-5-universal-app-130f3ada9e5b

สารบัญ

สารบัญ

  • Angular Universal กับการทำ SSR ที่ไฉไลขึ้น
  • RxJS 5.5 และ Lettable Operators
  • HttpClient และ Interceptor
  • ประสิทธิภาพของคอมไพเลอร์
  • Forms และ Validation
  • อื่นๆ
  • วิธีการอัพเกรดสู่ Angular 5
  • สรุป
  • เอกสารอ้างอิง