เจาะลึกข้อดีและคุณสมบัติของ 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

ภาพประกอบ: Angular Component และ React Hooks ค่อนข้างคล้ายกัน
Angular Component และ React Hooks ค่อนข้างคล้ายกัน

ฟังก์ชัน 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 ไม่เพียงแต่ฟังก์ชันแท้จะต้องได้รับการปกป้องจากผลข้างเคียงเท่านั้น เราต้องหลีกเลี่ยงการใช้ในส่วนประกอบและคำสั่งด้วยเช่นกัน

มาดูกันว่าเราจะนำความงามของเทคนิคนี้ไปใช้ในกรอบงานเชิงมุมได้อย่างไร

ภาพประกอบ: ผลข้างเคียงของ NgRx 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)