Angular vs. React: ไหนดีกว่าสำหรับการพัฒนาเว็บ?

เผยแพร่แล้ว: 2022-03-11

มีบทความมากมายที่ถกเถียงกันว่า React หรือ Angular เป็นตัวเลือกที่ดีกว่าสำหรับการพัฒนาเว็บ เราต้องการอีกอันหนึ่งหรือไม่?

เหตุผลที่ฉันเขียนบทความนี้ก็เพราะว่าไม่มีบทความใดที่ตีพิมพ์แล้ว แม้ว่าจะมีข้อมูลเชิงลึกที่ดีก็ตาม แต่ให้เจาะลึกมากพอสำหรับนักพัฒนาส่วนหน้าที่ใช้ได้จริงในการตัดสินใจว่าบทความใดเหมาะสมกับความต้องการของพวกเขา

ในบทความนี้ คุณจะได้เรียนรู้ว่าทั้ง Angular และ React ต่างมุ่งแก้ปัญหา front-end ที่คล้ายคลึงกันอย่างไร แม้ว่าจะมีปรัชญาที่แตกต่างกันมาก และการเลือกอย่างใดอย่างหนึ่งเป็นเพียงเรื่องของความชอบส่วนบุคคล เพื่อเปรียบเทียบ เราจะสร้างแอปพลิเคชันเดียวกันสองครั้ง หนึ่งครั้งด้วย Angular และอีกครั้งด้วย React

ประกาศก่อนเวลาอันควรของ Angular

เมื่อ 2 ปีที่แล้ว ฉันเขียนบทความเกี่ยวกับ React Ecosystem ท่ามกลางประเด็นอื่นๆ บทความโต้แย้งว่า Angular กลายเป็นเหยื่อของ “ความตายโดยการประกาศล่วงหน้า” เมื่อก่อน ทางเลือกระหว่าง Angular และเกือบทุกอย่างเป็นเรื่องง่ายสำหรับทุกคนที่ไม่ต้องการให้โครงการของตนทำงานบนเฟรมเวิร์กที่ล้าสมัย Angular 1 นั้นล้าสมัยแล้ว และ Angular 2 ไม่พร้อมใช้งานในเวอร์ชันอัลฟ่าด้วยซ้ำ

เมื่อมองย้อนกลับไป ความกลัวนั้นมีเหตุผลไม่มากก็น้อย Angular 2 เปลี่ยนไปอย่างมากและต้องผ่านการรีไรท์ครั้งสำคัญก่อนการเปิดตัวครั้งสุดท้าย

สองปีต่อมา เรามี Angular 4 ที่สัญญาว่าจะมีเสถียรภาพสัมพัทธ์นับจากนี้เป็นต้นไป

ตอนนี้อะไร?

Angular vs. React: การเปรียบเทียบแอปเปิ้ลกับส้ม

บางคนบอกว่าการเปรียบเทียบ React กับ Angular ก็เหมือนกับการเปรียบเทียบแอปเปิ้ลกับส้ม ในขณะที่อันหนึ่งเป็นไลบรารีที่เกี่ยวข้องกับมุมมอง อีกอันหนึ่งเป็นเฟรมเวิร์กที่ครบถ้วน

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

ความแตกต่างที่ใหญ่ที่สุดอยู่ในการจัดการของรัฐ Angular มาพร้อมกับการรวมข้อมูลเข้าด้วยกัน ในขณะที่ React ในปัจจุบันมักจะถูกเสริมโดย Redux เพื่อให้การไหลของข้อมูลแบบทิศทางเดียวและทำงานกับข้อมูลที่ไม่เปลี่ยนรูป สิ่งเหล่านี้เป็นแนวทางที่ตรงกันข้ามในสิทธิของตนเอง และการอภิปรายนับไม่ถ้วนกำลังเกิดขึ้นว่าการเชื่อมโยงข้อมูลที่ไม่แน่นอน/การผูกข้อมูลนั้นดีกว่าหรือแย่กว่าที่ไม่เปลี่ยนรูปแบบ/ทิศทางเดียว

สนามเล่นระดับ

เนื่องจาก React นั้นแฮ็คง่ายกว่าอย่างมีชื่อเสียง ฉันจึงตัดสินใจเพื่อจุดประสงค์ในการเปรียบเทียบนี้ เพื่อสร้างการตั้งค่า React ที่สะท้อน Angular อย่างใกล้ชิดพอสมควรเพื่อให้สามารถเปรียบเทียบข้อมูลโค้ดแบบเคียงข้างกัน

คุณลักษณะบางอย่างของ Angular ที่โดดเด่นแต่ไม่ได้อยู่ใน React โดยค่าเริ่มต้นคือ:

คุณสมบัติ แพ็คเกจเชิงมุม ห้องสมุดโต้ตอบ
การผูกข้อมูล การพึ่งพาการฉีด (DI) @เชิงมุม/แกน MobX
คุณสมบัติการคำนวณ rxjs MobX
การกำหนดเส้นทางตามส่วนประกอบ @angular/เราเตอร์ ตอบสนองเราเตอร์ v4
ส่วนประกอบการออกแบบวัสดุ @เชิงมุม/วัสดุ กล่องเครื่องมือตอบโต้
CSS กำหนดขอบเขตไปยังส่วนประกอบ @เชิงมุม/แกน โมดูล CSS
การตรวจสอบแบบฟอร์ม @angular/แบบฟอร์ม แบบฟอร์มสถานะ
เครื่องกำเนิดโครงการ @เชิงมุม/คลิ สคริปต์ตอบโต้ TS

การผูกข้อมูล

การผูกข้อมูลอาจเริ่มต้นได้ง่ายกว่าวิธีการแบบทิศทางเดียว แน่นอน มันเป็นไปได้ที่จะไปในทิศทางตรงกันข้ามโดยสิ้นเชิง และใช้ Redux หรือ mobx-state-tree กับ React และ ngrx กับ Angular แต่นั่นจะเป็นหัวข้อสำหรับโพสต์อื่น

คุณสมบัติการคำนวณ

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

