[譯]Angular vs React:誰更適合前端開發

Angular vs React:誰更適合前端開發

你們總在寫文章爭論,Angular 與 React 哪個纔是前端開發的更好選擇(譯者:在中國還要加上 vue :P)。咱們還須要另外一個嗎?

我之因此寫這篇文章,是由於這些的文章 —— 雖然它們包含不錯的觀點 —— 並無深刻討論:做爲一個實際的前端開發者,應該選取哪一種框架來知足本身的需求。javascript

在本文中,我會介紹 Angular 與 React 如何用不一樣的哲♂學理念解決相同的前端問題,以及選擇哪一種框架基本上是看我的喜愛。爲了方便進行比較,我準備編寫同一個 app 兩次,一次使用 Angular 一次使用 React。css

Angular 之殤

兩年前,我寫了一篇有關 React 生態系統的文章。在我看來,Angular 是「預發佈時就跪了」的倒黴蛋(victim of 「death by pre-announcement」)。那個時候,任何不想讓本身項目跑在過期框架上的開發者很容易在 Angular 和 React 之間作出選擇。Angular 1 就是被時代拋棄的框架,(本來的)Angular 2 甚至沒有活到 alpha 版本。html

不過過後證實,這種擔憂是多多少少有合理性的。Angular 2 進行了大幅度的修改,甚至在最終發佈前對主要部分進行了重寫。前端

兩年後,咱們有了相對穩定的 Angular 4。vue

怎麼樣?java

Angular vs React:風馬牛不相及 (Comparing Apples and Oranges)

把 React 和 Angular 拿來比較是件很沒意義的事情(校對逆寒: Comparing Apples and Oranges 是一種俚語說法,比喻把兩件徹底不一樣的東西拿來相提並論)。由於 React 只是一個處理界面(view)的庫,而 Angular 是一個完整齊備的全家桶框架。react

固然,大部分 React 開發者會添加一系列的庫,使得 React 成爲完整的框架。可是這套完整框架的工做流程又一次和 Angular 徹底不一樣,因此其可比性也頗有限。android

二者最大的差異是對狀態(state)的管理。Angular 經過數據綁定(data-binding)來將狀態綁在數據上,而 React 現在一般引入 Redux 來提供單向數據流、處理不可變的數據(譯者:我我的理解這句話的意思是 Angular 的數據和狀態是互相影響的,而 React 只能經過切換不一樣的狀態來顯示不一樣的數據)。這是恰好互相對立的解決問題方法,而開發者們則不停的爭論可變的/數據綁定模式不可變的/單向的數據流二者間誰更優秀。webpack

公平競爭的環境

既然 React 更容易理解,爲了便於比較,我決定編寫一份 React 與 Angular 的對應表,來合理的並排比較二者的代碼結構。ios

Angular 中有可是 React 沒有默認自帶的特性有:

特性 — Angular 包 — React 庫

  • 數據綁定,依賴注入(DI)—— @angular/core — MobX

  • 計算屬性 —— rxjs— MobX

  • 基於組件的路由 —— @angular/router— React Router v4

  • Material design 的組件 —— @angular/material— React Toolbox

  • CSS 組件做用域 —— @angular/core — CSS modules

  • 表單驗證 —— @angular/forms — FormState

  • 程序生產器(Project generator)—— @angular/cli — React Scripts TS

數據綁定

相對單向數據流來講,數據綁定可能更適合入門。固然,也可使用徹底相反的作法(指單向數據流),好比使用 React 中的 Redux 或者 mobx-state-tree,或者使用 Angular 中的 ngrx。不過那就是另外一篇文章所要闡述的內容了。

計算屬性(Computed properties)

「除存儲屬性外,類、結構體和枚舉能夠定義計算屬性,計算屬性不直接存儲值,而是提供一個 getter 來獲取值,一個可選的 setter
來間接設置其餘屬性或變量的值。」

摘錄來自: Unknown. 「The Swift Programming Language 中文版」。 iBooks.

