天天看點

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

<b>本文講的是[譯]Angular vs React:誰更适合前端開發,</b>

<b></b>

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

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

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

怎麼樣?

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

兩者最大的差别是對狀态(state)的管理。Angular 通過資料綁定(data-binding)來将狀态綁在資料上,而 React 如今通常引入 Redux 來提供單向資料流、處理不可變的資料(譯者:我個人了解這句話的意思是 Angular 的資料和狀态是互相影響的,而 React 隻能通過切換不同的狀态來顯示不同的資料)。這是剛好互相對立的解決問題方法,而開發者們則不停的争論<code>可變的/資料綁定模式</code>與<code>不可變的/單向的資料流</code>兩者間誰更優秀。

既然 React 更容易了解,為了便于比較,我決定編寫一份 React 與 Angular 的對應表,來合理的并排比較兩者的代碼結構。

Angular 中有但是 React 沒有預設自帶的特性有:

特性 — Angular 包 — React 庫

“除存儲屬性外,類、結構體和枚舉可以定義計算屬性,計算屬性不直接存儲值,而是提供一個 getter 來擷取值,一個可選的 setter 來間接設定其他屬性或變量的值。” 摘錄來自: Unknown. “The Swift Programming Language 中文版”。 iBooks.

依賴注入有一定的争議性,因為它與目前 React 推行的<code>函數式程式設計/資料不可變性理念</code>背道而馳。事實證明,某種程度的依賴注入是資料綁定環境中必不可少的部分,因為它可以幫助沒有獨立資料層的結構解耦(這樣做更便于使用模拟資料和測試)。

另一項依賴注入(Angular 中已支援)的優點是可以在(app)不同的生命周期中保有不同的資料倉庫(store)。目前大部分 React 範例使用了映射到不同元件的全局狀态(global app state)。但是依我的經驗來看,當元件解除安裝(unmount)的時候清理全局狀态很容易産生 bug。

在元件加載(mount)的時候建立一個獨立的資料倉庫(同時可以無縫傳遞給此元件的子元件)非常友善,而且是一項很容易被忽略的概念。

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

元件依賴的路由允許元件管理自身的子路由,而不是配置一個大的全局路由。這種方案終于在 <code>react-router</code> 4 裡實作了。

使用進階元件(higher-level components)總是很棒的,而 material design 已經成為即便是在非谷歌的項目中也被廣泛接受的選擇。

表單校驗是非常重要而且使用廣泛的特性,使用相關的庫可以有效避免備援代碼和 bug。

使用一個指令行工具來建立項目比從 Github 上下載下傳樣闆檔案要友善的多。

那麼我們準備使用 React 和 Anuglar 編寫同一個 app。這個 app 并不複雜,隻是一個可以供任何人釋出文章的公共貼吧(Shoutboard)。

你可以在這裡體驗到這個 app:

<a href="https://link.juejin.im/?target=http%3A%2F%2Fshoutboard-angular.herokuapp.com%2F" target="_blank">使用 Angular 編寫的貼吧</a>

<a href="https://link.juejin.im/?target=https%3A%2F%2Fshoutboard-react.herokuapp.com%2F" target="_blank">使用 React 編寫的貼吧</a>

如果想閱讀本項目的完整源代碼,可以從如下位址下載下傳:

<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Ftomaash%2Fshoutboard-angular" target="_blank">貼吧源碼 Angular 版</a>

<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Ftomaash%2Fshoutboard-react" target="_blank">貼吧源碼 React 版</a>

薛定谔的貓:babel 的擴充很強大的。ts 不支援的 babel 都可以通過插件支援(stage0~stage4)。

首先,讓我們看一下兩者的入口檔案:

基本上,希望使用的元件要寫在 <code>declarations</code> 中,需要引入的第三方庫要寫在 <code>imports</code>中,希望注入的全局性資料倉庫(global store)要寫在 <code>providers</code> 中。子元件可以通路到已聲明的變量,而且有機會可以添加一些自己的東西。

Angular 的路由是已注入的,是以可以在程式的任何地方使用,并不僅僅是元件中。為了在 React 中達到相同的功能,我們使用