ด้วย React คุณสามารถใช้ @computed จาก MobX ซึ่งบรรลุวัตถุประสงค์เดียวกัน โดยมี API ที่ดีกว่าเล็กน้อย

การฉีดพึ่งพา

การพึ่งพาอาศัยกันเป็นเรื่องที่ถกเถียงกันเพราะมันขัดกับกระบวนทัศน์ React ปัจจุบันของการเขียนโปรแกรมเชิงฟังก์ชันและความไม่เปลี่ยนรูป ตามที่ปรากฎ การฉีดพึ่งพาบางชนิดแทบจะขาดไม่ได้ในสภาพแวดล้อมการผูกข้อมูล เนื่องจากช่วยในการแยก (และจำลองและทดสอบ) ซึ่งไม่มีสถาปัตยกรรมชั้นข้อมูลแยกต่างหาก

ข้อดีอีกประการหนึ่งของ DI (รองรับใน Angular) คือความสามารถในการมีวงจรชีวิตที่แตกต่างกันของร้านค้าต่างๆ กระบวนทัศน์ React ปัจจุบันส่วนใหญ่ใช้สถานะแอปส่วนกลางบางประเภทซึ่งจับคู่กับส่วนประกอบต่างๆ แต่จากประสบการณ์ของฉัน ทั้งหมดนั้นง่ายเกินไปที่จะแนะนำจุดบกพร่องเมื่อล้างสถานะส่วนกลางในการยกเลิกการต่อเชื่อมส่วนประกอบ

การมีร้านค้าที่สร้างขึ้นจากการเมาต์ส่วนประกอบ (และใช้งานได้อย่างราบรื่นสำหรับลูกของส่วนประกอบนี้) ดูเหมือนจะมีประโยชน์จริงๆ และแนวคิดมักถูกมองข้าม

แกะกล่องใน Angular แต่สามารถทำซ้ำได้ง่ายด้วย MobX เช่นกัน

การกำหนดเส้นทาง

การกำหนดเส้นทางแบบอิงคอมโพเนนต์ช่วยให้คอมโพเนนต์สามารถจัดการเส้นทางย่อยของตนเองได้ แทนที่จะมีการกำหนดค่าเราเตอร์ทั่วโลกขนาดใหญ่เพียงรายการเดียว วิธีนี้ทำให้ react-router ในเวอร์ชัน 4 ได้ในที่สุด

การออกแบบวัสดุ

เป็นเรื่องดีเสมอที่จะเริ่มต้นด้วยองค์ประกอบระดับสูง และการออกแบบวัสดุได้กลายเป็นสิ่งที่เหมือนกับตัวเลือกเริ่มต้นที่เป็นที่ยอมรับในระดับสากล แม้แต่ในโครงการที่ไม่ใช่ของ Google

ฉันได้จงใจเลือก React Toolbox เหนือ Material UI ที่แนะนำโดยปกติ เนื่องจาก Material UI มีปัญหาด้านประสิทธิภาพที่ยอมรับในตัวเองอย่างร้ายแรงด้วยแนวทาง inline-CSS ซึ่งพวกเขาวางแผนที่จะแก้ไขในเวอร์ชันถัดไป

นอกจากนี้ PostCSS/cssnext ที่ใช้ใน React Toolbox กำลังเริ่มแทนที่ Sass/LESS อยู่ดี

CSS ที่มีขอบเขต

คลาส CSS นั้นเหมือนกับตัวแปรส่วนกลาง มีหลายวิธีในการจัดระเบียบ CSS เพื่อป้องกันความขัดแย้ง (รวมถึง BEM) แต่มีแนวโน้มที่ชัดเจนในปัจจุบันในการใช้ไลบรารีที่ช่วยประมวลผล CSS เพื่อป้องกันความขัดแย้งเหล่านั้นโดยไม่จำเป็นต้องให้นักพัฒนาส่วนหน้าออกแบบระบบการตั้งชื่อ CSS ที่ซับซ้อน

การตรวจสอบแบบฟอร์ม

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

เครื่องกำเนิดโครงการ

การมีตัวสร้าง CLI สำหรับโครงการนั้นสะดวกกว่าการโคลนสำเร็จรูปจาก GitHub เล็กน้อย

แอปพลิเคชันเดียวกัน สร้างสองครั้ง

ดังนั้นเราจะสร้างแอปพลิเคชันเดียวกันใน React และ Angular ไม่มีอะไรน่าตื่นเต้น มีเพียง Shoutboard ที่อนุญาตให้ทุกคนโพสต์ข้อความไปยังหน้าทั่วไป

คุณสามารถลองใช้แอปพลิเคชันได้ที่นี่:

  • Shoutboard Angular
  • ปฏิกิริยากระดานโต้คลื่น

แอปพลิเคชั่นกระดานโต้คลื่น

หากคุณต้องการมีซอร์สโค้ดทั้งหมด คุณสามารถรับได้จาก GitHub:

  • แหล่งที่มาของ Shoutboard เชิงมุม
  • แหล่ง React ของ Shoutboard

คุณจะสังเกตเห็นว่าเราใช้ TypeScript สำหรับแอป React ด้วยเช่นกัน ข้อดีของการตรวจสอบประเภทใน TypeScript นั้นชัดเจน และตอนนี้ เนื่องจากการจัดการการนำเข้าที่ดีขึ้น ในที่สุด async/await และ rest spread ก็ได้มาถึงใน TypeScript 2 มันจึงทิ้ง Babel/ES7/Flow ไว้ในผงธุลี

ให้เพิ่ม Apollo Client ให้กับทั้งคู่เพราะเราต้องการใช้ GraphQL ฉันหมายถึง REST นั้นยอดเยี่ยม แต่หลังจากผ่านไปสิบปีแล้ว มันก็เก่าไป

Bootstrap และการกำหนดเส้นทาง

ก่อนอื่น มาดูจุดเข้าใช้งานของทั้งสองแอปพลิเคชันกันก่อน