考慮到性能問題,Angular 中簡單的 getters 每次渲染時都被調用,因此被排除在外。此次咱們使用 RsJS 中的 BehaviorSubject 來處理此類問題。

在 React 中,可使用 MobX 中的 @computed 來達成相同的效果,並且此 api 會更方便一些。

依賴注入

依賴注入有必定的爭議性,由於它與當前 React 推行的函數式編程/數據不可變性理念背道而馳。事實證實,某種程度的依賴注入是數據綁定環境中必不可少的部分,由於它能夠幫助沒有獨立數據層的結構解耦(這樣作更便於使用模擬數據和測試)。

另外一項依賴注入(Angular 中已支持)的優勢是能夠在(app)不一樣的生命週期中保有不一樣的數據倉庫(store)。目前大部分 React 範例使用了映射到不一樣組件的全局狀態(global app state)。可是依個人經驗來看,當組件卸載(unmount)的時候清理全局狀態很容易產生 bug。

在組件加載(mount)的時候建立一個獨立的數據倉庫(同時能夠無縫傳遞給此組件的子組件)很是方便,並且是一項很容易被忽略的概念。

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

路由

組件依賴的路由容許組件管理自身的子路由,而不是配置一個大的全局路由。這種方案終於在 react-router 4 裏實現了。

Material Design

使用高級組件(higher-level components)老是很棒的,而 material design 已經成爲即使是在非谷歌的項目中也被普遍接受的選擇。

我特地選擇了 React Toolbox 而不是一般推薦的 Material UI,由於 Material UI 有一系列公開認可的行內 css 性能問題,而它的開發者們計劃在下個版本解決這些問題。

此外,React Toolbox 中已經開始使用即將取代 Sass/LESS 的 PostCSS/cssnext

帶有做用域的 CSS

CSS 的類比較像是全局變量一類的東西。有許多方法來組織 CSS 以免互相起衝突(包括 BEM),可是當前的趨勢是使用庫輔助處理 CSS 以免衝突,而不是須要前端開發者煞費苦心的設計精密的 CSS 命名系統。

表單校驗

表單校驗是很是重要並且使用普遍的特性,使用相關的庫能夠有效避免冗餘代碼和 bug。

程序生成器(Project Generator,也就是命令行工具)

使用一個命令行工具來建立項目比從 Github 上下載樣板文件要方便的多。

分別使用 React 與 Angular 實現同一個 app

那麼咱們準備使用 React 和 Anuglar 編寫同一個 app。這個 app 並不複雜,只是一個能夠供任何人發佈帖子的公共貼吧(Shoutboard)。

你能夠在這裏體驗到這個 app:

若是想閱讀本項目的完整源代碼,能夠從以下地址下載:

你瞧,咱們一樣使用 TypeScript 編寫 React app,由於可以使用類型檢查的優點仍是很讚的。做爲一種處理引入更優秀的方式,async/await 以及 rest spread 現在終於能夠在 TypeScript2 裏使用,這樣就不須要 Babel/ES7/Flow 了(leaves Babel/ES7/Flow in the dust)。

薛定諤的貓:babel 的擴展很強大的。ts 不支持的 babel 均可以經過插件支持(stage0~stage4)。

一樣,咱們爲二者添加了 Apollo Client,由於我但願使用 GraphQL 風格的接口。個人意思是,REST 風格的接口確實不錯,可是通過十幾年的發展後,它已經跟不上時代了。

啓動與路由

首先,讓咱們看一下二者的入口文件:

Angular

// 路由配置
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
  ],
  // 與整個 app 生命週期關聯的服務(service)
  providers: [
    AppService
  ],
  // 啓動時最早訪問的組件
  bootstrap: [AppComponent]
})

@Injectable()
export class AppService {
  username = 'Mr. User'
}複製代碼

基本上,但願使用的組件要寫在 declarations 中,須要引入的第三方庫要寫在 imports 中,但願注入的全局性數據倉庫(global store)要寫在 providers 中。子組件能夠訪問到已聲明的變量,並且有機會能夠添加一些本身的東西。