總結:兩個 app 的啟動檔案都非常直覺。React 看起來更簡單一點的,使用 import 代替了子產品的加載。不過接下來我們會看到,雖然在入口檔案中加載子產品有點啰嗦,但是之後使用起來會很便利;而手動建立一個單例也有自己的麻煩。至于路由建立時的文法問題,是 JSON 更好還是 JSX 更好隻是單純的個人喜好。

現在有兩種方法來進行頁面跳轉。聲明式的方法,使用超連結 <code>&lt;a href...&gt;</code> 标簽;指令式的方法,直接調用 routing (以及 location)API。

Angular Router 自動檢測處于目前頁面的 <code>routerLink</code>,為其加載适當的 <code>routerLinkActive</code>CSS 樣式,友善在頁面中凸顯。

router 使用特殊的 <code>&lt;router-outlet&gt;</code> 标簽來渲染目前路徑對應的視圖(不管是哪種)。當 app 的子元件嵌套的比較深的時候,便可以使用很多 <code>&lt;router-outlet&gt;</code> 标簽。

路由子產品可以注入進任何服務(一半是因為 TypeScript 是強類型語言的功勞),<code>private</code> 的聲明修飾可以将路由存儲在元件的執行個體上,不需要再顯式聲明。使用 <code>navigate</code> 方法便可以切換路徑。

React Router 也可以通過 <code>activeClassName</code> 來設定目前連接配接的 CSS 樣式。

然而,我們不能直接使用 CSS 樣式的名稱,因為經過 CSS 子產品編譯後(CSS 樣式的名字)會變得獨一無二,是以必須使用 <code>style</code> 來進行輔助。稍後會詳細解釋。

如上面所見,React Router 在 <code>&lt;App&gt;</code> 标簽内使用 <code>&lt;Switch&gt;</code> 标簽。因為 <code>&lt;Switch&gt;</code> 标簽隻是包裹并加載目前路由,這意味着目前元件的子路由就是 <code>this.props.children</code>。當然這些子元件也是這麼組成的。

<code>mobx-router-store</code> 也允許簡單的注入以及導航。

總結:兩種方案都相當類似。Angular 看起來更直覺,React 的組合更簡單。

事實證明,将資料層與展示層分離開是非常有必要的。我們希望通過依賴注入讓資料邏輯層的元件(這裡的叫法是 model/store/service)關聯上表示層元件的生命周期,這樣就可以創造一個或多個的資料層元件執行個體,不需要幹擾全局狀态。同時,這麼做更容易相容不同的資料與可視化層。

這篇文章的例子非常簡單,所有的依賴注入的東西看起來似乎有點畫蛇添足。但是随着 app 業務的增加,這種做法會很友善的。

任何類(class)均可以使用 <code>@injectable</code> 的裝飾器進行修飾,這樣它的屬性與方法便可以在其他元件中調用。

通過将 <code>HomeService</code> 注冊進元件的 <code>providers</code>,此元件獲得了一個獨有的 <code>HomeService</code>。它不是單例,但是每一個元件在初始化的時候都會收到一個新的 <code>HomeService</code> 執行個體化對象。這意味着不會有之前 <code>HomeService</code> 使用過的過期資料。

相對而言,<code>AppService</code> 被注冊進了 <code>app.module</code> 檔案(參見之前的入口檔案),是以它是駐留在每一個元件中的單例,貫穿整個 app 的生命周期。能夠從元件中控制服務的聲明周期是一項非常有用、而且常被低估的概念。

依賴注入通過在 TypeScript 類型定義的元件構造函數(constructor)内配置設定服務(service)的執行個體來起作用(譯者:也就是上面代碼中的 <code>public homeService: HomeService</code>)。此外,<code>public</code> 的關鍵詞修飾的參數會自動指派給 <code>this</code> 的同名變量,這樣我們就不必再編寫那些無聊的 <code>this.homeService = homeService</code> 代碼了。

Angular 的模闆文法被證明相當優雅(譯者:其實這也算是個人偏好問題),我喜歡 <code>[()]</code> 的縮寫,這樣就代表雙向綁定(2-way data binding)。但是其本質上(under the hood)是屬性綁定 + 事件驅動。就像(與元件關聯後)服務的生命周期所規定的那樣,<code>homeService.counter</code> 每次離開 <code>/home</code> 頁面的時候都會重置,但是 <code>appService.username</code>會保留,而且可以在任何頁面通路到。