เชิงมุม

 const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'posts', component: PostsComponent }, { path: 'form', component: FormComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' } ] @NgModule({ declarations: [ AppComponent, PostsComponent, HomeComponent, FormComponent, ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ApolloModule.forRoot(provideClient), FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule, MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule ], providers: [ AppService ], bootstrap: [AppComponent] })
 @Injectable() export class AppService { username = 'Mr. User' }

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

ปฏิกิริยา

 const appStore = AppStore.getInstance() const routerStore = RouterStore.getInstance() const rootStores = { appStore, routerStore } ReactDOM.render( <Provider {...rootStores} > <Router history={routerStore.history} > <App> <Switch> <Route exact path='/home' component={Home as any} /> <Route exact path='/posts' component={Posts as any} /> <Route exact path='/form' component={Form as any} /> <Redirect from='/' to='/home' /> </Switch> </App> </Router> </Provider >, document.getElementById('root') )

คอมโพเนนต์ <Provider/> ใช้สำหรับการฉีดการพึ่งพาใน MobX มันบันทึกร้านค้าในบริบทเพื่อให้ส่วนประกอบ React สามารถฉีดเข้าไปได้ในภายหลัง ใช่ บริบทของ React สามารถใช้ (เนื้อหา) ได้อย่างปลอดภัย

เวอร์ชัน React สั้นกว่าเล็กน้อยเนื่องจากไม่มีการประกาศโมดูล โดยปกติแล้ว คุณเพียงแค่นำเข้าและพร้อมใช้งาน บางครั้งการพึ่งพาอาศัยกันอย่างหนักแบบนี้เป็นสิ่งที่ไม่ต้องการ (การทดสอบ) ดังนั้นสำหรับร้านค้าซิงเกิลตันทั่วโลก ฉันต้องใช้รูปแบบ GoF ที่มีอายุหลายสิบปีนี้:

 export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }

เราเตอร์ของ Angular เป็นแบบฉีดได้ ดังนั้นจึงสามารถใช้ได้จากทุกที่ ไม่ใช่แค่ส่วนประกอบเท่านั้น เพื่อให้บรรลุสิ่งเดียวกันใน react เราใช้แพ็คเกจ mobx-react-router และฉีด routerStore

สรุป: Bootstrapping ทั้งสองแอปพลิเคชันค่อนข้างตรงไปตรงมา React มีความได้เปรียบที่ง่ายกว่า โดยใช้เพียงการนำเข้าแทนโมดูล แต่อย่างที่เราจะเห็นในภายหลัง โมดูลเหล่านั้นมีประโยชน์มาก การทำซิงเกิลตันด้วยตนเองนั้นค่อนข้างน่ารำคาญ สำหรับไวยากรณ์การประกาศการกำหนดเส้นทาง JSON กับ JSX เป็นเพียงเรื่องของการตั้งค่า

ลิงค์และการนำทางที่จำเป็น

ดังนั้นมีสองกรณีในการเปลี่ยนเส้นทาง ประกาศ โดยใช้องค์ประกอบ <a href...> และความจำเป็น เรียก API การกำหนดเส้นทาง (และด้วยเหตุนี้ตำแหน่ง) โดยตรง

เชิงมุม

 <h1> Shoutboard Application </h1> <nav> <a routerLink="/home" routerLinkActive="active">Home</a> <a routerLink="/posts" routerLinkActive="active">Posts</a> </nav> <router-outlet></router-outlet>

เราเตอร์เชิงมุมจะตรวจจับโดยอัตโนมัติว่า routerLink ใดทำงานอยู่ และใส่คลาส routerLinkActive ที่เหมาะสมลงไป เพื่อให้สามารถกำหนดสไตล์ได้

เราเตอร์ใช้องค์ประกอบพิเศษ <router-outlet> เพื่อแสดงเส้นทางปัจจุบันตามคำสั่ง เป็นไปได้ที่จะมี <router-outlet> จำนวนมาก ในขณะที่เราเจาะลึกเข้าไปในส่วนประกอบย่อยของแอปพลิเคชัน

 @Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }

โมดูลของเราเตอร์สามารถนำไปใช้กับบริการใดๆ ก็ได้ (กึ่งเวทย์มนตร์ตามประเภท TypeScript) การประกาศ private จะเก็บไว้ในอินสแตนซ์โดยไม่ต้องมีการกำหนดอย่างชัดเจน ใช้วิธี navigate เพื่อสลับ URL

ปฏิกิริยา

 import * as style from './app.css' // … <h1>Shoutboard Application</h1> <div> <NavLink to='/home' activeClassName={style.active}>Home</NavLink> <NavLink to='/posts' activeClassName={style.active}>Posts</NavLink> </div> <div> {this.props.children} </div>

React Router ยังสามารถตั้งค่าคลาสของลิงก์ที่ใช้งานอยู่ด้วย activeClassName

ในที่นี้ เราไม่สามารถระบุชื่อคลาสได้โดยตรง เนื่องจากมันถูกสร้างขึ้นโดยคอมไพเลอร์โมดูล CSS และเราจำเป็นต้องใช้ตัวช่วย style เพิ่มเติมในภายหลัง

ตามที่เห็นด้านบน React Router ใช้องค์ประกอบ <Switch> ภายในองค์ประกอบ <App> เนื่องจากองค์ประกอบ <Switch> เพิ่งห่อและต่อเชื่อมเส้นทางปัจจุบัน หมายความว่าเส้นทางย่อยขององค์ประกอบปัจจุบันเป็นเพียง this.props.children ที่สามารถเรียบเรียงได้เช่นกัน

 export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }

mobx-router-store ยังช่วยให้ฉีดและนำทางได้ง่าย

สรุป: ทั้งสองวิธีในการกำหนดเส้นทางมีความใกล้เคียงกัน ดูเหมือนว่า Angular จะใช้งานง่ายกว่า ในขณะที่ React Router มีความสามารถในการเขียนที่ตรงไปตรงมามากกว่าเล็กน้อย

การฉีดพึ่งพา

