จาก Ep1 ได้เขียนถึง backend ไปแล้ว ใน บล็อกนี้จึงเขียนเกี่ยวกับ frontend ล้วนๆ โดยขอกล่าวถึง concept พื้นฐาน ของ Angular framework และ การวางโครงสร้างไฟลล์ ในโปรเจคนี้ แต่อาจจะยกมาไม่หมด ยกมาแค่ส่วนที่น่าสนใจจริงๆ เท่านั้น
เพื่อนๆ สามารถ เข้าไปดู Source Code ที่ Github ของผม ที่ https://github.com/Ratimon/full-stack-blockchain-network อย่างไรก็ตามโปรเจคนี้ไม่ได้เหมาะกับ production ยังมีโค้ดที่ต้อง พัฒนาและแก้อีกเยอะหากจะนำไปใช้จริงครับ
ใน บล็อกที่แล้ว Ep1 ได้กล่าวเกี่ยวกับ backend ซึ่งได้แบ่งเป็นออกเป็น 3 ฟีเจอร์คือ
- wallet
- faucet
- explorer
ดังนั้น ผมได้สร้างและแบ่งโฟลเดอร์ออกเป็นหลักๆเพิ่มอีก 3 ส่วน คือ wallet/, faucet/ และ explorer/ ตามภาพ

และได้กำหนดให้เป็น Lazy loading feature ดังนี้ ที่ app.module.ts
Lazy Loading คือ design pattern ที่กำหนดให้จะมีการโหลด Object ก็ต่อเมื่อถึงจุดใดจุดหนึ่งที่กำหนดไว้เท่านั้น

ในที่นี้ จุดประสงค์หลักก็คือ ทำให้เราสามารถทำ routing แยก feature ได้อย่างง่ายดาย นอกจากนี้ การกำหนดให้ Angular โหลดโค้ดเมื่อต้องการเรียกใช้เท่านั้น เป็นการ optimize web performance แบบหนึ่งอีกด้วย
Component architecture: พื้นฐานเกี่ยวกับ Angular
Angular เป็น framework แบบ component-based โดยไอเดียหลักๆก็คือจะแบ่งหน้าเว็บออกเป็นหลายๆส่วน เรียกว่า web component ซึ่งทำให้สามารถนำมาใช้ซ้ำได้
หากเพื่อนๆ ไม่ชินกับ Angular ลองใช้ React.js และ Vue.js
โดยหากมี component ใหนที่ใช้ข้าม module component นั้นจะถูกนำไปรวมที่ โฟลเดอร์ shared ตามภาพ

โดย component จะแบ่งได้เป็น สองแบบคือ
- Stateful Component, Smart Component หรือ Container Component
- Stateless Component, Dump Component หรือ Presentational Component
Stateful component จะมีหน้าที่คือจัดการทุกอย่างเกี่ยวกับ data หรือ เชื่อมต่อกับส่วนของ service จากนั้นส่งต่อข้อมูลต่อไปยัง Stateless Component
Stateless Component มีหน้าที่คือแสดงผลของ data ที่ถูกส่งต่อมา และสามารถ ปล่อย event ไปยัง Stateful component นั่นเอง
Service คือส่วนที่รวบรวม หรือ encapsulate business logic ของแอพลิเคชั่นไว้ ส่งผลให้ ส่วนในการแสดงผล (component) กับ business logic แยกกันทำให้โค้ดเป็นโมดูล อ่านง่าย และสามารถใช้ซ้ำได้
โดยโครงสร้าง ของ angular สามารถเขียนให้อยู่ในรูปดังนี้