React

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 中被用來依賴注入。它將數據倉庫保存在上下文(context)中,這樣 React 組件能夠稍後進行注入。是的,React 上下文能夠(大概)保證使用的安全性

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

React 版本的入口文件相對要簡短一些,由於不須要作那麼多模塊聲明 —— 一般的狀況下,只要導入就可使用了。有時候這種硬依賴很麻煩(好比測試的時候),因此對於全局單例來講,我只好使用老式的(decades-old) GoF 模式

Angular 的路由是已注入的,因此能夠在程序的任何地方使用,並不只僅是組件中。爲了在 React 中達到相同的功能,咱們使用
mobx-react-router 並注入routerStore

總結:兩個 app 的啓動文件都很是直觀。React 看起來更簡單一點的,使用 import 代替了模塊的加載。不過接下來咱們會看到,雖然在入口文件中加載模塊有點囉嗦,可是以後使用起來會很便利;而手動建立一個單例也有本身的麻煩。至於路由建立時的語法問題,是 JSON 更好仍是 JSX 更好只是單純的我的喜愛。

如今有兩種方法來進行頁面跳轉。聲明式的方法,使用超連接 <a href...> 標籤;命令式的方法,直接調用 routing (以及 location)API。

Angular

<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 CSS 樣式,方便在頁面中凸顯。

router 使用特殊的 <router-outlet> 標籤來渲染當前路徑對應的視圖(無論是哪一種)。當 app 的子組件嵌套的比較深的時候,即可以使用不少 <router-outlet> 標籤。

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

路由模塊能夠注入進任何服務(一半是由於 TypeScript 是強類型語言的功勞),private 的聲明修飾能夠將路由存儲在組件的實例上,不須要再顯式聲明。使用 navigate 方法即可以切換路徑。

React

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 樣式。

然而,咱們不能直接使用 CSS 樣式的名稱,由於通過 CSS 模塊編譯後(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 的組合更簡單。

依賴注入

事實證實,將數據層與展現層分離開是很是有必要的。咱們但願經過依賴注入讓數據邏輯層的組件(這裏的叫法是 model/store/service)關聯上表示層組件的生命週期,這樣就能夠創造一個或多個的數據層組件實例,不須要干擾全局狀態。同時,這麼作更容易兼容不一樣的數據與可視化層。

這篇文章的例子很是簡單,全部的依賴注入的東西看起來彷佛有點多此一舉。可是隨着 app 業務的增長,這種作法會很方便的。

Angular

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

任何類(class)都可以使用 @injectable 的裝飾器進行修飾,這樣它的屬性與方法即可以在其餘組件中調用。

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  providers: [
    HomeService // 註冊在這裏
  ]
})

export class HomeComponent {
  constructor(
    public homeService: HomeService,
    public appService: AppService,
  ) { }
}複製代碼

經過將 HomeService 註冊進組件的 providers,此組件得到了一個獨有的 HomeService。它不是單例,可是每個組件在初始化的時候都會收到一個新的 HomeService 實例化對象。這意味着不會有以前 HomeService 使用過的過時數據。

相對而言,AppService 被註冊進了 app.module 文件(參見以前的入口文件),因此它是駐留在每個組件中的單例,貫穿整個 app 的生命週期。可以從組件中控制服務的聲明週期是一項很是有用、並且常被低估的概念。

依賴注入經過在 TypeScript 類型定義的組件構造函數(constructor)內分配服務(service)的實例來起做用(譯者:也就是上面代碼中的 public homeService: HomeService)。此外,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-way data binding)。可是其本質上(under the hood)是屬性綁定 + 事件驅動。就像(與組件關聯後)服務的生命週期所規定的那樣,homeService.counter 每次離開 /home 頁面的時候都會重置,可是 appService.username 會保留,並且能夠在任何頁面訪問到。