ได้รับการพิสูจน์แล้วว่ามีประโยชน์ในการแยกชั้นข้อมูลออกจากชั้นการนำเสนอ สิ่งที่เราพยายามทำให้สำเร็จด้วย DI ที่นี่คือการทำให้ส่วนประกอบของชั้นข้อมูล (ในที่นี้เรียกว่าโมเดล/ร้านค้า/บริการ) เป็นไปตามวงจรชีวิตของส่วนประกอบที่มองเห็นได้ และด้วยเหตุนี้จึงทำให้สามารถสร้างส่วนประกอบดังกล่าวได้อย่างน้อยหนึ่งอินสแตนซ์โดยไม่จำเป็นต้องสัมผัสส่วนกลาง สถานะ. นอกจากนี้ ควรผสมและจับคู่ข้อมูลที่เข้ากันได้และเลเยอร์การแสดงภาพ

ตัวอย่างในบทความนี้ง่ายมาก ดังนั้นเนื้อหา DI ทั้งหมดอาจดูเหมือนใช้ยากเกินไป แต่ก็มีประโยชน์เมื่อแอปพลิเคชันเติบโตขึ้น

เชิงมุม

 @Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }

ดังนั้นคลาสใด ๆ ที่สามารถสร้างได้ @injectable และคุณสมบัติและเมธอดของคลาสนั้นทำให้ส่วนประกอบพร้อมใช้งาน

 @Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }

โดยการลงทะเบียน HomeService กับ providers ส่วนประกอบ เราทำให้พร้อมใช้งานสำหรับส่วนประกอบนี้โดยเฉพาะ ตอนนี้ไม่ใช่ซิงเกิลตัน แต่แต่ละอินสแตนซ์ของส่วนประกอบจะได้รับสำเนาใหม่ สดใหม่บนการต่อเชื่อมของส่วนประกอบ นั่นหมายถึงไม่มีข้อมูลเก่าจากการใช้งานครั้งก่อน

ในทางตรงกันข้าม AppService ได้รับการลงทะเบียนใน app.module (ดูด้านบน) ดังนั้นจึงเป็นซิงเกิลตันและยังคงเหมือนเดิมสำหรับส่วนประกอบทั้งหมด แม้ว่าอายุการใช้งานของแอปพลิเคชัน ความสามารถในการควบคุมวงจรชีวิตของบริการจากส่วนประกอบต่างๆ เป็นแนวคิดที่มีประโยชน์อย่างยิ่งแต่ยังไม่ค่อยได้รับการชื่นชม

DI ทำงานโดยกำหนดอินสแตนซ์บริการให้กับตัวสร้างของส่วนประกอบ ซึ่งระบุโดยประเภท TypeScript นอกจากนี้ คีย์เวิร์ด public จะกำหนดพารามิเตอร์ให้กับ this โดยอัตโนมัติ เพื่อที่เราจะได้ไม่ต้องเขียน this.homeService = homeService ที่น่าเบื่ออีกต่อไป

 <div> <h3>Dashboard</h3> <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <br/> <span>Clicks since last visit: {{homeService.counter}}</span> <button (click)='homeService.increment()'>Click!</button> </div>

ไวยากรณ์เทมเพลตของ Angular ค่อนข้างหรูหรา ฉันชอบทางลัด [()] ซึ่งทำงานเหมือนการผูกข้อมูลแบบ 2 ทาง แต่ภายใต้ประทุน จริงๆ แล้วมันคือการเชื่อมโยงแอตทริบิวต์ + เหตุการณ์ ตามที่วงจรชีวิตของบริการของเรากำหนด homeService.counter จะรีเซ็ตทุกครั้งที่เราออกจาก /home แต่ appService.username ยังคงอยู่และสามารถเข้าถึงได้จากทุกที่

ปฏิกิริยา

 import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }

ด้วย MobX เราจำเป็นต้องเพิ่ม @observable decorator ให้กับคุณสมบัติใด ๆ ที่เราต้องการให้สังเกตได้

 @observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }

ในการจัดการวงจรชีวิตอย่างถูกต้อง เราต้องทำงานมากกว่าในตัวอย่างเชิงมุมเล็กน้อย เรารวม HomeComponent ไว้ใน Provider ซึ่งได้รับอินสแตนซ์ใหม่ของ HomeStore ในแต่ละการเมานท์

 interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component<HomeComponentProps, any> { render() { const { homeStore, appStore } = this.props return <div> <h3>Dashboard</h3> <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>Clicks since last visit: {homeStore.counter}</span> <button onClick={homeStore.increment}>Click!</button> </div> } }

HomeComponent ใช้ @observer decorator เพื่อรับฟังการเปลี่ยนแปลงในคุณสมบัติ @observable

กลไกการทำงานใต้ท้องเครื่องนี้ค่อนข้างน่าสนใจ เรามาทำความเข้าใจกันสั้นๆ กันที่นี่ มัณฑนากร @observable แทนที่คุณสมบัติในอ็อบเจ็กต์ด้วย getter และ setter ซึ่งช่วยให้มันดักฟังการโทรได้ เมื่อเรียกใช้ฟังก์ชันการเรนเดอร์ของคอมโพเนนต์ที่เพิ่ม @observer คุณสมบัติเหล่านั้นจะถูกเรียก และพวกเขาเก็บการอ้างอิงไปยังส่วนประกอบที่เรียกใช้

จากนั้น เมื่อเรียก setter และค่าถูกเปลี่ยน ฟังก์ชันการเรนเดอร์ของส่วนประกอบที่ใช้คุณสมบัติในการเรนเดอร์ครั้งสุดท้ายจะถูกเรียก ตอนนี้ ข้อมูลเกี่ยวกับคุณสมบัติที่จะใช้เมื่อมีการอัพเดต และวงจรทั้งหมดสามารถเริ่มต้นใหม่ได้

กลไกที่ง่ายมากและค่อนข้างมีประสิทธิภาพเช่นกัน คำอธิบายเชิงลึกเพิ่มเติมที่นี่

มัณฑนากร @inject ใช้เพื่อฉีดอินสแตนซ์ของ appStore และ homeStore ลงในอุปกรณ์ประกอบฉากของ HomeComponent ณ จุดนี้ แต่ละร้านมีวงจรชีวิตที่แตกต่างกัน appStore จะเหมือนกันตลอดอายุการใช้งานของแอปพลิเคชัน แต่ homeStore ถูกสร้างขึ้นใหม่ในแต่ละการนำทางไปยังเส้นทาง "/home"

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

