Angular مقابل React: أيهما أفضل لتطوير الويب؟

نشرت: 2022-03-11

هناك عدد لا يحصى من المقالات التي تناقش ما إذا كان React أو Angular هو الخيار الأفضل لتطوير الويب. هل نحتاج واحد آخر؟

السبب في أنني كتبت هذا المقال هو أنه لم يتم نشر أي من المقالات بالفعل - على الرغم من أنها تحتوي على رؤى رائعة - تتعمق بما يكفي لمطور الواجهة الأمامية العملي ليقرر أي منها قد يناسب احتياجاته.

في هذه المقالة ، سوف تتعلم كيف يهدف كل من Angular و React إلى حل المشكلات الأمامية المتشابهة على الرغم من وجود فلسفات مختلفة جدًا ، وما إذا كان اختيار أحدهما أو الآخر هو مجرد مسألة تفضيل شخصي. لمقارنتهم ، سنبني نفس التطبيق مرتين ، مرة مع Angular ومرة ​​أخرى مع React.

إعلان Angular المفاجئ

قبل عامين ، كتبت مقالًا عن نظام React البيئي. من بين النقاط الأخرى ، جادل المقال بأن Angular أصبح ضحية "الموت عن طريق الإعلان المسبق". في ذلك الوقت ، كان الاختيار بين Angular وأي شيء آخر تقريبًا خيارًا سهلاً لأي شخص لا يريد أن يعمل مشروعه في إطار عمل قديم. كان Angular 1 قديمًا ، ولم يكن Angular 2 متاحًا حتى في إصدار ألفا.

بعد فوات الأوان ، كانت المخاوف مبررة إلى حد ما. تغيرت Angular 2 بشكل كبير ، بل إنها خضعت لإعادة كتابة كبيرة قبل الإصدار النهائي.

بعد ذلك بعامين ، لدينا Angular 4 مع وعد بالاستقرار النسبي من الآن فصاعدًا.

ماذا الآن؟

الزاوي مقابل رد الفعل: مقارنة التفاح والبرتقال

يقول بعض الناس أن المقارنة بين React و Angular تشبه مقارنة التفاح بالبرتقال. في حين أن إحداهما مكتبة تتعامل مع وجهات النظر ، فإن الأخرى عبارة عن إطار كامل.

بالطبع ، سيضيف معظم مطوري React بعض المكتبات إلى React لتحويلها إلى إطار عمل كامل. ثم مرة أخرى ، غالبًا ما يكون سير العمل الناتج لهذا المكدس مختلفًا تمامًا عن Angular ، لذلك لا تزال المقارنة محدودة.

الاختلاف الأكبر يكمن في إدارة الدولة. يأتي Angular مزودًا بربط البيانات المجمّع ، بينما عادةً ما يتم تعزيز React اليوم بواسطة Redux لتوفير تدفق بيانات أحادي الاتجاه والعمل مع بيانات غير قابلة للتغيير. هذه مناهج متعارضة في حد ذاتها ، وتجري الآن مناقشات لا حصر لها حول ما إذا كان ربط البيانات / المتغير أفضل أم أسوأ من غير قابل للتغيير / أحادي الاتجاه.

مستوى اللعب

نظرًا لأنه من المعروف أن React أسهل في الاختراق ، فقد قررت ، لغرض هذه المقارنة ، بناء إعداد React الذي يعكس Angular بشكل وثيق بشكل معقول للسماح بمقارنة مقتطفات التعليمات البرمجية جنبًا إلى جنب.

بعض ميزات Angular التي تبرز ولكنها ليست في React افتراضيًا هي:

ميزة الحزمة الزاوي مكتبة React
ربط البيانات ، حقن التبعية (DI) @ الزاوي / الأساسية MobX
الخصائص المحسوبة rxjs MobX
التوجيه القائم على المكونات @ الزاوي / راوتر رياكت راوتر v4.0
مكونات تصميم المواد @ الزاوي / المادة رد فعل Toolbox
نطاق CSS للمكونات @ الزاوي / الأساسية وحدات CSS
عمليات التحقق من صحة النموذج @ الزاوي / الأشكال FormState
مولد المشروع @ الزاوي / CLI رياكت سكربتات TS