React

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 例子多作一點工做。咱們用 Provider 來包裹 HomeComponent ,這樣在每次加載的時候都得到一個新的 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 修飾的組件調用其渲染函數(render function)時,這些屬性的 getter 方法也會被調用,getter 方法會將對屬性的引用保存在調用它們的組件上。

而後,當 setter 方法被調用、這些屬性的值也改變的時候,上一次渲染這些屬性的組件會(再次)調用其渲染函數。這樣被改變過的屬性會在界面上更新,而後整個週期會從新開始(譯者注:其實就是典型的觀察者模式啊...)。

這是一個很是簡單的機制,也是很棒的特性。更深刻的解釋在這裏.

@inject 裝飾器用來將 appStorehomeStore 的實例注入進 HomeComponent 的屬性。這種狀況下,每個數據倉庫(也)具備不一樣的生命週期。appStore 的生命週期一樣也貫穿整個 app,而 homeStore 在每次進入 "/home" 頁面的時候從新建立。

這麼作的好處,是不須要手動清理屬性。若是全部的數據倉庫都是全局變量,每次詳情頁想展現不一樣的數據就會很崩潰(譯者:由於每次都要手動擦掉上一次的遺留數據)。

總結:由於自帶管理生命週期的特性,Angular 的依賴注入更容易得到預期的效果。React 版本的作法也頗有效,可是會涉及到更多的引用。

計算屬性

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

爲了在 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
    // 手動訂閱 couterMessage 依賴的方法
    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 處理計算屬性的話反而會比 React 例子要麻煩一點,並且程序員必須手動去訂閱(就像在構造函數中作的那樣)。

<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 的管道(pipe)來引用 RxJS 項目。這是一個很棒的作法,比在組件中訂閱要簡短一些。用戶姓名與輸入框則經過 [(ngModel)] 實現了雙向綁定。儘管看起來很奇怪,但這麼作實際上至關優雅。就像一個數據綁定到 appService.username 的語法糖,並且自動相應用戶的輸入事件。

總結:計算屬性在 React/MobX 比在 Angular/RxJ 中更容易實現,可是 RxJS 能夠提供一些有用的函數式響應編程(FRP)的、不久以後會被人們所稱讚的新特性。

模板與 CSS

爲了演示二者的模版棧是多麼的相愛相殺(against each other),咱們來編寫一個展現帖子列表的組件。

Angular

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css'],
  providers: [
    PostsService
  ]
})

export class PostsComponent implements OnInit {
  // 譯者:請注意這裏的 implements OnInit
  // 這是 Angular 4 爲了實現控制組件生命週期而提供的鉤子(hook)接口
  constructor(
    public postsService: PostsService,
    public appService: AppService
  ) { }

  // 這裏是對 OnInit 的具體實現,必須寫成 ngOnInit
  // ngOnInit 方法在組件初始化的時候會被調用
  // 以達到和 React 中 componentWillMount 相同的做用
  // Angular 4 還提供了不少用於控制生命週期鉤子
  // 結果譯者都沒記住(捂臉跑)
  ngOnInit() {
    this.postsService.initializePosts()
  }
}複製代碼

本組件(指 post.component.ts 文件)鏈接了此組件(指具體的帖子組件)的 HTML、CSS,並且在組件初始化的時候經過注入過的服務從 API 讀取帖子的數據。AppService 是一個定義在 app 入口文件中的單例,而 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 的 import 裏(參見上面的入口文件)。*ngFor 指令用來循環使用 md-card 輸出每個帖子。

Local CSS:

.mat-card {
  margin-bottom: 1rem;
}複製代碼

這段局部 CSS 只在 md-card 組件中起做用

Global CSS:

.float-right {
  float: right;
}複製代碼

這段 CSS 類定義在全局樣式文件 style.css 中,這樣全部的組件均可以用標準的方法使用它(指 style.css 文件)的樣式,class="float-right"。

Compiled CSS:

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