สรุป: ในฐานะที่เป็นการจัดการวงจรชีวิตของผู้ให้บริการในฟีเจอร์โดยธรรมชาติของ DI ของ Angular แน่นอนว่าการทำสำเร็จที่นั่นทำได้ง่ายกว่า เวอร์ชัน React ยังใช้งานได้ แต่เกี่ยวข้องกับต้นแบบมากกว่ามาก

คุณสมบัติการคำนวณ

ปฏิกิริยา

มาเริ่มกันที่ React กันก่อน มันมีวิธีแก้ปัญหาที่ตรงไปตรงมากว่านี้

 import { observable, computed, action } from 'mobx' export class HomeStore { import { observable, computed, action } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } @computed get counterMessage() { console.log('recompute counterMessage!') return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit` } }

ดังนั้นเราจึงมีคุณสมบัติที่คำนวณได้ซึ่งผูกกับตัว counter และส่งคืนข้อความที่มีพหูพจน์อย่างเหมาะสม ผลลัพธ์ของ counterMessage จะถูกแคช และคำนวณใหม่เมื่อตัว counter เปลี่ยนแปลงเท่านั้น

 <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>{homeStore.counterMessage}</span> <button onClick={homeStore.increment}>Click!</button>

จากนั้น เราอ้างอิงคุณสมบัติ (และวิธีการ increment ) จากเทมเพลต JSX ฟิลด์อินพุตถูกขับเคลื่อนโดยการผูกกับค่า และปล่อยให้เมธอดจาก appStore จัดการเหตุการณ์ของผู้ใช้

เชิงมุม

เพื่อให้บรรลุผลเช่นเดียวกันใน Angular เราจำเป็นต้องมีความคิดสร้างสรรค์มากขึ้น

 import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs/BehaviorSubject' @Injectable() export class HomeService { message = 'Welcome to home page' counterSubject = new BehaviorSubject(0) // Computed property can serve as basis for further computed properties counterMessage = new BehaviorSubject('') constructor() { // Manually subscribe to each subject that couterMessage depends on this.counterSubject.subscribe(this.recomputeCounterMessage) } // Needs to have bound this private recomputeCounterMessage = (x) => { console.log('recompute counterMessage!') this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`) } increment() { this.counterSubject.next(this.counterSubject.getValue() + 1) } }

เราจำเป็นต้องกำหนดค่าทั้งหมดที่ใช้เป็นพื้นฐานสำหรับคุณสมบัติที่คำนวณเป็น BehaviorSubject คุณสมบัติที่คำนวณเองยังเป็น BehaviorSubject เนื่องจากคุณสมบัติที่คำนวณใด ๆ สามารถทำหน้าที่เป็นอินพุตสำหรับคุณสมบัติที่คำนวณได้อื่น

แน่นอน RxJS สามารถทำได้มากกว่านี้ แต่นั่นอาจเป็นหัวข้อสำหรับบทความที่ต่างไปจากเดิมอย่างสิ้นเชิง ข้อเสียเล็กน้อยคือการใช้ RxJS เพียงเล็กน้อยสำหรับคุณสมบัติที่คำนวณได้นั้นละเอียดกว่าตัวอย่างการโต้ตอบเล็กน้อย และคุณต้องจัดการการสมัครสมาชิกด้วยตนเอง (เช่นที่นี่ในตัวสร้าง)

 <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <span>{{homeService.counterMessage | async}}</span> <button (click)='homeService.increment()'>Click!</button>

สังเกตว่าเราสามารถอ้างอิงหัวเรื่อง RxJS ด้วย | async . ได้อย่างไร ท่อ | async ซิงโครนัส นั่นเป็นสิ่งที่ดี สั้นกว่าการสมัครสมาชิกในส่วนประกอบของคุณมาก องค์ประกอบ input ถูกขับเคลื่อนโดยคำสั่ง [(ngModel)] แม้จะดูแปลกแต่จริง ๆ แล้วค่อนข้างสง่างาม เพียงน้ำตาลวากยสัมพันธ์สำหรับการผูกข้อมูลของค่ากับ appService.username และการกำหนดค่าอัตโนมัติจากเหตุการณ์อินพุตของผู้ใช้

สรุป: คุณสมบัติที่คำนวณได้นั้นง่ายต่อการใช้งานใน React/MobX มากกว่าใน Angular/RxJS แต่ RxJS อาจมีคุณสมบัติ FRP ที่มีประโยชน์มากกว่า ซึ่งอาจได้รับการชื่นชมในภายหลัง

เทมเพลตและ CSS

ในการแสดงว่าเทมเพลทซ้อนกันอย่างไร ลองใช้คอมโพเนนต์โพสต์ที่แสดงรายการโพสต์

เชิงมุม

 @Component({ selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [ PostsService ] }) export class PostsComponent implements OnInit { constructor( public postsService: PostsService, public appService: AppService ) { } ngOnInit() { this.postsService.initializePosts() } }

คอมโพเนนต์นี้เพียงเชื่อมต่อ HTML, CSS และบริการแบบฉีดเข้าด้วยกัน และยังเรียกใช้ฟังก์ชันเพื่อโหลดโพสต์จาก API ในการเริ่มต้น AppService เป็นซิงเกิลตันที่กำหนดไว้ในโมดูลแอปพลิเคชัน ในขณะที่ PostsService เป็นแบบชั่วคราว โดยมีอินสแตนซ์ใหม่ที่สร้างขึ้นในแต่ละเวลาที่สร้างองค์ประกอบ CSS ที่อ้างอิงจากส่วนประกอบนี้มีการกำหนดขอบเขตไว้ที่ส่วนประกอบนี้ ซึ่งหมายความว่าเนื้อหาไม่สามารถส่งผลกระทบใดๆ ภายนอกส่วนประกอบได้

 <a routerLink="/form" class="float-right"> <button md-fab> <md-icon>add</md-icon> </button> </a> <h3>Hello {{appService.username}}</h3> <md-card *ngFor="let post of postsService.posts"> <md-card-title>{{post.title}}</md-card-title> <md-card-subtitle>{{post.name}}</md-card-subtitle> <md-card-content> <p> {{post.message}} </p> </md-card-content> </md-card>