ربط البيانات

يمكن القول إن ربط البيانات أسهل في البدء به من النهج أحادي الاتجاه. بالطبع ، سيكون من الممكن الذهاب في الاتجاه المعاكس تمامًا ، واستخدام Redux أو mobx-state-tree مع React ، و ngrx مع Angular. لكن هذا سيكون موضوعًا لمنشور آخر.

الخصائص المحسوبة

بينما يتعلق الأمر بالأداء ، فإن الحروف الواضحة في Angular هي ببساطة غير واردة حيث يتم استدعاؤها في كل تصيير. من الممكن استخدام BehaviorSubject من RsJS ، الذي يؤدي المهمة.

باستخدام React ، من الممكن استخدامcomputed من MobX ، والذي يحقق نفس الهدف ، مع واجهة برمجة تطبيقات أجمل قليلاً.

حقن التبعية

يعد حقن التبعية أمرًا مثيرًا للجدل لأنه يتعارض مع نموذج React الحالي للبرمجة الوظيفية والثبات. كما اتضح ، فإن نوعًا من حقن التبعية لا غنى عنه تقريبًا في بيئات ربط البيانات ، لأنه يساعد في الفصل (وبالتالي السخرية والاختبار) حيث لا توجد بنية منفصلة لطبقة البيانات.

ميزة أخرى لـ DI (المدعومة في Angular) هي القدرة على الحصول على دورات حياة مختلفة لمتاجر مختلفة. تستخدم معظم نماذج React الحالية نوعًا من حالة التطبيق العالمية التي تحدد مكونات مختلفة ، ولكن من واقع خبرتي ، من السهل جدًا إدخال أخطاء عند تنظيف الحالة العامة عند إلغاء تحميل المكون.

يبدو أن امتلاك متجر يتم إنشاؤه على حامل المكون (ويكون متاحًا بسهولة لأطفال هذا المكون) مفيد حقًا ، وغالبًا ما يتم تجاهله.

من خارج منطقة الجزاء في Angular ، ولكن يمكن استنساخه بسهولة مع MobX أيضًا.

التوجيه

يسمح التوجيه القائم على المكونات للمكونات بإدارة المسارات الفرعية الخاصة بها بدلاً من وجود تكوين واحد كبير لجهاز التوجيه العالمي. لقد جعل هذا النهج أخيرًا react-router في الإصدار 4.

تصميم المواد

من الجيد دائمًا البدء ببعض المكونات عالية المستوى ، وقد أصبح التصميم المادي شيئًا مثل خيار افتراضي مقبول عالميًا ، حتى في المشاريع غير التابعة لـ Google.

لقد اخترت React Toolbox عمدًا على واجهة المستخدم المادية الموصى بها عادةً ، حيث أن واجهة المستخدم المادية لديها مشاكل خطيرة في الأداء معترف بها ذاتيًا مع نهج CSS المضمّن ، والتي يخططون لحلها في الإصدار التالي.

إلى جانب ذلك ، بدأت PostCSS / cssnext المستخدمة في React Toolbox في استبدال Sass / LESS على أي حال.

نطاق CSS

فئات CSS تشبه المتغيرات العالمية. هناك العديد من الأساليب لتنظيم CSS لمنع التعارضات (بما في ذلك BEM) ، ولكن هناك اتجاه حالي واضح في استخدام المكتبات التي تساعد في معالجة CSS لمنع هذا التعارض دون الحاجة إلى مطور الواجهة الأمامية لابتكار أنظمة تسمية CSS متقنة.

التحقق من صحة النموذج

تعتبر عمليات التحقق من صحة النموذج ميزة غير تافهة ومستخدمة على نطاق واسع. من الجيد الحصول على تلك التي تغطيها مكتبة لمنع تكرار الكود والأخطاء.

مولد المشروع

يعد امتلاك منشئ CLI لمشروع ما أكثر ملاءمة قليلاً من الاضطرار إلى استنساخ النماذج المعيارية من GitHub.

نفس التطبيق ، بني مرتين