credit: http://training.fabiobiondi.io/2017/07/10/create-an-accordion-component-in-angular-parent-children-communication/
เห็นได้ว่าการเชื่อมต่อระหว่าง Stateful component กับ Stateless Component เป็นแบบไปกลับทั้งสองทาง หรือ “two-way data-binding”
อาจจะยังไม่เห็นภาพกัน งั้นมาดูที่ module แรกกันเลยซึ่งก็ คือ wallet กันครับ
แต่ก่อนอื่น หลังจากโหลด source coude มาแล้ว เราต้อง compile code จาก typescript เป็น javascript ก่อนครับ
npm run dev-front
Wallet Module
ภายใน wallet โมดูล ยังได้แบ่งโฟล์เดอร์ออกเป็นส่วนๆดังนี้
- components จะรวม Stateless Component หรือ
Presentational Component ทั้งหมดไว้ - containers รวม Stateful Component หรือ Container Component ไว้
- guards รวมไฟลล์ guard ซึ่ง การกำหนดสิทธิ์การเข้าถึง route ต่างๆ
- models รวมไฟลล์ model ซึ่งเป็นการกำหนด type ชนิดของข้อมูลเอาไว้ก่อน เพื่อลด bug ในโปรแกรม
- services รวมไฟลล์ service

จะเห็นได้ว่า ใน module นี้ มีอยู่ 2 container คือ <wallet> และ<wallet-dashboard>
<wallet> container
จากรูป <wallet> ประกอบไปด้วย 3 component คือ

- <wallet-existing> เข้าสู่ หน้าของ wallet dashboard container และใช้ wallet โดยใช้ private key ที่ wallet ถืออยู่แล้ว
- <wallet-recover> กู้ wallet โดยใช้ private key ในการ restore
- <wallet-create> สร้าง private key ขึ้นมาใหม่ แบบสุ่ม และ ใช้ wallet

เครื่องหมาย [] หมายถึง การ pass data down จาก container ลงไปยัง dump component
เครื่องหมาย () หมายถึง การ event up จาก dump component ขึ้นไปยัง container ครับ อย่างเช่น
<wallet-existing
[wallet]="wallet" (use)="onUse($event)">
สามารถเขียน เป็น diagram ให้เข้าใจง่ายๆ ได้ดังนี้

ประโยชน์ของโครงสร้างแบบนี้คือ เวลาต้องการไปเปลี่ยนแปลงข้อมูล input (เกี่ยวกับ business logic) เราแค่เข้าไปดูที่ส่วน ของ container ไม่ต้องเข้าไปดูทุก component

container จะทำการติดต่อ ผ่าน service ด้วย dependency injection pattern นั่นเอง โดยกำหนดให้ service เป็น parameter ชนิด private ใน constructor function ของ component class และ กำหนด decorator @Injectable() ให้กับ service class ตามภาพด้านล่าง นั่นเอง

จะเห็นได้ว่า wallet service ได้รวม business logic ทั้งหมด ซึ่งรวมไปถึง servie ของ wallet dashboard container ด้วย ซึ่ง service เหล่านี้แหละที่จะไปติดต่อกับ RESRTful API ในส่วนของ backend ผ่าน HttpClient module สำเร็จรูปที่มากับ Angular ยกตัวอย่างเช่น

<wallet-dashboard> container

เช่นเดียวกับ wallet container ได้แบ่ง stateless components ออกเป็น 4 ส่วนคือ
- <wallet-send> เพื่อให้ user สามารถโอนเงินออกได้
- <wallet-receive> เพื่อให้ user copy public key ของตัวเองได้
- <wallet-key> ดู private key ของตัวเอง
- <wallet-mine> ให้ wallet ตัวเองเป็น miner และสามารถดู Transaction Pool หรือ unspent transaction output ในขณะนั้นได้ ตามภาพด้านล่าง

เนื่องจากว่า ผมต้องการ ให้ balance หรือ ยอดเงินในบัญชี และ transaction pool อัพเดตแบบ real-time ผมจึงสร้าง class socket service ขึ้นมาเพื่อ รับ real-time data ที่ถูกส่งออกมาจาก backend ดังนี้

โดย library ที่ใช้ในส่วนของ front-end คือ ‘socket.io-client’ และ ‘socket.io’ ในส่วนของ backend

Faucet Module

ในส่วนนี้ ขอกล่าวถึงในส่วน ของ dump component บ้าง
<faucet-form> component
เนื่องจาก web app จำดป็นต้องมีการ validation เพื่อป้องกันการผิดพลาด เมื่อ user ใส่ข้อมูลผิด app จะแจ้งเตือนและไม่ให้ ติดต่อกับ backend ได้