如果希望通過 MobX 實作同樣的效果,我們需要在任何需要監聽其變化的屬性上添加<code>@observable</code> 裝飾器。

為了正确的控制(資料層的)生命周期,開發者必須比 Angular 例子多做一點工作。我們用<code>Provider</code> 來包裹 <code>HomeComponent</code> ,這樣在每次加載的時候都獲得一個新的 <code>HomeStore</code> 執行個體。

<code>HomeComponent</code> 使用 <code>@observer</code> 裝飾器監聽被 <code>@observable</code> 裝飾器修飾的屬性變化。

其底層機制很有趣,是以我們簡單的介紹一下。<code>@observable</code> 裝飾器通過替換對象中(被觀察)屬性的 getter 和 setter 方法,攔截對該屬性的調用。當被 <code>@observer</code> 修飾的元件調用其渲染函數(render function)時,這些屬性的 getter 方法也會被調用,getter 方法會将對屬性的引用儲存在調用它們的元件上。

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

<code>@inject</code> 裝飾器用來将 <code>appStore</code> 和 <code>homeStore</code> 的執行個體注入進 <code>HomeComponent</code> 的屬性。這種情況下,每一個資料倉庫(也)具有不同的生命周期。<code>appStore</code> 的生命周期同樣也貫穿整個 app,而 <code>homeStore</code> 在每次進入 "/home" 頁面的時候重新建立。

這麼做的好處,是不需要手動清理屬性。如果所有的資料倉庫都是全局變量,每次詳情頁想展示不同的資料就會很崩潰(譯者:因為每次都要手動擦掉上一次的遺留資料)。

總結:因為自帶管理生命周期的特性,Angular 的依賴注入更容易獲得預期的效果。React 版本的做法也很有效,但是會涉及到更多的引用。

這次我們先講 React,它的做法更直覺一些。

這樣我們就将計算屬性綁定到 <code>counter</code> 上,同時傳回一段根據點選數量來确定的資訊。<code>counterMessage</code> 被放在緩存中,隻有當 <code>counter</code> 屬性被改變的時候才重新進行處理。

然後我們在 JSX 模版中引用此屬性(以及 <code>increment</code> 方法)。再将使用者的姓名資料綁定在輸入框上,通過 <code>appStore</code> 的一個方法處理使用者的(輸入)事件。

為了在 Angular 中實作相同的結果,我們必須另辟蹊徑。

我們需要初始化所有計算屬性的值,也就是所謂的 <code>BehaviorSubject</code>。計算屬性自身同樣也是<code>BehaviorSubject</code> ,因為每次計算後屬性都是另一個計算屬性的基礎。

注意,我們可以通過 <code>| async</code> 的管道(pipe)來引用 RxJS 項目。這是一個很棒的做法,比在元件中訂閱要簡短一些。使用者姓名與輸入框則通過 <code>[(ngModel)]</code> 實作了雙向綁定。盡管看起來很奇怪,但這麼做實際上相當優雅。就像一個資料綁定到 <code>appService.username</code> 的文法糖,而且自動相應使用者的輸入事件。

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

為了示範兩者的模版棧是多麼的相愛相殺(against each other),我們來編寫一個展示文章清單的元件。

本元件(指 post.component.ts 檔案)連接配接了此元件(指具體的文章元件)的 HTML、CSS,而且在元件初始化的時候通過注入過的服務從 API 讀取文章的資料。AppService 是一個定義在 app 入口檔案中的單例,而 PostsService 則是暫時的、每次建立元件時都會重新初始化的一個執行個體(譯者:又是不同生命周期的不同資料倉庫)。CSS 被引用到元件内,以便于将作用域限定在本元件内 —— 這意味着它不會影響元件外的東西。

在 HTML 模版中,我們從 Angular Material 引用了大部分元件。為了保證其正常使用,必須把它們包含在 app.module 的 import 裡(參見上面的入口檔案)。*ngFor 指令用來循環使用 md-card 輸出每一個文章。

Local CSS:

這段局部 CSS 隻在 <code>md-card</code> 元件中起作用

Global CSS:

這段 CSS 類定義在全局樣式檔案 <code>style.css</code> 中,這樣所有的元件都可以用标準的方法使用它(指 style.css 檔案)的樣式,class="float-right"。

Compiled CSS:

在編譯後的 CSS 檔案中,我們可以發現局部 CSS 的作用域通過添加 <code>[_ngcontent-c1]</code> 的屬性選擇器被限定在本元件中。每一個已渲染的 Angular 元件都會産生一個用作确定 CSS 作用域的類。

這種機制的優勢是我們可以正常的引用 CSS 樣式,而 CSS 的作用域在背景被處理了(is handled “under the hood”)。

在 React 中,開發者又一次需要使用 Provider 來使 PostsStore 的 依賴“短暫(transient)”。我們同樣引入 CSS 樣式,聲明為 <code>style</code> 以及 <code>appStyle</code> ,這樣就可以在 JSX 文法中使用 CSS 的樣式了。

當然,JSX 的文法比 Angular 的 HTML 模版更有 javascript 的風格,是好是壞取決于開發者的喜好。我們使用高階函數 <code>map</code> 來代替 *ngFor 指令循環輸出文章。

如今,Angular 也許是使用 TypeScript 最多的架構,但是實際上 JSX 文法才是 TypeScript 能真正發揮作用的地方。通過添加 CSS 子產品(在頂部引入),它能夠讓模版編碼的工作成為依靠插件進行代碼補全的享受(it really turns your template coding into code completion zen)。每一個事情都是經過類型檢驗的。元件、屬性甚至 CSS 類(<code>appStyle.floatRight</code> 以及<code>style.messageCard</code> 見下)。當然,JSX 文法的單薄特性比起 Angular 的模版更鼓勵将代碼拆分成元件和片段(fragment)。

如你所見,CSS 子產品加載器通過在每一個 CSS 類之後添加随機的字尾來保證其名字獨一無二。這是一種非常簡單的、可以有效避免命名沖突的辦法。(編譯好的)CSS 類随後會被 webpack 打包好的對象引用。這麼做的缺點之一是不能像 Angular 那樣隻建立一個 CSS 檔案來使用。但是從另一方面來說,這也未嘗不是一件好事。因為這種機制會強迫你正确的封裝 CSS 樣式。

總結:比起 Angular 的模版,我更喜歡 JSX 文法,尤其是支援代碼補全以及類型檢查。這真是一項殺手锏(really is a killer feature)。Angular 現在采用了 AOT 編譯器,也有一些新的東西。大約有一半的情況能使用代碼補全,但是不如 JSX/TypeScript 中做的那麼完善。

那麼我們決定使用 GraphQL 來儲存本 app 的資料。在服務端建立 GraphQL 風格的接口的簡單方法之一就是使用後端即時服務(Baas),比如說 Graphcool。其實,我們就是這麼做的。基本上,開發者隻需要定義資料模型和屬性,随後就可以友善的進行增删改查了。

因為很多 GraphQL 相關的代碼實作起來完全相同,那麼我們不必重複編寫兩次:

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

<code>PostsQuery</code> 隻是該查詢被随後引用的名稱,可以任意起名。

allPosts 是最重要的部分:它是查詢所有文章資料函數的引用。這是 Graphcool 建立的名字。

<code>orderBy</code> 和 <code>first</code> 是 allPost 的參數,<code>createdAt</code> 是文章資料模型的一個屬性。<code>first: 5</code> 意思是傳回查詢結果的前 5 條資料。

<code>id</code>、<code>name</code>、<code>title</code>、以及 <code>message</code> 是我們希望在傳回的結果中包含<code>文章</code>的資料屬性,其他的屬性會被過濾掉。

然後,作為 TypeScript 的模範市民,我們通過建立接口來處理 GraphQL 的結果。

GraphQL 查詢結果集是一個 RxJS 的被觀察者類(observable),該結果集可供我們訂閱。它有點像 Promise,但并不是完全一樣,是以我們不能使用 async/await。當然,确實有 toPromise 方法(将其轉化為 Promise 對象),但是這種做法并不是 Angular 的風格(譯者:那為啥 Angular 4 的入門 demo 用的就是 toPromise...)。我們通過設定 <code>fetchPolicy: 'network-only'</code> 來保證在這種情況不進行緩存操作,而是每次都從服務端擷取最新資料。