لذلك سننشئ نفس التطبيق في React و Angular. لا شيء مذهل ، فقط لوحة شوت بورد تسمح لأي شخص بإرسال رسائل إلى صفحة مشتركة.

يمكنك تجربة التطبيقات من هنا:

  • الزاوي Shoutboard
  • رد فعل Shoutboard

تطبيق Shoutboard

إذا كنت تريد الحصول على شفرة المصدر بالكامل ، فيمكنك الحصول عليها من GitHub:

  • مصدر الزاوي Shoutboard
  • مصدر رد فعل Shoutboard

ستلاحظ أننا استخدمنا TypeScript لتطبيق React أيضًا. مزايا التحقق من الكتابة في TypeScript واضحة. والآن ، مع وصول معالجة أفضل للواردات ، غير المتزامن / انتظار وانتشار الباقي أخيرًا في TypeScript 2 ، فإنه يترك Babel / ES7 / Flow في الغبار.

أيضًا ، دعنا نضيف Apollo Client لكليهما لأننا نريد استخدام GraphQL. أعني ، REST رائع ، لكن بعد عقد أو نحو ذلك ، أصبح قديمًا.

التمهيد والتوجيه

أولاً ، دعنا نلقي نظرة على نقطتي دخول كلا التطبيقين.

الزاوي

 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's Router قابل للحقن ، لذا يمكن استخدامه من أي مكان ، وليس من المكونات فقط. لتحقيق نفس الشيء في رد الفعل ، نستخدم حزمة mobx-React-router routerStore .

الملخص: يعد التمهيد لكلا التطبيقين أمرًا بسيطًا للغاية. تتميز 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>

يكتشف Angular Router تلقائيًا جهاز التوجيه ( routerLink ) النشط ، ويضع فئة routerLinkActive المناسبة عليه ، بحيث يمكن تصميمه.

يستخدم جهاز التوجيه عنصر <router-outlet> الخاص لتقديم ما يمليه المسار الحالي. من الممكن أن يكون لديك العديد من <router-outlet> s ، لأننا نتعمق أكثر في المكونات الفرعية للتطبيق.

 @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 ، يمكن القول إنها أنيقة للغاية. يعجبني الاختصار [()] ، الذي يعمل كربط بيانات ثنائي الاتجاه ، ولكن تحت الغطاء ، إنه في الواقع ارتباط + حدث سمة. وفقًا لدورة حياة خدماتنا ، ستتم إعادة تعيين homeService.counter في كل مرة ننتقل فيها بعيدًا عن /home ، ولكن يبقى appService.username ، ويمكن الوصول إليه من كل مكان.

تتفاعل

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

مع @observable ، نحتاج إلى إضافة مصممobservable إلى أي خاصية نريد جعلها قابلة للملاحظة.

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

لإدارة دورة الحياة بشكل صحيح ، نحتاج إلى القيام بعمل أكثر قليلاً من مثال Angular. نحن نلف 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 للاستماع إلى التغييرات في خصائص @observable .

آلية هذا الأمر مثيرة جدًا للاهتمام ، لذا دعنا نراجعها باختصار هنا. يستبدل مصمم @observable خاصية في كائن بـ getter و setter ، مما يسمح له باعتراض المكالمات. عندما يتم استدعاء وظيفة @observer المكون الإضافيobserver ، يتم استدعاء هذه الخصائص ، ويحتفظون بمرجع للمكون الذي دعاهم.

بعد ذلك ، عند استدعاء setter وتغيير القيمة ، يتم استدعاء وظائف التصيير للمكونات التي استخدمت الخاصية في آخر تصيير. الآن ، البيانات حول الخصائص التي يتم استخدامها حيث يتم تحديثها ، ويمكن أن تبدأ الدورة بأكملها من جديد.

آلية بسيطة للغاية وذات أداء جيد أيضًا. مزيد من الشرح المتعمق هنا.

يتم استخدام @inject decorator لحقن appStore و homeStore في HomeComponent . في هذه المرحلة ، لكل من هذه المتاجر دورة حياة مختلفة. appStore هو نفسه أثناء عمر التطبيق ، ولكن تم إنشاء homeStore حديثًا في كل تنقل إلى مسار "/ home".

