[译]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前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索