總結:RxJS 中的訂閱以及 async/await 其實有着非常相似的觀念。

同樣的,這是 GraphQL 相關的代碼:

修改(mutations,GraphQL 術語)的目的是為了建立或者更新資料。在修改中聲明一些變量是十分有益的,因為這其實是傳遞資料的方式。我們有 <code>name</code>、<code>title</code>、以及 <code>message</code> 這些變量,類型為字元串,每次調用本修改的時候都會為其指派。<code>createPost</code> 函數,又一次是由 Graphcool 來定義的。我們指定 <code>Post</code> 資料模型的屬性會從修改(mutation)對應的屬性裡獲得屬性值,而且希望每建立一條新資料的時候都會傳回一個新的 id。

當調用 <code>apollo.mutate</code> 方法的時候,我們會傳入一個希望的修改(mutation)以及修改中所包含的變量值。然後在訂閱的回調函數中獲得傳回結果,使用注入的<code>路由</code>來跳轉文章清單頁面。

和上面 Angular 的做法非常相似,差别就是有更多的“手動”依賴注入,更多的 async/await 的做法。

總結:又一次,并沒有太多不同。訂閱與 async/await 基本上就那麼點差異。

我們希望在 app 中用表單達到以下目标:

将表單作用域綁定至資料模型

為每個表單域進行校驗,有多條校驗規則

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

為了對整個表單進行驗證,最好使用另一個 FormState 執行個體來包裹這些字段,然後提供整體有效性的校驗。

<code>FormState</code> 執行個體擁有 <code>value</code>、<code>onChange</code>以及 <code>error</code> 三個屬性,可以非常友善的在前端元件中使用。

當 <code>form.hasError</code> 的傳回值是 <code>true</code> 的時候,我們讓按鈕控件保持禁用狀态。送出按鈕發送表單資料到之前編寫的 GraphQL 修改(mutation)上。

在 Angular 中,我們會使用 @angular/formspackage 中的 <code>FormService</code> 和 <code>FormBuilder</code>。

<code>@angular/forms</code>package.

首先,讓我們定義校驗資訊。

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

為了讓綁定的校驗資訊在正确的位置顯示,我們需要做一些處理。這段代碼源自官方文檔,隻做了一些微小的變化。基本上,在 FormService 中,表單域保有根據校驗名識别的錯誤,這樣我們就需要手動配對資訊與受影響的表單域。這并不是一個完全的缺陷,而是更容易國際化(譯者:即指的友善的對提示語進行多語言翻譯)。

和 React 一樣,如果表單資料是正确的,那麼資料可以被送出到 GraphQL 的修改。

最重要的是引用我們通過 FormBuilder 建立的表單組,也就是 <code>[formGroup]="postForm"</code> 配置設定的資料。表單中的表單域通過 <code>formControlName</code> 的屬性來限定表單的資料。當然,還得在表單資料驗證失敗的時候禁用 “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 吧。

或者,如果你喜歡大一點的真實項目:

<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fgothinkster%2Fangular-realworld-example-app" target="_blank">RealWorld Angular 4+</a>

<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fgothinkster%2Freact-mobx-realworld-example-app" target="_blank">RealWorld React/MobX</a>

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

React/Redux 與它的不可變/單向資料流的模式則是完全不同的另一種東西。

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

當然,Angular 還是獲得了一個堅實的立足點。尤其是在大型企業中,大型團隊需要标準化和長期化的支援。換句話說,Angular 是谷歌工程師們認為前端開發應有的樣子,如果它終究能有所成就的話(amounts to anything)。

對于 MobX 來說,處境也差不多。十分優秀,但是閱聽人不多。

結論是:在選擇 React 與 Angular 之前,先選擇自己的程式設計習慣(譯者:這結論等于沒結論)。

是可變的/資料綁定,還是不可變的/單向資料流?看起來真的很難抉擇。

<b>原文釋出時間為:2017年9月3日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