وتكمن فائدة ذلك في أنه ليس من الضروري تنظيف الخصائص يدويًا كما هو الحال عندما تكون جميع المتاجر عالمية ، وهو أمر مؤلم إذا كان المسار عبارة عن صفحة "تفاصيل" تحتوي على بيانات مختلفة تمامًا في كل مرة.

الملخص: نظرًا لكونك إدارة دورة حياة المزود في ميزة متأصلة في Angular's DI ، فمن السهل بالطبع تحقيقها هناك. نسخة 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 ، نشير في الغالب إلى المكونات من Angular Material. لإتاحتها ، كان من الضروري تضمينها في واردات app.module (انظر أعلاه). يتم استخدام التوجيه *ngFor لتكرار مكون md-card لكل منشور.

CSS المحلي:

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

يعمل CSS المحلي على زيادة إحدى الفئات الموجودة في مكون md-card .

CSS العالمية:

 .float-right { float: right; }

يتم تعريف هذه الفئة في ملف style.css العام لإتاحتها لجميع المكونات. يمكن الرجوع إليها بالطريقة القياسية ، class="float-right" .

تم تجميع CSS:

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

في CSS المترجمة ، يمكننا أن نرى أنه تم تحديد نطاق CSS المحلي للمكوِّن المعروض باستخدام محدد السمة [_ngcontent-c1] . يحتوي كل مكون Angular يتم تقديمه على فئة تم إنشاؤها مثل هذه لأغراض تحديد نطاق 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 بجافا سكريبت- y أكثر بكثير من قوالب HTML الخاصة بـ Angular ، والتي يمكن أن تكون شيئًا جيدًا أو سيئًا حسب ذوقك. بدلاً من التوجيه *ngFor ، نستخدم بنية 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; }

كما ترى ، تُصلح أداة تحميل وحدات CSS النمطية كل فئة من فئات CSS بإصلاح لاحق عشوائي ، مما يضمن التفرد. طريقة مباشرة لتجنب النزاعات. ثم تتم الإشارة إلى الفئات من خلال كائنات webpack المستوردة. قد يكون أحد العيوب المحتملة لهذا هو أنه لا يمكنك فقط إنشاء CSS بفئة وزيادتها ، كما فعلنا في مثال Angular. من ناحية أخرى ، يمكن أن يكون هذا أمرًا جيدًا في الواقع ، لأنه يجبرك على تغليف الأنماط بشكل صحيح.

الملخص: أنا شخصياً أحب JSX أفضل قليلاً من القوالب الزاويّة ، خاصة بسبب إكمال الكود ونوع دعم التحقق. هذه حقا ميزة قاتلة. يحتوي 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 هو الجزء الأكثر أهمية - فهو يشير إلى الوظيفة للاستعلام عن جميع السجلات باستخدام نموذج `Post`. تم إنشاء هذا الاسم بواسطة 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 يمكن ملاحظته ، ونحن نشترك فيه. إنه يشبه إلى حد ما الوعد ، ولكن ليس تمامًا ، لذلك لم يحالفنا الحظ في استخدام غير async/await . بالطبع ، لا يزال هناك وعد ، لكن لا يبدو أنه الطريقة الزاويّة على أي حال. قمنا بتعيين 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 مقابل غير المتزامن / الانتظار هي نفسها تمامًا.

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 ستحتوي على قيم من متغيرات الطفرات الخارجية ، ونريد أيضًا إرسال id الذي تم إنشاؤه حديثًا فقط في المقابل.

الزاوي

 @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 .

ملخص: مرة أخرى ، ليس هناك فرق كبير هنا. الاشتراك مقابل عدم التزامن / الانتظار هو في الأساس كل ما يختلف.

نماذج

نريد تحقيق الأهداف التالية من خلال النماذج الموجودة في هذا التطبيق:

  • ربط بيانات الحقول بنموذج
  • رسائل التحقق من الصحة لكل حقل ، قواعد متعددة
  • دعم للتحقق مما إذا كان النموذج بأكمله صالحًا

تتفاعل

 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 and 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} />

The FormState instance provides value , onChange , and error properties, which can be easily used with any front-end components.

 <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.