Angular 與 React:哪個更適合 Web 開發?

已發表: 2022-03-11

有無數文章討論 React 或 Angular 是否是 Web 開發的更好選擇。 我們還需要另一個嗎?

我寫這篇文章的原因是因為沒有一篇已經發表的文章——儘管它們包含了深刻的見解——深入到足以讓實際的前端開發人員決定哪一篇可能適合他們的需求。

在本文中,您將了解 Angular 和 React 如何以截然不同的理念解決相似的前端問題,以及是否選擇其中一個只是個人喜好問題。 為了比較它們,我們將構建相同的應用程序兩次,一次使用 Angular,另一次使用 React。

Angular 不合時宜的公告

兩年前,我寫了一篇關於 React 生態系統的文章。 除其他要點外,該文章認為 Angular 已成為“預先宣布死亡”的受害者。 那時,對於不希望他們的項目在過時的框架上運行的任何人來說,在 Angular 和幾乎其他任何東西之間進行選擇是一件容易的事。 Angular 1 已經過時,Angular 2 甚至沒有 alpha 版本。

事後看來,這種擔憂或多或少是有道理的。 Angular 2 發生了巨大的變化,甚至在最終發布之前進行了重大的重寫。

兩年後,我們有了 Angular 4,並承諾從現在開始相對穩定。

怎麼辦?

Angular vs. React:比較蘋果和橘子

有人說比較 React 和 Angular 就像比較蘋果和橘子。 一個是處理視圖的庫,另一個是成熟的框架。

當然,大多數 React 開發人員都會在 React 中添加一些庫,以將其變成一個完整的框架。 話又說回來,這個堆棧的最終工作流程通常仍然與 Angular 有很大不同,因此可比性仍然有限。

最大的區別在於狀態管理。 Angular 捆綁了數據綁定,而今天的 React 通常由 Redux 增強,以提供單向數據流並處理不可變數據。 這些本身就是對立的方法,現在無數的討論都在討論可變/數據綁定是否比不可變/單向更好或更差。

一個公平競爭的環境

由於 React 以更容易破解而著稱,為了進行比較,我決定構建一個 React 設置,該設置可以合理地密切反映 Angular,以允許並排比較代碼片段。

某些突出但默認情況下不在 React 中的 Angular 功能是:

特徵角包反應庫
數據綁定、依賴注入 (DI) @角/核心MobX
計算屬性rxjs MobX
基於組件的路由@角/路由器反應路由器 v4
材料設計組件@角/材料反應工具箱
CSS 作用於組件@角/核心CSS 模塊
表單驗證@角/形式表單狀態
項目生成器@角/cli 反應腳本 TS

數據綁定

數據綁定可以說比單向方法更容易開始。 當然,也可以完全相反,將 Redux 或 mobx-state-tree 與 React 結合使用,將 ngrx 與 Angular 結合使用。 但這將是另一篇文章的主題。

計算屬性

雖然關注性能,但 Angular 中的普通 getter 在每次渲染時都會被調用,這是毫無疑問的。 可以使用 RsJS 中的 BehaviorSubject 來完成這項工作。

使用 React,可以使用來自 MobX 的 @computed,它可以實現相同的目標,並且可以說是更好的 API。

依賴注入

依賴注入有點爭議,因為它違背了當前的 React 函數式編程和不變性範式。 事實證明,某種依賴注入在數據綁定環境中幾乎是必不可少的,因為它有助於在沒有單獨的數據層架構的情況下解耦(從而模擬和測試)。

DI(在 Angular 中支持)的另一個優點是能夠擁有不同商店的不同生命週期。 大多數當前的 React 範例使用某種映射到不同組件的全局應用程序狀態,但根據我的經驗,在卸載組件時清理全局狀態時很容易引入錯誤。

擁有一個在組件安裝時創建的商店(並且可以無縫地提供給該組件的子組件)似乎非常有用,但經常被忽視。

在 Angular 中開箱即用,但也很容易在 MobX 中重現。

路由

基於組件的路由允許組件管理自己的子路由,而不是擁有一個大的全局路由器配置。 這種方法終於在版本 4 中實現了react-router

材料設計

從一些更高級別的組件開始總是好的,並且材料設計已經成為一種普遍接受的默認選擇,即使在非 Google 項目中也是如此。

我特意選擇了 React Toolbox 而不是通常推薦的 Material UI,因為 Material UI 在他們的內聯 CSS 方法中存在嚴重的性能問題,他們計劃在下一個版本中解決。

此外,React Toolbox 中使用的 PostCSS/cssnext 無論如何都開始取代 Sass/LESS。

作用域 CSS

CSS 類類似於全局變量。 有許多組織 CSS 以防止衝突的方法(包括 BEM),但目前有一個明顯的趨勢是使用庫來幫助處理 CSS 以防止這些衝突,而不需要前端開發人員設計複雜的 CSS 命名系統。

表單驗證

表單驗證是一項重要且使用非常廣泛的功能。 很高興將這些內容包含在庫中以防止代碼重複和錯誤。

項目生成器

為項目使用 CLI 生成器比從 GitHub 克隆樣板文件要方便一些。

相同的應用程序,構建兩次

