เจาะลึกข้อดีและคุณสมบัติของ NgRx
เผยแพร่แล้ว: 2022-03-11หากหัวหน้าทีมแนะนำให้นักพัฒนาเขียนโค้ดสำเร็จรูปจำนวนมากแทนที่จะเขียนวิธีการสองสามวิธีในการแก้ปัญหาบางอย่าง พวกเขาต้องการข้อโต้แย้งที่น่าเชื่อถือ วิศวกรซอฟต์แวร์เป็นนักแก้ปัญหา พวกเขาชอบที่จะทำสิ่งต่าง ๆ ให้เป็นอัตโนมัติและหลีกเลี่ยงต้นแบบที่ไม่จำเป็น
แม้ว่า NgRx จะมาพร้อมกับโค้ดต้นแบบ แต่ก็มีเครื่องมืออันทรงพลังสำหรับการพัฒนา บทความนี้แสดงให้เห็นว่าการใช้เวลาเขียนโค้ดเพิ่มขึ้นอีกเล็กน้อยจะทำให้เกิดประโยชน์ที่คุ้มค่ากับความพยายาม
นักพัฒนาส่วนใหญ่เริ่มใช้การจัดการสถานะเมื่อ Dan Abramov เปิดตัวไลบรารี Redux บางคนเริ่มใช้การจัดการของรัฐเพราะเป็นกระแส ไม่ใช่เพราะขาด นักพัฒนาที่ใช้โครงการ "Hello World" มาตรฐานสำหรับการจัดการของรัฐสามารถพบว่าตัวเองเขียนโค้ดเดียวกันซ้ำแล้วซ้ำอีกอย่างรวดเร็ว ความซับซ้อนที่เพิ่มขึ้นโดยเปล่าประโยชน์
ในที่สุด บางคนก็หงุดหงิดและละทิ้งการจัดการของรัฐโดยสิ้นเชิง
ปัญหาเริ่มต้นของฉันกับ NgRx
ฉันคิดว่าข้อกังวลต้นแบบนี้เป็นปัญหาสำคัญกับ NgRx ตอนแรกเราไม่สามารถเห็นภาพรวมเบื้องหลังได้ NgRx เป็นห้องสมุด ไม่ใช่กระบวนทัศน์การเขียนโปรแกรมหรือความคิด อย่างไรก็ตาม เพื่อให้เข้าใจฟังก์ชันและความสามารถในการใช้งานของไลบรารีนี้อย่างเต็มที่ เราต้องขยายความรู้ของเราอีกเล็กน้อยและมุ่งเน้นไปที่การเขียนโปรแกรมเชิงฟังก์ชัน นั่นคือเวลาที่คุณอาจเขียนโค้ดสำเร็จรูปและรู้สึกพอใจกับมัน (ฉันหมายความว่าอย่างนั้น) ฉันเคยเป็นคนขี้ระแวง NgRx; ตอนนี้ฉันเป็นแฟนของ NgRx
ไม่นานมานี้ ฉันเริ่มใช้การจัดการของรัฐ ฉันผ่านประสบการณ์สำเร็จรูปที่อธิบายข้างต้น ดังนั้นฉันจึงตัดสินใจหยุดใช้ห้องสมุด เนื่องจากฉันรัก JavaScript ฉันจึงพยายามบรรลุความรู้พื้นฐานเกี่ยวกับเฟรมเวิร์กยอดนิยมทั้งหมดที่ใช้อยู่ในปัจจุบันเป็นอย่างน้อย นี่คือสิ่งที่ฉันได้เรียนรู้ขณะใช้ React
React มีคุณสมบัติที่เรียกว่า Hooks Hooks เป็นฟังก์ชันง่ายๆ ที่รับอาร์กิวเมนต์และส่งกลับค่า เช่นเดียวกับคอมโพเนนต์ใน Angular เบ็ดสามารถมีสถานะได้ซึ่งเรียกว่าผลข้างเคียง ตัวอย่างเช่น ปุ่มธรรมดาใน Angular สามารถแปลเป็น React ได้ดังนี้:
@Component({ selector: 'simple-button', template: ` <button>Hello {{ name }}</button> `, }) export class SimpleButtonComponent { @Input() name!: string; } export default function SimpleButton(props: { name: string }) { return <button>{props.name} </button>; }อย่างที่คุณเห็น นี่คือการเปลี่ยนแปลงที่ตรงไปตรงมา:
- SimpleButtonComponent => SimpleButton
- @Input() name => props.name
- template => return value
ฟังก์ชัน React SimpleButton ของเรามีคุณลักษณะที่สำคัญอย่างหนึ่งในโลกของการเขียนโปรแกรมเชิงฟังก์ชัน นั่น คือ เป็น ฟังก์ชันบริสุทธิ์ หากคุณกำลังอ่านข้อความนี้ ฉันคิดว่าคุณเคยได้ยินคำนั้นอย่างน้อยหนึ่งครั้ง NgRx.io อ้างถึงฟังก์ชันบริสุทธิ์สองครั้งในแนวคิดหลัก:
- การเปลี่ยนแปลงสถานะได้รับการจัดการโดย ฟังก์ชันบริสุทธิ์ที่ เรียกว่า
reducersซึ่งใช้สถานะปัจจุบันและการดำเนินการล่าสุดเพื่อคำนวณสถานะใหม่ - Selectors เป็น ฟังก์ชันล้วนๆ ที่ ใช้เพื่อเลือก ได้มา และประกอบชิ้นส่วนของรัฐ
ใน React นักพัฒนาควรใช้ Hooks เป็นฟังก์ชัน pures ให้ได้มากที่สุด Angular ยังสนับสนุนให้นักพัฒนาใช้รูปแบบเดียวกันโดยใช้กระบวนทัศน์ Smart-Dumb Component
นั่นคือเมื่อฉันรู้ว่าฉันขาดทักษะการเขียนโปรแกรมเชิงฟังก์ชันที่สำคัญบางอย่าง ใช้เวลาไม่นานในการเข้าใจ NgRx เนื่องจากหลังจากเรียนรู้แนวคิดหลักของการเขียนโปรแกรมเชิงฟังก์ชัน ฉันมี "Aha! ชั่วขณะ”: ฉันได้ปรับปรุงความเข้าใจเกี่ยวกับ NgRx และต้องการใช้ NgRx มากขึ้นเพื่อให้เข้าใจถึงประโยชน์ที่ได้รับมากขึ้น
บทความนี้จะแบ่งปันประสบการณ์การเรียนรู้และความรู้ที่ฉันได้รับเกี่ยวกับ NgRx และการเขียนโปรแกรมเชิงฟังก์ชัน ฉันไม่ได้อธิบาย API สำหรับ NgRx หรือวิธีเรียกใช้การดำเนินการหรือใช้ตัวเลือก แต่ฉันแบ่งปันว่าทำไมฉันถึงรู้สึกขอบคุณที่ NgRx เป็นห้องสมุดที่ยอดเยี่ยม: ไม่ใช่แค่แนวโน้มที่ค่อนข้างใหม่ แต่ยังให้ประโยชน์มากมาย
มาเริ่มกันด้วย การเขียนโปรแกรมเชิงฟังก์ชัน
ฟังก์ชั่นการเขียนโปรแกรม
การเขียนโปรแกรมเชิงฟังก์ชันเป็นกระบวนทัศน์ที่แตกต่างจากกระบวนทัศน์อื่นๆ อย่างมาก นี่เป็นหัวข้อที่ซับซ้อนมากพร้อมคำจำกัดความและแนวทางปฏิบัติมากมาย อย่างไรก็ตาม การเขียนโปรแกรมเชิงฟังก์ชันประกอบด้วยแนวคิดหลักบางประการ และการรู้ว่าสิ่งเหล่านี้เป็นข้อกำหนดเบื้องต้นสำหรับการเรียนรู้ NgRx (และ JavaScript โดยทั่วไป)
แนวคิดหลักเหล่านี้คือ:
- ฟังก์ชั่นเพียว
- สถานะไม่เปลี่ยนรูป
- ผลข้างเคียง
ฉันพูดซ้ำ: มันเป็นแค่กระบวนทัศน์ ไม่มีอะไรมากไปกว่านี้ ไม่มีไลบรารี functional.js ที่เราดาวน์โหลดและใช้เพื่อเขียนซอฟต์แวร์ที่ใช้งานได้ เป็นเพียงวิธีคิดในการเขียนใบสมัคร เริ่มจากแนวคิดหลักที่สำคัญที่สุด: ฟังก์ชั่นบริสุทธิ์
ฟังก์ชั่นเพียว
ฟังก์ชันถือเป็นฟังก์ชันบริสุทธิ์ หากเป็นไปตามกฎง่ายๆ สองข้อ:
- การส่งอาร์กิวเมนต์เดียวกันจะคืนค่าเดิมเสมอ
- ไม่มีผลข้างเคียงที่สังเกตได้ซึ่งเกี่ยวข้องกับการเรียกใช้ฟังก์ชัน (การเปลี่ยนแปลงสถานะภายนอก การเรียกการดำเนินการ I/O เป็นต้น)
ดังนั้นฟังก์ชันบริสุทธิ์จึงเป็นเพียงฟังก์ชันโปร่งใสที่ยอมรับอาร์กิวเมนต์บางตัว (หรือไม่มีอาร์กิวเมนต์เลย) และคืนค่าที่คาดหวัง คุณมั่นใจได้ว่าการเรียกใช้ฟังก์ชันนี้จะไม่ส่งผลให้เกิดผลข้างเคียง เช่น การเชื่อมต่อเครือข่ายหรือการเปลี่ยนแปลงสถานะผู้ใช้ทั่วโลก
มาดูตัวอย่างง่ายๆ สามตัวอย่าง:
//Pure function function add(a,b){ return a + b; } //Impure function breaking rule 1 function random(){ return Math.random(); } //Impure function breaking rule 2 function sayHello(name){ console.log("Hello " + name); }- ฟังก์ชันแรกบริสุทธิ์เพราะจะส่งกลับคำตอบเดิมเสมอเมื่อส่งผ่านอาร์กิวเมนต์เดียวกัน
- ฟังก์ชันที่สองไม่บริสุทธิ์เพราะไม่ได้กำหนดไว้และส่งคืนคำตอบที่แตกต่างกันทุกครั้งที่มีการเรียก
- ฟังก์ชั่นที่สามไม่บริสุทธิ์เพราะใช้ผลข้างเคียง (เรียก
console.log)
ง่ายต่อการแยกแยะว่าฟังก์ชันนั้นบริสุทธิ์หรือไม่ เหตุใดฟังก์ชันบริสุทธิ์จึงดีกว่าฟังก์ชันที่ไม่บริสุทธิ์ เพราะมันง่ายกว่าที่จะคิดเกี่ยวกับ ลองนึกภาพว่าคุณกำลังอ่านซอร์สโค้ดและดูการเรียกใช้ฟังก์ชันที่คุณรู้ว่าบริสุทธิ์ หากชื่อฟังก์ชันถูกต้อง คุณไม่จำเป็นต้องสำรวจมัน คุณรู้ว่ามันไม่ได้เปลี่ยนแปลงอะไรเลย มันให้ผลตอบแทนตามที่คุณคาดหวัง เป็นสิ่งสำคัญสำหรับการดีบักเมื่อคุณมีแอปพลิเคชันองค์กรขนาดใหญ่ที่มีตรรกะทางธุรกิจมากมาย เนื่องจากสามารถประหยัดเวลาได้อย่างมาก
นอกจากนี้ยังง่ายต่อการทดสอบ คุณไม่จำเป็นต้องฉีดอะไรเลยหรือล้อเลียนฟังก์ชันบางอย่างในนั้น คุณเพียงแค่ส่งผ่านอาร์กิวเมนต์และทดสอบว่าผลลัพธ์ตรงกันหรือไม่ มีความเชื่อมโยงที่ชัดเจนระหว่างการทดสอบกับตรรกะ: หากส่วนประกอบนั้นง่ายต่อการทดสอบ ก็จะเข้าใจได้ง่ายถึงวิธีการและเหตุผลในการทำงาน
ฟังก์ชันล้วนๆ มาพร้อมกับฟังก์ชันที่ใช้งานสะดวกและเป็นมิตรต่อประสิทธิภาพ ซึ่งเรียกว่าการช่วยจำ หากเรารู้ว่าการเรียกอาร์กิวเมนต์เดียวกันจะคืนค่าเดิม เราก็สามารถแคชผลลัพธ์ได้และไม่ต้องเสียเวลาเรียกมันอีก NgRx อยู่เหนือการท่องจำอย่างแน่นอน นั่นเป็นเหตุผลหลักประการหนึ่งว่าทำไมมันถึงเร็ว
คุณอาจถามตัวเองว่า “แล้วผลข้างเคียงล่ะ? พวกเขาไปไหน?" ในการพูดคุย GOTO ของเขา Russ Olsen พูดติดตลกว่าลูกค้าของเราไม่จ่ายเงินให้เราสำหรับการทำงานที่บริสุทธิ์ พวกเขาจ่ายให้เราสำหรับผลข้างเคียง นั่นเป็นความจริง ไม่มีใครสนใจฟังก์ชันบริสุทธิ์ของเครื่องคิดเลขหากไม่ได้พิมพ์ไว้ที่ใด ผลข้างเคียงมีอยู่ในจักรวาลของการเขียนโปรแกรมเชิงฟังก์ชัน เราจะเห็นว่าในไม่ช้า
สำหรับตอนนี้ ไปที่ขั้นตอนต่อไปในการบำรุงรักษาสถาปัตยกรรมแอปพลิเคชันที่ซับซ้อน แนวคิดหลักถัดไป: immutable state
สถานะไม่เปลี่ยนรูป
มีคำจำกัดความง่าย ๆ สำหรับสถานะที่ไม่เปลี่ยนรูป:
- คุณสามารถสร้างหรือลบสถานะได้เท่านั้น คุณไม่สามารถอัปเดตได้
พูดง่ายๆ ก็คือ ในการอัปเดตอายุของออบเจ็กต์ User … :
let user = { username:"admin", age:28 }… คุณควรเขียนแบบนี้:
// Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }ทุกการเปลี่ยนแปลงเป็นอ็อบเจกต์ใหม่ที่มีการคัดลอกคุณสมบัติจากอันเก่า เช่นนี้เราจึงอยู่ในสภาพที่ไม่เปลี่ยนรูปแล้ว
String, Boolean และ Number เป็นสถานะที่ไม่เปลี่ยนรูป: คุณไม่สามารถผนวกหรือแก้ไขค่าที่มีอยู่ได้ ในทางตรงกันข้าม วันที่เป็นวัตถุที่เปลี่ยนแปลงได้: คุณจัดการวัตถุวันที่เดียวกันเสมอ
ความไม่เปลี่ยนรูปนำไปใช้กับแอปพลิเคชัน: หากคุณส่งผ่านวัตถุผู้ใช้ภายในฟังก์ชันที่เปลี่ยนอายุ วัตถุนั้นไม่ควรเปลี่ยนวัตถุผู้ใช้ ควรสร้างวัตถุผู้ใช้ใหม่ด้วยอายุที่อัปเดตแล้วส่งคืน:
function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);ทำไมเราควรอุทิศเวลาและความสนใจให้กับสิ่งนี้? มีประโยชน์สองสามอย่างที่ควรค่าแก่การเน้นย้ำ
ประโยชน์อย่างหนึ่งสำหรับภาษาโปรแกรมแบ็คเอนด์เกี่ยวข้องกับการประมวลผลแบบขนาน หากการเปลี่ยนแปลงสถานะไม่ได้ขึ้นอยู่กับข้อมูลอ้างอิง และทุกการอัปเดตเป็นอ็อบเจ็กต์ใหม่ คุณสามารถแบ่งกระบวนการออกเป็นชิ้นๆ และทำงานเดียวกันด้วยเธรดจำนวนนับไม่ถ้วนโดยไม่ต้องใช้หน่วยความจำร่วมกัน คุณยังสามารถทำงานพร้อมกันข้ามเซิร์ฟเวอร์ได้
สำหรับเฟรมเวิร์ก เช่น Angular และ React การประมวลผลแบบขนานเป็นหนึ่งในวิธีที่เป็นประโยชน์มากกว่าในการปรับปรุงประสิทธิภาพของแอปพลิเคชัน ตัวอย่างเช่น Angular ต้องตรวจสอบคุณสมบัติของวัตถุทุกรายการที่คุณส่งผ่านการผูกอินพุตเพื่อดูว่าส่วนประกอบต้องแสดงผลใหม่หรือไม่ แต่ถ้าเราตั้งค่า ChangeDetectionStrategy.OnPush แทนที่จะเป็นค่าดีฟอลต์ มันจะตรวจสอบโดยการอ้างอิง ไม่ใช่โดยแต่ละคุณสมบัติ ในแอปพลิเคชันขนาดใหญ่ สิ่งนี้ช่วยประหยัดเวลาได้อย่างแน่นอน หากเราอัปเดตสถานะโดยไม่เปลี่ยนรูป เราก็จะได้รับการเพิ่มประสิทธิภาพนี้ฟรี
ประโยชน์อื่น ๆ สำหรับสถานะที่ไม่เปลี่ยนรูปที่ภาษาโปรแกรมและเฟรมเวิร์กทั้งหมดใช้ร่วมกันนั้นคล้ายกับประโยชน์ของฟังก์ชันบริสุทธิ์: ง่ายต่อการคิดและทดสอบ เมื่อการเปลี่ยนแปลงเป็นสถานะใหม่ที่เกิดจากสภาพเก่า คุณจะรู้แน่ชัดว่าคุณกำลังทำอะไรอยู่ และคุณสามารถติดตามได้อย่างชัดเจนว่าสถานะเปลี่ยนไปอย่างไรและที่ไหน คุณจะไม่สูญเสียประวัติการอัปเดต และคุณสามารถเลิกทำ/ทำซ้ำการเปลี่ยนแปลงสำหรับสถานะได้ (React DevTools เป็นตัวอย่างหนึ่ง)
อย่างไรก็ตาม หากมีการอัพเดตสถานะเดียว คุณจะไม่ทราบประวัติของการเปลี่ยนแปลงเหล่านั้น ลองนึกถึงสถานะที่ไม่เปลี่ยนรูป เช่น ประวัติการทำธุรกรรมของบัญชีธนาคาร เป็นสิ่งที่ต้องมี
ตอนนี้เราได้ตรวจสอบความไม่เปลี่ยนรูปและความบริสุทธิ์แล้ว มาจัดการกับแนวคิดหลักที่เหลือ: ผลข้างเคียง กัน
ผลข้างเคียง
เราสามารถสรุปคำจำกัดความของผลข้างเคียงได้:
- ในวิทยาการคอมพิวเตอร์ มีการกล่าวถึงการดำเนินการ ฟังก์ชัน หรือนิพจน์ว่ามีผลข้างเคียงหากปรับเปลี่ยนค่าตัวแปรสถานะบางอย่างภายนอกสภาพแวดล้อมภายในเครื่อง กล่าวคือมีผลที่สังเกตได้นอกเหนือจากการคืนค่า (ผลกระทบหลัก) ไปยังผู้เรียกใช้การดำเนินการ
พูดง่ายๆ ก็คือ ทุกสิ่งที่เปลี่ยนสถานะนอกขอบเขตของฟังก์ชัน—การดำเนินการ I/O ทั้งหมดและงานบางอย่างที่ไม่ได้เชื่อมต่อโดยตรงกับฟังก์ชัน—ถือได้ว่าเป็นผลข้างเคียง อย่างไรก็ตาม เราต้องหลีกเลี่ยงการใช้ผลข้างเคียงภายในฟังก์ชันล้วนๆ เนื่องจากผลข้างเคียงขัดแย้งกับปรัชญาการเขียนโปรแกรมเชิงฟังก์ชัน หากคุณใช้การดำเนินการ I/O ภายในฟังก์ชันบริสุทธิ์ การดำเนินการดังกล่าวจะยุติการเป็นฟังก์ชันบริสุทธิ์
อย่างไรก็ตาม เราจำเป็นต้องมีผลข้างเคียงที่ใดที่หนึ่ง เนื่องจากแอปพลิเคชันที่ไม่มีผลข้างเคียงนั้นก็ไร้ประโยชน์ ใน Angular ไม่เพียงแต่ฟังก์ชันแท้จะต้องได้รับการปกป้องจากผลข้างเคียงเท่านั้น เราต้องหลีกเลี่ยงการใช้ในส่วนประกอบและคำสั่งด้วยเช่นกัน
มาดูกันว่าเราจะนำความงามของเทคนิคนี้ไปใช้ในกรอบงานเชิงมุมได้อย่างไร
ฟังก์ชั่นการเขียนโปรแกรมเชิงมุม
สิ่งแรกที่ต้องทำความเข้าใจเกี่ยวกับ Angular คือความจำเป็นในการแยกส่วนประกอบออกเป็นส่วนประกอบที่มีขนาดเล็กลงให้บ่อยที่สุดเท่าที่จะทำได้ เพื่อให้สามารถบำรุงรักษาและทดสอบได้ง่ายขึ้น นี่เป็นสิ่งจำเป็น เนื่องจากเราจำเป็นต้องแบ่งตรรกะทางธุรกิจของเรา นอกจากนี้ นักพัฒนาเชิงมุมควรปล่อยให้ส่วนประกอบต่างๆ ใช้เพื่อวัตถุประสงค์ในการแสดงผลเท่านั้น และย้ายตรรกะทางธุรกิจทั้งหมดภายในบริการ
ในการขยายแนวคิดเหล่านี้ ผู้ใช้ Angular ได้เพิ่มรูปแบบ "Dumb-Smart Component" ลงในคำศัพท์ของพวกเขา รูปแบบนี้ต้องการให้ไม่มีการเรียกใช้บริการภายในส่วนประกอบขนาดเล็ก เนื่องจากตรรกะทางธุรกิจอยู่ในบริการ เรายังคงต้องเรียกวิธีการบริการเหล่านี้ รอการตอบกลับ จากนั้นจึงทำการเปลี่ยนแปลงสถานะเท่านั้น ดังนั้น ส่วนประกอบจึงมีตรรกะเชิงพฤติกรรมอยู่ภายใน
เพื่อหลีกเลี่ยงปัญหาดังกล่าว เราสามารถสร้างส่วนประกอบอัจฉริยะ (ส่วนประกอบหลัก) หนึ่งรายการซึ่งมีตรรกะทางธุรกิจและพฤติกรรม ส่งผ่านสถานะผ่านคุณสมบัติอินพุต และเรียกใช้การดำเนินการเพื่อฟังพารามิเตอร์เอาต์พุต ด้วยวิธีนี้ ส่วนประกอบขนาดเล็กมีไว้เพื่อการเรนเดอร์เท่านั้นอย่างแท้จริง แน่นอนว่า Root Component ของเราต้องมีการเรียกใช้บริการอยู่ภายใน และเราไม่สามารถลบออกได้ แต่ยูทิลิตี้ของมันจะถูกจำกัดให้อยู่ที่ตรรกะทางธุรกิจเท่านั้น ไม่มีการเรนเดอร์
มาดูตัวอย่าง Counter Component ตัวนับคือส่วนประกอบที่มีปุ่มสองปุ่มที่เพิ่มหรือลดค่า และหนึ่ง displayField ที่แสดงค่า currentValue ดังนั้นเราจึงลงเอยด้วยองค์ประกอบสี่ประการ:
- เคาน์เตอร์คอนเทนเนอร์
- เพิ่มปุ่ม
- ปุ่มลดลง
- มูลค่าปัจจุบัน
ตรรกะทั้งหมดอยู่ภายใน CounterContainer ดังนั้นทั้งสามเป็นเพียงตัวแสดงภาพ นี่คือรหัสสำหรับทั้งสามคน:
@Component({ selector: 'decrease-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Decrease </button>`, }) export class DecreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); } @Component({ selector: 'current-value', template: `<button> {{ currentValue }} </button>`, }) export class CurrentValueComponent { @Input() currentValue!: string; } @Component({ selector: 'increase-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Increase </button>`, }) export class IncreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); }ดูว่าพวกเขาเรียบง่ายและบริสุทธิ์แค่ไหน พวกมันไม่มีสถานะหรือผลข้างเคียง พวกเขาแค่ขึ้นอยู่กับคุณสมบัติของอินพุตและเหตุการณ์ที่เปล่งออกมา ลองนึกภาพว่าการทดสอบนั้นง่ายเพียงใด เราสามารถเรียกมันว่าส่วนประกอบบริสุทธิ์ได้ นั่นคือสิ่งที่มันเป็นจริงๆ ขึ้นอยู่กับพารามิเตอร์อินพุตเท่านั้น ไม่มีผลข้างเคียง และคืนค่าเดิมเสมอ (สตริงเทมเพลต) โดยส่งพารามิเตอร์เดียวกัน

ดังนั้นฟังก์ชันบริสุทธิ์ในการเขียนโปรแกรมเชิงฟังก์ชันจึงถูกถ่ายโอนไปยังส่วนประกอบบริสุทธิ์ใน Angular แต่ตรรกะทั้งหมดไปอยู่ที่ไหน? ตรรกะยังคงอยู่แต่ในที่ที่ต่างออกไปเล็กน้อยคือ CounterComponent
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { @Input() disabled!: boolean; currentValue = 0; get decreaseIsDisabled() { return this.currentValue === 0; } get increaseIsDisabled() { return this.currentValue === 100; } constructor() {} ngOnInit(): void {} decrease() { this.currentValue -= 1; } increase() { this.currentValue += 1; } } อย่างที่คุณเห็น ตรรกะของพฤติกรรมอยู่ใน CounterContainer แต่ส่วนการเรนเดอร์ขาดหายไป (มันประกาศส่วนประกอบภายในเทมเพลต) เนื่องจากส่วนการเรนเดอร์มีไว้สำหรับส่วนประกอบล้วนๆ
เราสามารถใส่บริการได้มากเท่าที่เราต้องการเพราะเราจัดการการจัดการข้อมูลและการเปลี่ยนแปลงสถานะทั้งหมดที่นี่ สิ่งหนึ่งที่ควรกล่าวถึงคือ ถ้าเรามีองค์ประกอบที่ซ้อนกันอย่างลึก เราต้องไม่สร้างองค์ประกอบระดับรูทเพียงองค์ประกอบเดียว เราสามารถแบ่งออกเป็นส่วนประกอบอัจฉริยะที่มีขนาดเล็กลงและใช้รูปแบบเดียวกันได้ ในท้ายที่สุด ขึ้นอยู่กับความซับซ้อนและระดับที่ซ้อนกันสำหรับแต่ละส่วนประกอบ
เราสามารถข้ามจากรูปแบบนั้นไปยังไลบรารี NgRx ได้อย่างง่ายดาย ซึ่งอยู่เหนือมันเพียงชั้นเดียว
ห้องสมุด NgRx
เราสามารถแบ่งเว็บแอปพลิเคชันออกเป็นสามส่วนหลัก:
- ตรรกะทางธุรกิจ
- สถานะการสมัคร
- ลอจิกการแสดงผล
Business Logic คือพฤติกรรมทั้งหมดที่เกิดขึ้นกับแอปพลิเคชัน เช่น ระบบเครือข่าย อินพุต เอาต์พุต API เป็นต้น
Application State คือสถานะของแอปพลิเคชัน อาจเป็นแบบโกลบอล ในฐานะผู้ใช้ที่ได้รับอนุญาตในปัจจุบัน และในเครื่องได้ เช่นเดียวกับค่า Counter Component ปัจจุบัน
Rendering Logic ครอบคลุมการเรนเดอร์ เช่น การแสดงข้อมูลโดยใช้ DOM การสร้างหรือลบองค์ประกอบ และอื่นๆ
ด้วยการใช้รูปแบบ Dumb-Smart เราแยก Rendering Logic ออกจาก Business Logic และ Application State แต่เราสามารถแบ่งพวกมันได้เช่นกันเพราะทั้งสองมีแนวคิดต่างกัน สถานะแอปพลิเคชันเป็นเหมือนสแนปชอตของแอปของคุณในเวลาปัจจุบัน Business Logic เปรียบเสมือนฟังก์ชันการทำงานแบบคงที่ที่ปรากฏในแอปของคุณเสมอ เหตุผลที่สำคัญที่สุดในการแบ่งพวกเขาก็คือ Business Logic ส่วนใหญ่เป็นผลข้างเคียงที่เราต้องการหลีกเลี่ยงในโค้ดแอปพลิเคชันให้มากที่สุด นี่คือช่วงเวลาที่ไลบรารี NgRx ซึ่งมีกระบวนทัศน์การใช้งานส่องสว่าง
ด้วย NgRx คุณจะแยกส่วนเหล่านี้ออกทั้งหมด มีสามส่วนหลัก:
- ลด
- การกระทำ
- ตัวเลือก
เมื่อรวมเข้ากับการเขียนโปรแกรมเชิงฟังก์ชัน ทั้งสามรวมกันทำให้เรามีเครื่องมืออันทรงพลังในการจัดการแอปพลิเคชันทุกขนาด ลองตรวจสอบแต่ละรายการ
ลด
รีดิวเซอร์เป็นฟังก์ชันล้วนๆ ซึ่งมีลายเซ็นที่เรียบง่าย ใช้สถานะเก่าเป็นพารามิเตอร์และส่งกลับสถานะใหม่ไม่ว่าจะมาจากสถานะเก่าหรือสถานะใหม่ สถานะนั้นเป็นออบเจ็กต์เดียวซึ่งอยู่กับวงจรชีวิตของแอปพลิเคชันของคุณ มันเหมือนกับแท็ก HTML ซึ่งเป็นวัตถุรูทเดียว
คุณไม่สามารถแก้ไขวัตถุสถานะได้โดยตรง คุณต้องแก้ไขด้วยตัวลด ซึ่งมีประโยชน์หลายประการ:
- ตรรกะของสถานะการเปลี่ยนแปลงอยู่ในที่เดียว และคุณรู้ว่าสถานะเปลี่ยนแปลงที่ไหนและอย่างไร
- ฟังก์ชันรีดิวเซอร์เป็นฟังก์ชันล้วนๆ ซึ่งง่ายต่อการทดสอบและจัดการ
- เนื่องจากตัวลดขนาดเป็นฟังก์ชันล้วนๆ จึงสามารถบันทึกได้ ทำให้สามารถแคชและหลีกเลี่ยงการคำนวณเพิ่มเติมได้
- การเปลี่ยนแปลงสถานะจะไม่เปลี่ยนรูป คุณไม่เคยอัปเดตอินสแตนซ์เดียวกัน คุณจะส่งคืนใหม่เสมอ ซึ่งจะทำให้ประสบการณ์การดีบัก "การเดินทางข้ามเวลา"
นี่เป็นตัวอย่างเล็กน้อยของตัวลด:
function usernameReducer(oldState, username) { return {...oldState, username} }แม้ว่าจะเป็นตัวลดหุ่นจำลองที่เรียบง่าย แต่ก็เป็นโครงกระดูกสำหรับตัวลดขนาดที่ยาวและซับซ้อนทั้งหมด พวกเขาทั้งหมดมีประโยชน์เหมือนกัน เราอาจมีตัวลดขนาดหลายร้อยตัวในแอปพลิเคชันของเราและเราสามารถสร้างได้มากเท่าที่เราต้องการ
สำหรับ Counter Component สถานะและตัวลดอาจมีลักษณะดังนี้:
interface State{ decreaseDisabled:boolean; increaseDisabled:boolean; currentValue:number; } const MIN_VALUE=0; const MAX_VALUE =100; function decreaseReducer(oldState) { const newValue = oldState.currentValue -1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE } function increaseReducer(oldState) { const newValue = oldState.currentValue + 1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE }เราลบสถานะออกจากองค์ประกอบ ตอนนี้เราต้องการวิธีอัปเดตสถานะของเราและเรียกตัวลดที่เหมาะสม นั่นคือเมื่อการกระทำเข้ามาเล่น
การกระทำ
การดำเนินการเป็นวิธีแจ้ง NgRx ให้เรียกตัวลดและอัปเดตสถานะ หากปราศจากสิ่งนั้น การใช้ NgRx ก็ไร้ความหมาย การกระทำเป็นวัตถุธรรมดาที่เราแนบกับตัวลดปัจจุบัน หลังจากเรียกใช้แล้ว ตัวลดที่เหมาะสมจะถูกเรียก ดังนั้นในตัวอย่างของเรา เราอาจมีการดำเนินการต่อไปนี้:
enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);การกระทำของเราแนบมากับตัวลด ตอนนี้ เราสามารถแก้ไขส่วนประกอบคอนเทนเนอร์ของเราเพิ่มเติม และเรียกใช้การดำเนินการที่เหมาะสมเมื่อจำเป็น:
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }หมายเหตุ: เราลบสถานะออกและจะเพิ่มกลับในไม่ช้า
ขณะนี้ CounterContainer ของเราไม่มีตรรกะในการเปลี่ยนแปลงสถานะใดๆ มันแค่รู้ว่าต้องส่งอะไร ตอนนี้เราต้องการวิธีแสดงข้อมูลนี้ในมุมมอง นั่นคือประโยชน์ของตัวเลือก
ตัวเลือก
ตัวเลือกยังเป็นฟังก์ชันบริสุทธิ์ที่ง่ายมาก แต่ไม่เหมือนกับตัวลดค่าตรงที่ไม่มีการอัพเดตสถานะ ตามชื่อที่สื่อถึง ตัวเลือกเพียงแค่เลือกเท่านั้น ในตัวอย่างของเรา เราสามารถมีตัวเลือกง่ายๆ สามตัวเลือก:
function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; } ด้วยการใช้ตัวเลือกเหล่านี้ เราสามารถเลือกแต่ละส่วนของสถานะภายในส่วนประกอบ CounterContainer อันชาญฉลาดของเราได้
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="ecreaseIsDisabled$ | async" (decrease)="decrease()" > </decrease-button> <current-value [currentValue]="currentValue$ | async"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled$ | async" > </increase-button> `, }) export class CounterContainerComponent implements OnInit { decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled); increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled); currentValue$ = this.store.select(selectCurrentValue); constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }การเลือกเหล่านี้เป็นแบบอะซิงโครนัสโดยค่าเริ่มต้น (เช่นเดียวกับ Observables โดยทั่วไป) สิ่งนี้ไม่มีความสำคัญ อย่างน้อยก็จากมุมมองของรูปแบบ เช่นเดียวกับซิงโครนัสเนื่องจากเราจะเลือกบางอย่างจากสถานะของเรา
ลองย้อนกลับไปดูภาพใหญ่เพื่อดูว่าเราได้บรรลุอะไรมาบ้าง เรามี Counter Application ซึ่งมีสามส่วนหลักที่เกือบจะแยกออกจากกัน ไม่มีใครรู้ว่าสถานะของแอปพลิเคชันจัดการตัวเองอย่างไรหรือเลเยอร์การเรนเดอร์แสดงสถานะอย่างไร
ชิ้นส่วนที่แยกออกมาใช้สะพาน (Actions, Selectors) เพื่อเชื่อมต่อซึ่งกันและกัน พวกเขาแยกส่วนกันมากจนเราสามารถนำรหัส State Application ทั้งหมดและย้ายไปยังโครงการอื่น เช่น สำหรับรุ่นมือถือ เป็นต้น สิ่งเดียวที่เราต้องดำเนินการคือการแสดงผล แต่การทดสอบล่ะ?
ในความเห็นที่ต่ำต้อยของฉัน การทดสอบเป็นส่วนที่ดีที่สุดของ NgRx การทดสอบโปรเจ็กต์ตัวอย่างนี้คล้ายกับการเล่นโอเอกซ์ มีเพียงฟังก์ชันบริสุทธิ์และส่วนประกอบบริสุทธิ์เท่านั้น ดังนั้นการทดสอบเหล่านี้จึงเป็นเรื่องง่าย ทีนี้ลองนึกดูว่าโครงการนี้จะใหญ่ขึ้นด้วยส่วนประกอบหลายร้อยส่วนหรือไม่ ถ้าเราทำตามรูปแบบเดียวกัน เราก็จะเพิ่มชิ้นส่วนต่างๆ เข้าด้วยกันมากขึ้นเรื่อยๆ มันจะไม่กลายเป็นซอร์สโค้ดที่ยุ่งเหยิงและอ่านไม่ได้
เราเกือบเสร็จแล้ว มีเพียงสิ่งสำคัญเท่านั้นที่ต้องกล่าวถึง: ผลข้างเคียง ฉันพูดถึงผลข้างเคียงหลายครั้งแล้ว แต่ฉันหยุดอธิบายไม่ได้ว่าจะเก็บไว้ที่ไหน
นั่นเป็นเพราะว่าผลข้างเคียงคือไอซิ่งบนเค้ก และด้วยการสร้างรูปแบบนี้ มันง่ายมากที่จะลบมันออกจากโค้ดของแอปพลิเคชัน
ผลข้างเคียง
สมมติว่าแอปพลิเคชันตัวนับของเรามีตัวจับเวลาอยู่ในนั้น และทุก ๆ สามวินาทีมันจะเพิ่มค่าหนึ่งโดยอัตโนมัติ นี่เป็นผลข้างเคียงง่ายๆ ที่ต้องอาศัยอยู่ที่ไหนสักแห่ง มันเป็นผลข้างเคียงตามคำจำกัดความตามคำขอของ Ajax
หากเรานึกถึงผลข้างเคียง ส่วนใหญ่จะมีเหตุผลหลักสองประการ:
- การทำสิ่งใดนอกสภาวะแวดล้อมของรัฐ
- กำลังอัปเดตสถานะแอปพลิเคชัน
ตัวอย่างเช่น การจัดเก็บบางสถานะภายใน LocalStorage เป็นตัวเลือกแรก ในขณะที่การอัปเดตสถานะจากการตอบกลับ Ajax เป็นตัวเลือกที่สอง แต่ทั้งสองมีลายเซ็นเดียวกัน: ผลข้างเคียงแต่ละรายการต้องมีจุดเริ่มต้น จำเป็นต้องเรียกอย่างน้อยหนึ่งครั้งเพื่อเริ่มการดำเนินการ
ตามที่เราสรุปไว้ก่อนหน้านี้ NgRx มีเครื่องมือที่ดีในการสั่งงานใครซักคน นั่นคือการกระทำ เราสามารถเรียกผลข้างเคียงใดๆ ก็ได้โดยส่งการกระทำ รหัสเทียมอาจมีลักษณะดังนี้:
function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);มันค่อนข้างไร้สาระ ดังที่ฉันได้กล่าวไว้ก่อนหน้านี้ ผลข้างเคียงอาจอัปเดตบางอย่างหรือไม่ หากผลข้างเคียงไม่อัปเดต แสดงว่าไม่ต้องดำเนินการใดๆ เราก็แค่ทิ้งมันไว้ แต่ถ้าเราต้องการอัพเดทสถานะเราจะทำอย่างไร? เช่นเดียวกับที่คอมโพเนนต์พยายามอัปเดตสถานะ: เรียกการดำเนินการอื่น ดังนั้นเราจึงเรียกการดำเนินการภายในผลข้างเคียง ซึ่งจะอัปเดตสถานะ:
function startTimer(store) { setInterval(()=> { // We are dispatching another action dispatch(CounterActions.IncreaseValue) }, 3000) } on(CounterActions.StartTime, startTimer); ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);ตอนนี้เรามีแอปพลิเคชั่นที่ใช้งานได้เต็มรูปแบบแล้ว
สรุปประสบการณ์ NgRx ของเรา
มีบางหัวข้อสำคัญที่ฉันอยากจะพูดถึงก่อนที่เราจะเสร็จสิ้นการเดินทาง NgRx ของเรา:
- รหัสที่แสดงเป็นรหัสหลอกง่าย ๆ ที่ฉันคิดค้นสำหรับบทความ เหมาะสำหรับวัตถุประสงค์ในการสาธิตเท่านั้น NgRx เป็นที่ที่แหล่งข้อมูลจริงอาศัยอยู่
- ไม่มีแนวทางอย่างเป็นทางการใดที่พิสูจน์ทฤษฎีของฉันเกี่ยวกับการเชื่อมต่อการเขียนโปรแกรมเชิงฟังก์ชันกับไลบรารี NgRx เป็นเพียงความคิดเห็นของฉันที่เกิดขึ้นหลังจากอ่านบทความหลายสิบบทความและตัวอย่างซอร์สโค้ดที่สร้างโดยคนที่มีทักษะสูง
- หลังจากใช้ NgRx คุณจะรู้ว่ามันซับซ้อนกว่าตัวอย่างง่ายๆ นี้มาก เป้าหมายของฉันไม่ใช่การทำให้ดูเรียบง่ายกว่าที่เป็นจริง แต่เพื่อแสดงให้คุณเห็นว่าถึงแม้จะซับซ้อนเล็กน้อยและอาจส่งผลให้มีเส้นทางที่ยาวกว่าไปยังจุดหมายปลายทาง แต่ก็คุ้มค่ากับความพยายามที่เพิ่มเข้ามา
- การใช้งานที่แย่ที่สุดสำหรับ NgRx คือการใช้งานทุกที่ โดยไม่คำนึงถึงขนาดหรือความซับซ้อนของแอปพลิเคชัน มีบางกรณีที่คุณ ไม่ควร ใช้ NgRx; ตัวอย่างเช่นในรูปแบบ แทบจะเป็นไปไม่ได้เลยที่จะนำแบบฟอร์มไปใช้ใน NgRx แบบฟอร์มติดอยู่กับ DOM เอง พวกเขาไม่สามารถแยกกันอยู่ได้ หากคุณพยายามแยกพวกเขาออก คุณจะพบว่าตัวเองเกลียดไม่เพียงแค่ NgRx แต่ยังเกลียดเทคโนโลยีเว็บโดยทั่วไป
- บางครั้งการใช้รหัสต้นแบบเดียวกัน แม้แต่ตัวอย่างเล็กๆ น้อยๆ ก็อาจกลายเป็นฝันร้าย แม้ว่าจะเป็นประโยชน์ต่อเราในอนาคตก็ตาม หากเป็นกรณีนี้ เพียงผสานรวมกับไลบรารีที่น่าทึ่งอื่น ซึ่งเป็นส่วนหนึ่งของระบบนิเวศ NgRx (ComponentStore)