在編譯後的 CSS 文件中,咱們能夠發現局部 CSS 的做用域經過添加 [_ngcontent-c1] 的屬性選擇器被限定在本組件中。每個已渲染的 Angular 組件都會產生一個用做肯定 CSS 做用域的類。

這種機制的優點是咱們能夠正常的引用 CSS 樣式,而 CSS 的做用域在後臺被處理了(is handled 「under the hood」)。

React

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 的 依賴「短暫(transient)」。咱們一樣引入 CSS 樣式,聲明爲 style 以及 appStyle ,這樣就能夠在 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 最多的框架,可是實際上 JSX 語法纔是 TypeScript 能真正發揮做用的地方。經過添加 CSS 模塊(在頂部引入),它可以讓模版編碼的工做成爲依靠插件進行代碼補全的享受(it really turns your template coding into code completion zen)。每個事情都是通過類型檢驗的。組件、屬性甚至 CSS 類(appStyle.floatRight 以及 style.messageCard 見下)。固然,JSX 語法的單薄特性比起 Angular 的模版更鼓勵將代碼拆分紅組件和片斷(fragment)。

Local CSS:

.messageCard {
  margin-bottom: 1rem;
}複製代碼

Global CSS:

.floatRight {
  float: right;
}複製代碼

Compiled CSS:

.floatRight__qItBM {
  float: right;
}

.messageCard__1Dt_9 {
    margin-bottom: 1rem;
}複製代碼

如你所見,CSS 模塊加載器經過在每個 CSS 類以後添加隨機的後綴來保證其名字獨一無二。這是一種很是簡單的、能夠有效避免命名衝突的辦法。(編譯好的)CSS 類隨後會被 webpack 打包好的對象引用。這麼作的缺點之一是不能像 Angular 那樣只建立一個 CSS 文件來使用。可是從另外一方面來講,這也何嘗不是一件好事。由於這種機制會強迫你正確的封裝 CSS 樣式。

總結:比起 Angular 的模版,我更喜歡 JSX 語法,尤爲是支持代碼補全以及類型檢查。這真是一項殺手鐗(really is a killer feature)。Angular 如今採用了 AOT 編譯器,也有一些新的東西。大約有一半的狀況能使用代碼補全,可是不如 JSX/TypeScript 中作的那麼完善。

GraphQL — 加載數據

那麼咱們決定使用 GraphQL 來保存本 app 的數據。在服務端建立 GraphQL 風格的接口的簡單方法之一就是使用後端即時服務(Baas),好比說 Graphcool。其實,咱們就是這麼作的。基本上,開發者只須要定義數據模型和屬性,隨後就能夠方便的進行增刪改查了。

通用代碼

由於不少 GraphQL 相關的代碼實現起來徹底相同,那麼咱們沒必要重複編寫兩次:

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

比起傳統的 REST 風格的接口,GraphQL 是一種爲了提供函數性富集合的查詢語言。讓咱們分析一下這個特定的查詢。

  • PostsQuery 只是該查詢被隨後引用的名稱,能夠任意起名。

  • allPosts 是最重要的部分:它是查詢全部帖子數據函數的引用。這是 Graphcool 建立的名字。

  • orderByfirst 是 allPost 的參數,createdAt 是帖子數據模型的一個屬性。first: 5 意思是返回查詢結果的前 5 條數據。

  • idnametitle、以及 message 是咱們但願在返回的結果中包含帖子的數據屬性,其餘的屬性會被過濾掉。

你瞧,這真的太棒了。仔細閱讀這個頁面的內容來熟悉更多有關 GraphQL 查詢的東西。

interface Post {
  id: string
  name: string
  title: string
  message: string
}

interface PostsQueryResult {
  allPosts: Array<Post>
}複製代碼

而後,做爲 TypeScript 的模範市民,咱們經過建立接口來處理 GraphQL 的結果。

Angular