จากรูป จะทำการ dependency injection ในส่วนของ FormBuilder และใช้ built-in validator แบบง่ายๆ ดังนี้
form = this.fb.group({
recipient: [
'',
Validators.compose([
Validators.required,
Validators.minLength(130),
Validators.maxLength(130)
])]
});
ซึ่งความหมายก็คือ public key ต้องเป็น string ความยาว 130 ตัว

ซึ่ง animation จะใช้ scss ในการแต่งดังนี้

Further Improvement: จำกัดการ request เงิน
เช่น 1 request ต่อ 1 public key ต่อ 1 ชั่วโมง + captcha
แต่ผมไม่มีเวลา เลยยังไม่ได้ implement ลงไปครับ 5555
Explorer Module

นอกจาก components, containers, model , และ services ที่ได้เกริ่นไปข้างต้นแล้ว ยังมี pipes

จุดประสงค์หลักของ pipe คือเพื่อจัดรูปแบบของ data ที่ส่งมาใหม่จาก backend ให้เหมาะต่อ users เวลาติดต่อกับ frontend
การประยุกต์ใช้ Customised Pipe กับการ filter data ด้วย search box
คือ เมื่อ user ใส่ address (หรือ pubic key) ลงไป สามารถคัด เอาเฉพาะ Transaction ที่เกี่ยวข้อง กับ public key นั้นๆ ไม่ว่าจะเป็น ผู้ส่ง หรือ ผู้รับ ก็ตาม จาก Transaction ทั้งหมด

ผมได้ใช้
[(ngModel)] = "searchText"
โดย syntax นี้ใช้สำหรับ 2-way data binding ซึ่งก็คือ
- เขื่อมต่อ model กับ view
- เขื่อมต่อ view กับ model
ตอนนี้เมื่อมีการเปลี่ยนแปลง model ส่วนของ view หรือ component ก็จะอัพเดตตามไปด้วย ในทางกลับกัน เมื่อ user เปลี่ยนแปลง input ผ่านทาง view คราวนี้ model ก็จะอัพเดตเช่นกัน ตามรูป

ในส่วนของ filtertransaction Pipe ซึ่งมี searchText เป็น parameter ต้องเขียนขึ้นมาใหม่ ตามรูป

จากนั้นก็ไป declare ใน explorer.module.ts ดังนี้


การประยุกต์ใช้ Pipe กับ Paginator และ Virtual Ccroll ด้วย Angular Material 7
หากใครยังไม่เคยใช้ Angular Material ลองดู doc แล้วติดตั้งได้เลยครับ https://material.angular.io/guide/getting-started

เนื่องจาก พอเวลาผ่านไป จำนวน transaction ก็จะเพิ่มมากขึ้นจนไม่สามาถแสดงผลได้หมดในหน้าเดียว ดังนั้น การใช้ virtual scroll ซึ่งเป็นฟีเจอร์ที่เพิ่งเพิ่มมาใหม่ ใน Angular Material 7 และ paginator โดยสามารถทำได้ดังนี้

และใช้ built-in html component คือ <cdk-virtual-scroll-viewport> และ <mat-paginator> ดังนี้
<cdk-virtual-scroll-viewport itemSize="10">
<transaction-item
*ngFor="let transaction of transactions | filtertransaction : searchText | slice : lowValue : highValue"
[transaction]="transaction">
</transaction-item>
<br>
</cdk-virtual-scroll-viewport>
<mat-paginator
[length]="transactions.length"
[pageSize]="pageSize"
(page)="pageEvent = getPaginatorData($event)">
</mat-paginator>
Conclusion and my Thought: สรุป
ตลอด การ Dev ในส่วนของ frontend โดยใช้ Angular 7 สิ่งที่ผมชอบมากที่สุด คือความเป็น Modularity ส่งผลให้ code แต่ละส่วน แยกเป็น module ต่างๆ อย่างสวยงาม ซึ่งสะดวกมากๆ เมื่อต้องการกลับไปอ่านโค้ด, แก้ไข, หรือ เสกลเพิ่มฟีเจอร์ใหม่ๆ
สุดท้ายนี้ โปรเจคนี้ยังไม่สมบูรณ์ และ ผมยังมือใหม่มากๆ หากมีข้อผิดพลาดใดๆ ติเตียนได้เต็มที่เลยครับ
Tweet