ในเทมเพลต HTML เราอ้างอิงส่วนประกอบส่วนใหญ่จากวัสดุเชิงมุม เพื่อให้พร้อมใช้งาน จำเป็นต้องรวมไว้ในการนำเข้า app.module (ดูด้านบน) คำสั่ง *ngFor ใช้เพื่อทำซ้ำองค์ประกอบ md-card สำหรับแต่ละโพสต์

CSS ท้องถิ่น:

 .mat-card { margin-bottom: 1rem; }

CSS ในพื้นที่เป็นเพียงการเพิ่มคลาสที่มีอยู่บนองค์ประกอบ md-card

CSS ทั่วโลก:

 .float-right { float: right; }

คลาสนี้ถูกกำหนดในไฟล์ global style.css เพื่อให้พร้อมใช้งานสำหรับส่วนประกอบทั้งหมด สามารถอ้างอิงได้ในวิธีมาตรฐาน class="float-right"

CSS ที่คอมไพล์แล้ว:

 .float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }

ใน CSS ที่คอมไพล์แล้ว เราจะเห็นว่า CSS ในเครื่องถูกกำหนดขอบเขตไปยังองค์ประกอบที่แสดงผลโดยใช้ตัวเลือกแอตทริบิวต์ [_ngcontent-c1] ทุกองค์ประกอบเชิงมุมที่แสดงผลมีคลาสที่สร้างขึ้นเช่นนี้สำหรับการกำหนดขอบเขต CSS

ข้อดีของกลไกนี้คือเราสามารถอ้างอิงคลาสได้ตามปกติ และการกำหนดขอบเขตจะได้รับการจัดการ "ภายใต้ประทุน"

ปฏิกิริยา

 import * as style from './posts.css' import * as appStyle from '../app.css' @observer export class Posts extends React.Component<any, any> { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return <Provider postsStore={this.postsStore}> <PostsComponent /> </Provider> } }

ใน React อีกครั้ง เราต้องใช้แนวทางของ Provider เพื่อทำให้การพึ่งพา PostsStore "ชั่วคราว" เรายังนำเข้าสไตล์ CSS ที่อ้างอิงเป็น style และ appStyle เพื่อให้สามารถใช้คลาสจากไฟล์ CSS เหล่านั้นใน JSX

 interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component<PostsComponentProps, any> { render() { const { postsStore, appStore } = this.props return <div> <NavLink to='form'> <Button icon='add' floating accent className={appStyle.floatRight} /> </NavLink> <h3>Hello {appStore.username}</h3> {postsStore.posts.map(post => <Card key={post.id} className={style.messageCard}> <CardTitle title={post.title} subtitle={post.name} /> <CardText>{post.message}</CardText> </Card> )} </div> } }

โดยธรรมชาติแล้ว JSX ให้ความรู้สึก JavaScript-y มากกว่าเทมเพลต HTML ของ Angular ซึ่งอาจเป็นสิ่งที่ดีหรือไม่ดีก็ได้ขึ้นอยู่กับรสนิยมของคุณ แทนที่จะใช้ *ngFor directive เราใช้โครงสร้าง map เพื่อวนซ้ำโพสต์

ตอนนี้ Angular อาจเป็นเฟรมเวิร์กที่ทำให้ TypeScript น่าสนใจที่สุด แต่จริงๆ แล้วมันคือ JSX ที่ TypeScript โดดเด่นจริงๆ ด้วยการเพิ่มโมดูล CSS (นำเข้าด้านบน) จะทำให้การเข้ารหัสเทมเพลตของคุณกลายเป็นโค้ดที่สมบูรณ์ ทุกสิ่งเดียวได้รับการตรวจสอบประเภท ส่วนประกอบ คุณลักษณะ แม้กระทั่งคลาส CSS ( appStyle.floatRight และ style.messageCard ดูด้านล่าง) และแน่นอน ลักษณะแบบลีนของ JSX นั้นสนับสนุนให้แยกออกเป็นส่วนประกอบและส่วนย่อยมากกว่าเทมเพลตของ Angular เล็กน้อย

CSS ท้องถิ่น:

 .messageCard { margin-bottom: 1rem; }

CSS ทั่วโลก:

 .floatRight { float: right; }

CSS ที่คอมไพล์แล้ว:

 .floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }

อย่างที่คุณเห็น postfix ตัวโหลดโมดูล CSS แต่ละคลาส CSS ด้วย postfix แบบสุ่ม ซึ่งรับประกันความเป็นเอกลักษณ์ วิธีตรงไปตรงมาเพื่อหลีกเลี่ยงความขัดแย้ง คลาสจะถูกอ้างอิงผ่านอ็อบเจ็กต์ที่นำเข้า webpack ข้อเสียประการหนึ่งที่เป็นไปได้คือคุณไม่สามารถสร้าง CSS ด้วยคลาสและเพิ่มมันได้ ดังที่เราทำในตัวอย่างเชิงมุม ในทางกลับกัน นี่อาจเป็นสิ่งที่ดี เพราะมันบังคับให้คุณใส่สไตล์ลงในแค็ปซูลอย่างเหมาะสม

สรุป: โดยส่วนตัวแล้วฉันชอบ JSX มากกว่าเทมเพลต Angular โดยเฉพาะอย่างยิ่งเนื่องจากการเติมโค้ดให้สมบูรณ์และรองรับการตรวจสอบประเภท นั่นเป็นคุณสมบัตินักฆ่าจริงๆ ตอนนี้ Angular มีคอมไพเลอร์ AOT ซึ่งสามารถมองเห็นบางสิ่งได้เช่นกัน การเติมโค้ดให้สมบูรณ์ก็ใช้งานได้ประมาณครึ่งหนึ่งของเนื้อหาที่นั่น แต่ยังไม่สมบูรณ์เท่ากับ JSX/TypeScript