@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),該結果集可供咱們訂閱。它有點像 Promise,但並非徹底同樣,因此咱們不能使用 async/await。固然,確實有 toPromise 方法(將其轉化爲 Promise 對象),可是這種作法並非 Angular 的風格(譯者:那爲啥 Angular 4 的入門 demo 用的就是 toPromise...)。咱們經過設置 fetchPolicy: 'network-only' 來保證在這種狀況不進行緩存操做,而是每次都從服務端獲取最新數據。

React

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 語法的優勢了(譯者:async/await 語法的優勢即是用寫同步代碼的模式處理異步狀況,沒必要在使用 Promose 的 then 回調,邏輯更清晰,也更容易 debug)。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
    }
  }
`複製代碼

修改(mutations,GraphQL 術語)的目的是爲了建立或者更新數據。在修改中聲明一些變量是十分有益的,由於這實際上是傳遞數據的方式。咱們有 nametitle、以及 message 這些變量,類型爲字符串,每次調用本修改的時候都會爲其賦值。createPost 函數,又一次是由 Graphcool 來定義的。咱們指定 Post 數據模型的屬性會從修改(mutation)對應的屬性裏得到屬性值,並且但願每建立一條新數據的時候都會返回一個新的 id。

Angular

@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 方法的時候,咱們會傳入一個但願的修改(mutation)以及修改中所包含的變量值。而後在訂閱的回調函數中得到返回結果,使用注入的路由來跳轉帖子列表頁面。

React

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')
  }
}複製代碼

和上面 Angular 的作法很是類似,差異就是有更多的「手動」依賴注入,更多的 async/await 的作法。

總結:又一次,並無太多不一樣。訂閱與 async/await 基本上就那麼點差別。

表單:

咱們但願在 app 中用表單達到如下目標:

  • 將表單做用域綁定至數據模型

  • 爲每一個表單域進行校驗,有多條校驗規則

  • 支持檢查整個表格的值是否合法

React

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 的庫是這麼工做的:對於每個表單域,須要定義一個 FieldStateFieldState 的參數是表單域的初始值。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 實例擁有 valueonChange以及 error 三個屬性,能夠很是方便的在前端組件中使用。

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

form.hasError 的返回值是 true 的時候,咱們讓按鈕控件保持禁用狀態。提交按鈕發送表單數據到以前編寫的 GraphQL 修改(mutation)上。

Angular

在 Angular 中,咱們會使用 @angular/formspackage 中的 FormServiceFormBuilder

@angular/formspackage.

@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'
    }
  }複製代碼

首先,讓咱們定義校驗信息。

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)]
  ],
})
}複製代碼

使用 FormBuilder,很容易建立表格結構,甚至比 React 的例子更出色。

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
  }複製代碼

爲了讓綁定的校驗信息在正確的位置顯示,咱們須要作一些處理。這段代碼源自官方文檔,只作了一些微小的變化。基本上,在 FormService 中,表單域保有根據校驗名識別的錯誤,這樣咱們就須要手動配對信息與受影響的表單域。這並非一個徹底的缺陷,而是更容易國際化(譯者:即指的方便的對提示語進行多語言翻譯)。

onSubmit({ value, valid }) {
    if (!valid) {
      return
    }
    this.formService.addPost(value)
  }

  onCancel() {
    this.router.navigate(['/posts'])
  }
}複製代碼

和 React 同樣,若是表單數據是正確的,那麼數據能夠被提交到 GraphQL 的修改。

<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>複製代碼

最重要的是引用咱們經過 FormBuilder 建立的表單組,也就是 [formGroup]="postForm" 分配的數據。表單中的表單域經過 formControlName 的屬性來限定表單的數據。固然,還得在表單數據驗證失敗的時候禁用 「Submit」 按鈕。順便還須要添加髒數據檢查,由於這種狀況下,髒數據可能會引發表單校驗不經過。咱們但願每次初始化 button 都是可用的。

總結:對於 React 以及 Angular 的表單方面來講,表單校驗和前端模版差異都很大。Angular 的方法是使用一些更「魔幻」的作法而不是簡單的綁定,可是從另外一方面說,這麼作的更完整也更完全。

編譯文件大小

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.

啊,還有一件事。那就是使用程序默認設置進行打包後 bundle 文件的大小:特指 React 中的 Tree Shaking 以及 Angular 中的 AOT 編譯。

  • Angular: 1200 KB
  • React: 300 KB

嗯,並不意外,Angular 確實是個巨無霸。

使用 gzip 進行壓縮的後,二者的大小分別會下降至 275kb 和 127kb。

請記住,這還只是主要的庫。相比較而言真正處理邏輯的代碼是很小的部分。在真實的狀況下,這部分的比率大概是 1:2 到 1:4 之間。同時,當開發者開始在 React 中引入一堆第三方庫的時候,文件的體積也會隨之快速增加。

庫的靈活性與框架的穩定性

那麼,看起來咱們仍是沒法(再一次)對 「Angular 與 React 中何者纔是更好的前端開發框架」給出明確的答案。

事實證實,React 與 Angular 中的開發工做流程能夠很是類似(譯者:由於用的是 mobx 而不是 redux),而這其實和使用 React 的哪個庫有關。固然,這仍是一個我的喜愛問題。

若是你喜歡現成的技術棧,牛逼的依賴注入並且計劃體驗 RxJS 的好處,那麼選擇 Angular 吧。

若是你喜歡自由定製本身的技術棧,喜歡 JSX 的直觀,更喜歡簡單的計算屬性,那麼就用 React/MobX 吧。

固然,你能夠從這裏以及這裏得到本文 app 的全部源代碼。

或者,若是你喜歡大一點的真實項目:

先選擇本身的編程習慣

使用 React/MobX 實際上比起 React/Redux 更接近於 Angular。雖然在模版以及依賴管理中有一些顯著的差別,可是它們有着類似的可變/數據綁定的風格。

React/Redux 與它的不可變/單向數據流的模式則是徹底不一樣的另外一種東西。

不要被 Redux 庫的體積迷惑,它也許很嬌小,但確實是一個框架。現在大部分 Redux 的優秀作法關注使用兼容 Redux 的庫,好比用來處理異步代碼以及獲取數據的 Redux Saga,用來管理表單的 Redux Form,用來記錄選擇器(Redux 計算後的值)的Reselect,以及用來管理組件生命週期的 Recompose。同時 Redux 社區也在從 Immutable.js 轉向 lodash/fp,更專一於處理普通的 JS 對象而不是轉化它們。

React Boilerplate是一個很是著名的使用 Redux 的例子。這是一個強大的開發棧,可是若是你仔細研究的話,會發現它與到目前爲止本文提到的東西很是、很是不同。

我以爲主流 JavaScript 社區一直對 Angular 抱有某種程度的偏見(譯者:我也有這種感受,做爲全公司惟一會 Angular 的稀有動物每次想在組內推廣 Angular 都會遇到無窮大的阻力)。大部分對 Angular 表達不滿的人也許還沒法欣賞到 Angular 中老版本與新版本之間的巨大改變。以個人觀點來看,這是一個很是整潔高效的框架,若是早一兩年出現確定會在世界範圍內掀起一陣 Angular 的風潮(譯者:惋惜早一兩年出的是 Angular 1.x)。

固然,Angular 仍是得到了一個堅實的立足點。尤爲是在大型企業中,大型團隊須要標準化和長期化的支持。換句話說,Angular 是谷歌工程師們認爲前端開發應有的樣子,若是它終究能有所成就的話(amounts to anything)。

對於 MobX 來講,處境也差很少。十分優秀,可是受衆很少。

結論是:在選擇 React 與 Angular 以前,先選擇本身的編程習慣(譯者:這結論等於沒結論)。

是可變的/數據綁定,仍是不可變的/單向數據流?看起來真的很難抉擇。

> 我但願你能喜歡這篇客座文章。這篇文章最初發表在 Toptal,而且已經得到轉載受權。

❤ 若是你喜歡這篇文章,輕輕扎一下小藍心吧老鐵


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索