所以我們將在 React 和 Angular 中創建相同的應用程序。 沒什麼了不起的,只是一個允許任何人將消息發佈到公共頁面的留言板。

您可以在此處試用應用程序:

  • 角燈板
  • 槍手反應

看板應用

如果您想擁有完整的源代碼,可以從 GitHub 獲取:

  • Shoutboard 角源
  • Shoutboard React 源碼

你會注意到我們也為 React 應用程序使用了 TypeScript。 TypeScript 中類型檢查的優勢是顯而易見的。 現在,隨著對導入的更好處理、async/await 和 rest spread 終於出現在 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 的路由器是可注入的,因此它可以在任何地方使用,而不僅僅是組件。 為了在 react 中實現同樣的效果,我們使用 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>

 @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 在<App>元素中使用<Switch>元素。 由於<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 路數據綁定,但在後台,它實際上是屬性綁定 + 事件。 正如我們服務的生命週期所決定的那樣,每次我們離開/homehomeService.counter都會重置,但appService.username保持不變,並且可以從任何地方訪問。

反應

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

使用 MobX,我們需要將@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擴展組件的渲染函數時,這些屬性 getter 會被調用,並且它們會保留對調用它們的組件的引用。

然後,當調用 setter 並更改值時,調用上次渲染時使用該屬性的組件的渲染函數。 現在,關於哪些屬性在哪裡使用的數據被更新,整個週期可以重新開始。

一個非常簡單的機制,也非常高效。 更深入的解釋在這裡。

@inject裝飾器用於將appStorehomeStore實例注入到HomeComponent的 props 中。 此時,這些商店中的每一個都有不同的生命週期。 appStore在應用程序的生命週期中是相同的,但homeStore是在每次導航到“/home”路徑時新創建的。

這樣做的好處是不需要手動清理屬性,因為所有商店都是全局的,如果路由是某個“詳細”頁面,每次都包含完全不同的數據,這會很痛苦。

總結:作為 Angular 的 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>

然後,我們從 JSX 模板中引用屬性(和increment方法)。 輸入字段通過綁定到一個值來驅動,並讓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>

注意我們如何使用| async來引用 RxJS 主題。 | 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 樣式,引用為styleappStyle ,以便能夠在 JSX 中使用這些 CSS 文件中的類。

 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 感覺比 Angular 的 HTML 模板更接近 JavaScript,這取決於你的喜好,這可能是好事還是壞事。 我們使用map構造來迭代帖子,而不是*ngFor指令。

現在,Angular 可能是最吹捧 TypeScript 的框架,但實際上 TypeScript 真正閃耀的地方是 JSX。 通過添加 CSS 模塊(上面導入),它真的把你的模板編碼變成了代碼完成禪。 每件事都經過類型檢查。 組件、屬性,甚至 CSS 類( appStyle.floatRightstyle.messageCard ,見下文)。 當然,與 Angular 的模板相比,JSX 的精簡特性更鼓勵拆分成組件和片段。

本地 CSS:

 .messageCard { margin-bottom: 1rem; }

全局 CSS:

 .floatRight { float: right; }

編譯的CSS:

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

如您所見,CSS 模塊加載器使用隨機後綴為每個 CSS 類添加後綴,這保證了唯一性。 避免衝突的直接方法。 然後通過 webpack 導入的對象引用類。 這樣做的一個可能的缺點是,您不能像我們在 Angular 示例中所做的那樣,只創建一個帶有類的 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是最重要的部分 - 它引用了使用 `Post` 模型查詢所有記錄的函數。 這個名字是由 Graphcool 創建的。
  • orderByfirstallPosts函數的參數。 createdAtPost模型的屬性之一。 first: 5表示它將只返回查詢的前 5 個結果。
  • idnametitlemessage是我們希望包含在結果中的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 的 observable,我們訂閱了它。 它有點像一個承諾,但不完全是,所以我們不走運使用async/await 。 當然,還有 toPromise,不過好像也不是 Angular 的方式。 我們設置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使用 Promise,我們可以利用async/await語法。 React 中還有其他方法只是將 GraphQL 查詢“磁帶”到高階組件,但在我看來,將數據和表示層混合在一起有點太多了。

總結: RxJS subscribe 和 async/await 的思路其實是一模一樣的。

GraphQL - 保存數據

通用代碼

同樣,一些 GraphQL 相關代碼:

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

突變的目的是創建或更新記錄。 因此,聲明一些帶有突變的變量是有益的,因為這些是如何將數據傳遞給它的方式。 所以我們有nametitlemessage變量,類型為String ,每次調用這個突變時都需要填充它們。 createPost函數同樣由 Graphcool 定義。 我們指定Post模型的鍵將具有來自突變變量的值,並且我們希望只發送新創建的 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的使用。

摘要:同樣,這裡沒有太大區別。 subscribe 與 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”,當值無效時返回驗證消息。 使用checkcheckRequired輔助函數,它看起來都具有很好的聲明性。

要對整個表單進行驗證,最好也使用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實例提供valueonChangeerror屬性,可以輕鬆地與任何前端組件一起使用。

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

捆綁大小

哦,還有一件事。 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.