GraphQL - กำลังโหลดข้อมูล

ดังนั้นเราจึงตัดสินใจใช้ GraphQL เพื่อจัดเก็บข้อมูลสำหรับแอปพลิเคชันนี้ วิธีที่ง่ายที่สุดวิธีหนึ่งในการสร้างแบ็คเอนด์ของ GraphQL คือการใช้ BaaS เช่น Graphcool นั่นคือสิ่งที่เราทำ โดยพื้นฐานแล้ว คุณเพียงแค่กำหนดแบบจำลองและคุณลักษณะ และ CRUD ของคุณก็พร้อมใช้

รหัสทั่วไป

เนื่องจากโค้ดที่เกี่ยวข้องกับ GraphQL บางตัวเหมือนกัน 100% สำหรับการใช้งานทั้งสอง อย่าทำซ้ำสองครั้ง:

 const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `

GraphQL เป็นภาษาคิวรีที่มีเป้าหมายเพื่อให้มีชุดการทำงานที่สมบูรณ์ยิ่งขึ้นเมื่อเปรียบเทียบกับปลายทาง RESTful แบบคลาสสิก ลองแยกคำถามนี้โดยเฉพาะ

  • PostsQuery เป็นเพียงชื่อสำหรับข้อความค้นหานี้เพื่ออ้างอิงในภายหลัง มันสามารถตั้งชื่ออะไรก็ได้
  • allPosts เป็นส่วนที่สำคัญที่สุด - มันอ้างอิงฟังก์ชันเพื่อค้นหาระเบียนทั้งหมดด้วยโมเดล 'โพสต์' ชื่อนี้สร้างโดย Graphcool
  • orderBy และ first คือพารามิเตอร์ของฟังก์ชัน allPosts createdAt เป็นหนึ่งในแอตทริบิวต์ของโมเดล Post first: 5 หมายความว่าจะส่งกลับเพียง 5 ผลลัพธ์แรกของแบบสอบถาม
  • id , name , title และ message เป็นแอตทริบิวต์ของโมเดล Post ที่เราต้องการรวมไว้ในผลลัพธ์ แอตทริบิวต์อื่นๆ จะถูกกรองออก

อย่างที่คุณเห็นแล้ว มันค่อนข้างทรงพลัง ดูหน้านี้เพื่อทำความคุ้นเคยกับการสืบค้นข้อมูลของ GraphQL

 interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }

ใช่ ในฐานะพลเมืองของ TypeScript ที่ดี เราสร้างอินเทอร์เฟซสำหรับผลลัพธ์ GraphQL

เชิงมุม

 @Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }

แบบสอบถาม GraphQL เป็น RxJS ที่สังเกตได้ และเราสมัครรับข้อมูล มันใช้งานได้เหมือนสัญญา แต่ไม่มาก ดังนั้นเราจึงโชคไม่ดีที่ใช้ async/await แน่นอนว่ายังมี Promise แต่ดูเหมือนว่าจะไม่ใช่วิธีเชิงมุมอยู่ดี เราตั้งค่า fetchPolicy: 'network-only' เพราะในกรณีนี้ เราไม่ต้องการแคชข้อมูล แต่ดึงข้อมูลในแต่ละครั้ง

ปฏิกิริยา

 export class PostsStore { appStore: AppStore @observable posts: Array<Post> = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }

เวอร์ชัน React เกือบจะเหมือนกัน แต่เนื่องจาก apolloClient ใช้คำสัญญา เราจึงสามารถใช้ประโยชน์จากไวยากรณ์ async/await ได้ มีวิธีอื่นใน React ที่เพียงแค่ "เทป" การสืบค้น GraphQL ไปยังส่วนประกอบลำดับที่สูงกว่า แต่สำหรับฉันแล้วดูเหมือนว่าการผสมผสานข้อมูลและเลเยอร์การนำเสนอเข้าด้วยกันมากเกินไป

สรุป: แนวคิดของการสมัครสมาชิก RxJS กับ async/await ค่อนข้างเหมือนกัน

GraphQL - การบันทึกข้อมูล

รหัสทั่วไป

อีกครั้งบางรหัสที่เกี่ยวข้องกับ GraphQL:

 const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `

วัตถุประสงค์ของการกลายพันธุ์คือการสร้างหรืออัปเดตระเบียน ดังนั้นจึงเป็นประโยชน์ที่จะประกาศตัวแปรบางตัวที่มีการกลายพันธุ์เพราะเป็นวิธีการส่งข้อมูลเข้าไป ดังนั้นเราจึงมีตัวแปร name , title และ message ซึ่งพิมพ์เป็น String ซึ่งเราจำเป็นต้องกรอกทุกครั้งที่เราเรียกการกลายพันธุ์นี้ ฟังก์ชัน createPost ถูกกำหนดโดย Graphcool อีกครั้ง เราระบุว่าคีย์ของโมเดล Post จะมีค่าจากตัวแปร out mutation และเราต้องการให้ส่งเฉพาะ id ของ Post ที่สร้างขึ้นใหม่เป็นการตอบแทน

เชิงมุม

 @Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } }

เมื่อเรียก apollo.mutate เราจำเป็นต้องจัดเตรียมการกลายพันธุ์ที่เราเรียกและตัวแปรด้วย เราได้รับผลลัพธ์ในการ subscribe โทรกลับและใช้ router ที่ฉีดเพื่อนำทางกลับไปที่รายการโพสต์

ปฏิกิริยา

 export class FormStore { constructor() { this.appStore = AppStore.getInstance() this.routerStore = RouterStore.getInstance() this.postFormState = new PostFormState() } submit = async () => { await this.postFormState.form.validate() if (this.postFormState.form.error) return const result = await this.appStore.apolloClient.mutate( { mutation: AddPostMutation, variables: { name: this.appStore.username, title: this.postFormState.title.value, message: this.postFormState.message.value } } ) this.goBack() } goBack = () => { this.routerStore.history.push('/posts') } }

คล้ายกันมากกับด้านบน โดยมีความแตกต่างของการฉีดพึ่งพา "แบบแมนนวล" มากกว่า และการใช้ async/await

สรุป: อีกครั้งที่นี่ไม่แตกต่างกันมากนัก สมัครสมาชิกกับ async/await นั้นแตกต่างกันโดยสิ้นเชิง

แบบฟอร์ม

เราต้องการบรรลุเป้าหมายต่อไปนี้ด้วยแบบฟอร์มในแอปพลิเคชันนี้:

  • การผูกข้อมูลของเขตข้อมูลกับแบบจำลอง
  • ข้อความตรวจสอบความถูกต้องสำหรับแต่ละช่อง กฎหลายข้อ
  • รองรับการตรวจสอบว่าทั้งแบบฟอร์มถูกต้องหรือไม่

ปฏิกิริยา

 export const check = (validator, message, options) => (value) => (!validator(value, options) && message) export const checkRequired = (msg: string) => check(nonEmpty, msg) export class PostFormState { title = new FieldState('').validators( checkRequired('Title is required'), check(isLength, 'Title must be at least 4 characters long.', { min: 4 }), check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }), ) message = new FieldState('').validators( checkRequired('Message cannot be blank.'), check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }), check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }), ) form = new FormState({ title: this.title, message: this.message }) }

ดังนั้น ไลบรารี formstate จะทำงานดังนี้: สำหรับแต่ละฟิลด์ในแบบฟอร์มของคุณ คุณต้องกำหนด FieldState พารามิเตอร์ที่ส่งผ่านคือค่าเริ่มต้น คุณสมบัติ validators ใช้ฟังก์ชัน ซึ่งส่งคืน "false" เมื่อค่าถูกต้อง และข้อความตรวจสอบความถูกต้องเมื่อค่าไม่ถูกต้อง ด้วยฟังก์ชันตัวช่วย check และ checkRequired ฟังก์ชันทั้งหมดสามารถแสดงได้อย่างชัดเจน

เพื่อให้มีการตรวจสอบความถูกต้องสำหรับทั้งฟอร์ม การรวมฟิลด์เหล่านั้นด้วยอินสแตนซ์ FormState นั้นมีประโยชน์ด้วย ซึ่งจะทำให้มีความถูกต้องโดยรวม

 @inject('appStore', 'formStore') @observer export class FormComponent extends React.Component<FormComponentProps, any> { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return <div> <h2> Create a new post </h2> <h3> You are now posting as {appStore.username} </h3> <Input type='text' label='Title' name='title' error={postFormState.title.error} value={postFormState.title.value} onChange={postFormState.title.onChange} /> <Input type='text' multiline={true} rows={3} label='Message' name='message' error={postFormState.message.error} value={postFormState.message.value} onChange={postFormState.message.onChange} />

อินสแตนซ์ FormState ให้คุณสมบัติ value onChange และ error ซึ่งสามารถใช้กับส่วนประกอบส่วนหน้าได้อย่างง่ายดาย

 <Button label='Cancel' onClick={formStore.goBack} raised accent /> &nbsp; <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }

When form.hasError is true , we keep the button disabled. The submit button sends the form to the GraphQL mutation presented earlier.

Angular

In Angular, we are going to use FormService and FormBuilder , which are parts of the @angular/forms package.

 @Component({ selector: 'app-form', templateUrl: './form.component.html', providers: [ FormService ] }) export class FormComponent { postForm: FormGroup validationMessages = { 'title': { 'required': 'Title is required.', 'minlength': 'Title must be at least 4 characters long.', 'maxlength': 'Title cannot be more than 24 characters long.' }, 'message': { 'required': 'Message cannot be blank.', 'minlength': 'Message is too short, minimum is 50 characters', 'maxlength': 'Message is too long, maximum is 1000 characters' } }

First, let's define the validation messages.

 constructor( private router: Router, private formService: FormService, public appService: AppService, private fb: FormBuilder, ) { this.createForm() } createForm() { this.postForm = this.fb.group({ title: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(24)] ], message: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(1000)] ], }) }

Using FormBuilder , it's quite easy to create the form structure, even more succintly than in the React example.

 get validationErrors() { const errors = {} Object.keys(this.postForm.controls).forEach(key => { errors[key] = '' const control = this.postForm.controls[key] if (control && !control.valid) { const messages = this.validationMessages[key] Object.keys(control.errors).forEach(error => { errors[key] += messages[error] + ' ' }) } }) return errors }

To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. This is not entirely a drawback; it, for example, lends itself more easily to internationalization.

 onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }

Again, when the form is valid, data can be sent to GraphQL mutation.

 <h2> Create a new post </h2> <h3> You are now posting as {{appService.username}} </h3> <form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate> <md-input-container> <input mdInput placeholder="Title" formControlName="title"> <md-error>{{validationErrors['title']}}</md-error> </md-input-container> <br> <br> <md-input-container> <textarea mdInput placeholder="Message" formControlName="message"></textarea> <md-error>{{validationErrors['message']}}</md-error> </md-input-container> <br> <br> <button md-raised-button (click)="onCancel()" color="warn">Cancel</button> <button md-raised-button type="submit" color="primary" [disabled]="postForm.dirty && !postForm.valid">Submit</button> <br> <br> </form>

The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm" assignment. Fields inside the form are bound to the form model through the formControlName property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.

Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.

Bundle size

Oh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.

  • Angular: 1200 KB
  • React: 300 KB

Well, not much surprise here. Angular has always been the bulkier one.

When using gzip, the sizes go down to 275kb and 127kb respectively.

Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.

Flexibility of Libraries vs. Robustness of Framework

So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.

It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.

If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.

If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.

Again, you can get the complete source code of the application from this article here and here.

Or, if you prefer bigger, RealWorld examples:

  • RealWorld Angular 4+
  • RealWorld React/MobX

Choose Your Programming Paradigm First

Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.

React/Redux with its immutable/unidirectional paradigm is a completely different beast.

Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.

A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.

I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.

Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.

As for MobX, similar assessment applies. Really great, but underappreciated.

In conclusion: before choosing between React and Angular, choose your programming paradigm first.

mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.