作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
托马斯·霍拉斯的头像

Tomas Holas

Tomas最初是一个Ruby on Rails爱好者, 但在2010年,他转向了JavaScript,从此更喜欢使用Angular, React, and NodeJS.

Previously At

IBM
Share

有无数的文章在争论React和Angular哪个是更好的web开发选择. 我们还需要一个吗?

我写这篇文章的原因是 the articles published 虽然它们包含了很好的见解,但已经足够深入到一个实际的前端开发人员来决定React或Angular是否适合他们的需求.

在这篇关于React vs . React的文章中. Angular, 你将了解Angular和React是如何用不同的理念来解决类似的前端问题的, 而选择哪一种仅仅是个人偏好的问题. To compare them, 我们将构建相同的应用程序两次, 一次用Angular,然后再用React.

Angular不合时宜的声明

两年前,我写了一篇关于 React Ecosystem. Among other points, 这篇文章认为,Angular已经成为“预先宣布死亡”的受害者.” Back then, 对于那些不想让自己的项目在过时的框架上运行的人来说,在Angular和几乎任何其他框架之间做出选择都是很容易的. Angular 1已经过时了,Angular 2甚至还没有alpha版本.

事后看来,这些担忧或多或少是有道理的. Angular 2发生了巨大的变化,甚至在最终版本发布之前进行了重大重写.

两年后,我们有了Angular 4,并承诺从现在开始相对稳定.

Now what?

Angular vs. 反应:比较苹果和橘子

有人说比较React和Angular就像比较苹果和橘子. React是一个框架还是一个库? React仅仅是一个处理视图的库, 不像Angular框架, 哪个是覆盖范围更广的解决方案.

Of course, most React developers 会给React添加一些库来把它变成一个完整的框架吗. Then again, 这个堆栈的最终工作流通常仍然与Angular有很大的不同, 因此,可比性仍然有限.

最大的区别在于国家管理. Angular自带数据绑定功能, 而今天的React通常被Redux增强,以提供单向数据流和处理不可变数据. 这些方法本身就是对立的, 现在有无数的讨论是关于可变/数据绑定比不可变/单向绑定更好还是更差.

公平竞争的环境

因为React是出了名的容易破解, I’ve decided, 为了进行比较, 构建一个与Angular镜像相当接近的React设置,以便对代码片段进行并行比较.

一些突出的Angular特性在React中是默认不存在的:

FeatureAngular packageReact library
数据绑定、依赖注入(DI)@angular/coreMobX
Computed propertiesrxjsMobX
基于组件的路由@angular/routerReact Router v4
材料设计组件@angular/materialReact Toolbox
CSS的作用域是组件@angular/coreCSS modules
Form validations@angular/formsFormState
Project generator@angular/cliReact Scripts TS

Data Binding

数据绑定可以说比单向方法更容易开始. 当然,也有可能朝着完全相反的方向,使用 Redux or mobx-state-tree with React, and ngrx with Angular. 但这将是另一篇文章的主题.

Computed Properties

就性能而言, Angular中的普通getter在每次渲染时都被调用,所以是毫无疑问的. 这是可以用的 BehaviorSubject from RsJS, which does the job.

在React中,可以使用 @computed 来自MobX,它实现了同样的目标,可以说是更好的API.

Dependency Injection

依赖注入是有争议的,因为它违背了当前React的函数式编程和不变性范式. As it turns out, 在数据绑定环境中,某种依赖注入几乎是必不可少的, 因为它有助于在没有独立数据层体系结构的情况下解耦(从而模拟和测试).

DI的另一个优势(在Angular中得到了支持)是能够为不同的存储提供不同的生命周期. 大多数当前的React范例都使用某种全局应用状态来映射到不同的组件, 但是从我的经验来看, 在组件卸载时清理全局状态很容易引入bug.

拥有一个在组件挂载上创建的存储(并且对该组件的子组件无缝可用)似乎真的很有用, 经常被忽视的概念.

在Angular中是开箱即用的,但在MobX中也很容易复制.

Routing

基于组件的路由允许组件管理自己的子路由,而不是拥有一个大的全局路由器配置. 这种方法终于成功了 react-router in version 4.

Material Design

从一些高级组件开始总是好的, 材料设计已经成为一种普遍接受的默认选择, 即使是在非谷歌项目中.

我特意选择了 React Toolbox 超过了通常推荐的 Material UI,正如Material UI严肃地自我承认的那样 performance problems 他们计划在下一个版本中解决这个问题.

Besides, PostCSS/cssnext React工具箱中使用的Sass/LESS已经开始取代了.

Scoped CSS

CSS类有点像全局变量. 有许多组织CSS以防止冲突的方法(包括 BEM), 但是目前有一个明显的趋势,就是使用库来帮助处理CSS,从而避免这些冲突,而不需要 front-end developer 设计复杂的CSS命名系统.

Form Validation

表单验证是一个非常重要且使用非常广泛的特性. 最好有一个库来涵盖这些内容,以防止代码重复和bug.

Project Generator

拥有一个项目的CLI生成器比从GitHub克隆样板文件要方便一点.

React or Angular? 同一个应用程序,构建两次

所以我们将在React和Angular中创建相同的应用程序. 没什么特别的,只是一个公告板,允许任何人在一个公共页面上发布消息.

你可以在这里试用这些应用程序:

  • Shoutboard Angular*
  • Shoutboard React*

* 注意:在发布后,Heroku停止提供免费托管,并且演示不再可用.

Shoutboard应用程序

如果你想要完整的源代码,你可以从GitHub获得:

你会注意到我们在React应用中也使用了TypeScript. TypeScript中类型检查的优点是显而易见的. And now, 更好的处理进口, async/await和rest spread终于在TypeScript 2中出现了, it leaves Babel/ES7/Flow in the dust.

Also, let’s add 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
  ],
  providers: [
    AppService
  ],
  引导(AppComponent):
})
@Injectable()
导出类AppService {
  username = 'Mr. User'
}

基本上,我们想在应用程序中使用的所有组件都需要转到声明中. 要导入的所有第三方库,以及要提供的所有全局存储. 子组件可以访问所有这些内容,并有机会添加更多本地内容.

React

const appStore = appStore.getInstance()
const routerStore = routerStore.getInstance()
 
const rootStores = {
  appStore,
  routerStore
}
 
ReactDOM.render(
  
    
      
        
          
          
          
          
        
      
    
  ,
  document.getElementById(根)
)

The 组件在MobX中用于依赖注入. 它将存储保存到上下文中,以便React组件稍后可以注入它们. 是的,React上下文可以(有争议)被使用 safely.

React版本要短一些,因为通常没有模块声明, 你只需要导入,就可以使用了. 有时这种硬依赖是不需要的(测试), 对于全局单例存储, 我只好用这个几十年前的 GoF pattern:

导出类AppStore {
  静态实例:AppStore
  静态getInstance() {
    return AppStore.实例|| AppStore.实例= new AppStore()
  }
  @observable用户名= 'Mr. User'
}

Angular的路由器是可注入的,所以它可以在任何地方使用,而不仅仅是在组件中. 为了在反应中达到同样的效果,我们使用 mobx-react-router 包装并注入 routerStore.

Summary: 引导这两个应用程序非常简单. React的优势在于更简单, 只使用导入而不是模块, but, as we’ll see later, 这些模块非常方便. 手动创建单例对象有点麻烦. 至于路由声明语法,JSON vs. JSX只是一个偏好问题.

有两种情况可以改变路线. Declarative, using 元素和命令式,直接调用路由(以及位置)API.

Angular

Shoutboard应用程序

Angular Router会自动检测哪些 routerLink 是主动的,又是放一个合适的 routerLinkActive 类,以便对其进行样式化.

路由器使用特殊的 元素来呈现当前路径所指示的任何内容. 可能有很多 S,当我们深入研究应用程序的子组件时.

@Injectable()
导出类FormService {
  构造函数(私有路由器:路由器){}
  goBack() {
    this.router.导航([' /帖子'])
  }
}

router模块可以被注入到任何服务中(一半是通过它的TypeScript类型) private 声明然后将其存储在实例中,而不需要显式赋值. Use navigate 转换url的方法.

React

导入*作为样式./app.css'
// …
  

Shoutboard应用程序

Home Posts
{this.props.children}

React Router也可以用来设置活动链接的类 activeClassName.

Here, 我们不能直接提供类名, 因为它是唯一的CSS模块编译器, 我们需要使用 style helper. More on that later.

如上所示,React Router使用 element inside an element. As the 元素只是封装并挂载当前路由, 表示当前组件的子路由是公正的 this.props.children. 所以它也是可组合的.

导出类FormStore {
  routerStore: routerStore
  constructor() {
    this.routerStore = routerStore.getInstance()
  }
  goBack = () => {
    this.routerStore.history.push('/posts')
  }
}

The mobx-router-store 包也允许容易的注入和导航.

Summary: 这两种路由方法非常相似. Angular似乎更直观,而 React Router 有更直接的可组合性吗.

Dependency Injection

事实已经证明,将数据层与表示层分离是有益的. 我们在这里试图通过DI实现的是让数据层的组件(这里称为模型/存储/服务)遵循可视化组件的生命周期, 从而允许创建这样的组件的一个或多个实例,而无需触及全局状态. 此外,应该可以混合和匹配兼容的数据层和可视化层.

本文中的示例非常简单, 所以所有的DI看起来都是多余的, 但随着应用程序的增长,它会派上用场.

Angular

@Injectable()
导出类HomeService {
  message = '欢迎来到主页'
  counter = 0
  increment() {
    this.counter++
  }
}

所以任何类都可以制作 @injectable,其属性和方法可供组件使用.

@Component({
  选择器:“app-home”,
  templateUrl: './home.component.html',
  providers: [
    HomeService
  ]
})
导出类HomeComponent {
  constructor(
    public homeService:
    public appService: appService;
  ) { }
}

By registering the HomeService to the component’s providers,我们使它专供这个组件使用. 它现在不是一个单体了, 但是组件的每个实例都会收到一个新的副本, 新安装的组件. 这意味着没有以前使用的陈旧数据.

In contrast, the AppService 已注册到 app.module (see above), 所以它是一个单例,对所有组件都保持不变, 尽管应用程序的生命周期. 能够从组件控制服务的生命周期是非常有用的, 然而被低估的概念.

DI通过将服务实例分配给组件的构造函数来工作, 由TypeScript类型标识. Additionally, the public Keywords自动分配参数给 this,这样我们就不用写那些无聊的东西了 this.homeService = homeService lines anymore.

Dashboard


Clicks since last visit: {{homeService.counter}}

Angular的模板语法,可以说相当优雅. I like the [()] shortcut, 哪一种工作方式类似于双向数据绑定, but under the hood, 它实际上是一个属性绑定+事件. 正如我们服务的生命周期所要求的那样, homeService.counter 会在我们每次离开时重置吗 /home, but the appService.username 停留,并可从任何地方访问.

React

从'mobx'中导入{observable}
 
导出类HomeStore {
  @observable counter = 0
  increment = () => {
    this.counter++
  }
}

在MobX中,我们需要添加 @observable 装饰器变成任何我们想要观察到的属性.

@observer
导出类Home扩展React.Component {
 
  homeStore: HomeStore
  componentWillMount () {
    this.homeStore = new homeStore ()
  }
 
  render() {
    return 
      
    
  }
}

为了正确地管理生命周期,我们需要做比Angular例子中更多的工作. We wrap the HomeComponent inside a Provider的新实例 HomeStore on each mount.

接口HomeComponentProps {
  appStore?: AppStore,
  homeStore?: HomeStore
}
 
@ inject(“应用商店”、“homeStore”)
@observer
导出类HomeComponent扩展React.Component {
  render() {
    const {homeStore, appStore} =这个.props
    return 

Dashboard

Clicks since last visit: {homeStore.counter}
} }

HomeComponent uses the @observer 装饰师要听变化 @observable properties.

它的内部机制非常有趣,所以让我们在这里简要地介绍一下. The @observable 装饰器用getter和setter替换对象中的属性, 这样它就可以窃听电话了. 的渲染函数 @observer 增广组件称为, 这些属性getter被调用, 它们保留了调用它们的组件的引用.

Then, 当调用setter并更改值时, 调用上次渲染时使用该属性的组件的渲染函数. 现在,关于在哪里使用了哪些属性的数据被更新了,整个循环可以重新开始.

一个非常简单的机制,而且性能也很好. 更深入的解释 here.

The @inject 装饰器用于注入 appStore and homeStore instances into HomeComponent’s props. 在这一点上,每个商店都有不同的生命周期. appStore 在应用程序的生命周期中是相同的,但是 homeStore 是新创建的每个导航到“/home”路线.

这样做的好处是,不需要像所有存储都是全局的那样手动清理属性, 如果路由是一些每次包含完全不同数据的“详细”页面,这是一种痛苦.

Summary: 因为提供程序的生命周期管理是Angular的DI的一个固有特性, it is, of course, 在那里更容易实现. React版本也是可用的,但涉及更多的样板文件.

Computed Properties

React

让我们从React开始解决这个问题,它有一个更直接的解决方案.

从“mobx”中导入{observable, computed, action}
 
导出类HomeStore {
从“mobx”中导入{observable, computed, action}
 
导出类HomeStore {
  @observable counter = 0
  increment = () => {
    this.counter++
  }
  @computed get counterMessage() {
    console.日志(“验算counterMessage!')
    return `${this.counter} ${this.counter === 1 ? 'click': 'clicks'}自从上次访问以来'
  }
}

我们有一个计算属性绑定到 counter 并返回一个适当的复数形式的消息. The result of counterMessage 被缓存,并且只有在 counter changes.


{homeStore.counterMessage}

然后,引用属性(and) increment 方法)从JSX模板. 输入字段通过绑定到一个值来驱动,并允许从 appStore 处理用户事件.

Angular

要在Angular中实现同样的效果,我们需要更有创意一点.

从“@angular/core”中导入{Injectable}
从rxjs/BehaviorSubject中导入{BehaviorSubject}
 
@Injectable()
导出类HomeService {
  message = '欢迎来到主页'
  counterSubject = new BehaviorSubject(0)
  //计算属性可以作为其他计算属性的基础
  counterMessage = new BehaviorSubject(")
  constructor() {
    //手动订阅countermessage所依赖的每个主题
    this.counterSubject.subscribe(this.recomputeCounterMessage)
  }
 
  //需要绑定这个
  private recomputeCounterMessage = (x) => {
    console.日志(“验算counterMessage!')
    this.counterMessage.下一步(' ${x} ${x === 1 ? 'click': 'clicks'}自从上次访问以来')
  }
 
  increment() {
    this.counterSubject.next(this.counterSubject.getValue() + 1)
  }
}

我们需要将作为计算属性基础的所有值定义为 BehaviorSubject. 计算属性本身也是一个 BehaviorSubject,因为任何计算属性都可以作为另一个计算属性的输入.

Of course, RxJS can do much more 不仅仅是这个,但这将是另一篇完全不同的文章的主题. 较小的缺点是,这种仅使用RxJS计算属性的琐碎用法比react示例要冗长一些, 您还需要手动管理订阅(就像这里的构造函数一样).


  

{{homeService.counterMessage | async}}

注意我们如何引用 RxJS subject with the | async pipe. 这是一个很好的方法,比需要在组件中订阅要短得多. The input 组件由 [(ngModel)] directive. 尽管看起来很奇怪,但实际上很优雅. 只是值的数据绑定的语法糖 appService.username,以及从用户输入事件自动赋值.

Summary: 计算属性在React/MobX中比在Angular/RxJS中更容易实现, 但是RxJS可能会提供一些更有用的FRP特性, 以后也许会感激你的.

Templates and CSS

展示模板如何相互堆叠, 让我们使用Posts组件来显示一个帖子列表.

Angular

@Component({
  选择器:“app-posts”,
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css'],
  providers: [
    PostsService
  ]
})
 
导出类PostsComponent实现OnInit {
  constructor(
    公共邮政服务:postsService;
    公共appService: appService
  ) { }
 
  ngOnInit() {
    this.postsService.initializePosts()
  }
}

这个组件只是将HTML连接在一起, CSS, 和注入的服务,还调用函数在初始化时从API加载帖子. AppService 在应用程序模块中定义了单例吗 PostsService 是瞬时的,在每个创建的时间组件上都创建一个新的实例. 从这个组件引用的CSS的作用域是这个组件, 这意味着内容不能影响组件之外的任何内容.


  

Hello {{appService.username}}

{{post.title}} {{post.name}}

{{post.message}}

在HTML模板中,我们主要引用Angular Material中的组件. 要使它们可用,必须将它们包含在 app.module imports (see above). The *ngFor 指令用于重复 md-card 每个岗位的组件.

Local CSS:

.mat-card {
  margin-bottom: 1快速眼动;
}

局部CSS只是增加了类中的一个 md-card component.

Global CSS:

.float-right {
  float: right;
}

这个类在global中定义 style.css 文件,使其可用于所有组件. 它可以以标准的方式被引用, class="float-right".

Compiled CSS:

.float-right {
  float: right;
}
.mat-card (_ngcontent-c1) {
    margin-bottom: 1快速眼动;
}

In compiled CSS, 我们可以看到,本地CSS已经通过使用 [_ngcontent-c1] attribute selector. 每个渲染的Angular组件都有一个这样的生成类,用于CSS作用域.

这种机制的优点是我们可以正常地引用类, 作用域是在“底层”处理的.”

React

导入*作为样式./posts.css'
import * as appStyle from '../app.css'
 
@observer
导出类Posts扩展React.Component {
 
  postsStore: postsStore
  componentWillMount () {
    this.postsStore = new postsStore ()
    this.postsStore.initializePosts()
  }
 
  render() {
    return 
      
    
  }
}

在React中,我们需要使用 Provider approach to make PostsStore 依赖“瞬时”. 我们还导入CSS样式,引用为 style and appStyle,以便能够在JSX中使用这些CSS文件中的类.

接口PostsComponentProps {
  appStore?: AppStore,
  postsStore?: PostsStore
}
 
@ inject(“应用商店”、“postsStore”)
@observer
导出类PostsComponent扩展React.Component {
  render() {
    const {postsStore, appStore} =这个.props
    return 
} }

Naturally, 与Angular的HTML模板相比,JSX感觉更像javascript, 这是好事还是坏事取决于你的口味. Instead of *ngFor 指令,我们使用 map 构造来迭代帖子.

Now, Angular可能是最推崇TypeScript的框架, 但TypeScript真正闪耀的地方是JSX. 通过添加CSS模块(上面导入的), 它真的把你的模板编码变成了代码完成. 每件事都经过类型检查. 组件、属性,甚至CSS类(appStyle.floatRight and style.messageCard, see below). And of course, 与Angular的模板相比,JSX的精简特性更鼓励将其拆分为组件和片段.

Local CSS:

.messageCard {
  margin-bottom: 1快速眼动;
}

Global CSS:

.floatRight {
  float: right;
}

Compiled CSS:

.floatRight__qItBM {
  float: right;
}
 
.messageCard__1Dt_9 {
    margin-bottom: 1快速眼动;
}

As you can see, CSS模块加载器会给每个CSS类添加一个随机后缀, 这保证了独特性. 避免冲突的直接方法. 然后通过webpack导入的对象引用类. 这样做的一个可能的缺点是,您不能只使用类创建CSS并对其进行扩展, 就像我们在Angular的例子中做的那样. On the other hand, 这其实是一件好事, 因为它迫使您正确封装样式.

Summary: 我个人更喜欢JSX,而不是Angular模板, 特别是因为代码完成和类型检查的支持. 这确实是一个杀手级功能. Angular现在有了AOT编译器, 哪还能看出几样东西呢, 代码自动完成功能也适用于大约一半的程序, 但它远没有JSX/TypeScript那么完整.

GraphQL -加载数据

因此,我们决定使用GraphQL为这个应用程序存储数据. 创建GraphQL后端最简单的方法之一是使用一些BaaS,比如Graphcool. 这就是我们所做的. 基本上,您只需定义模型和属性,CRUD就可以运行了.

Common Code

因为在这两个实现中,一些与graphql相关的代码是100%相同的, 我们不要重复两次:

const postquery = gql '
  query PostsQuery {
    allPosts(orderBy: createdAt_DESC, first: 5)
    {
      id,
      name,
      title,
      message
    }
  }
`

GraphQL是一种查询语言,旨在提供比经典RESTful端点更丰富的功能集. 让我们仔细分析这个特定的查询.

  • PostsQuery 只是这个查询以后引用的一个名称,它可以被命名为任何东西吗.
  • allPosts 是最重要的部分-它引用的功能查询与“后”模型的所有记录. 这个名字是由Graphcool创建的.
  • orderBy and first 的参数。 allPosts function. createdAt is one of the Post model's attributes. first: 5 意味着它将只返回查询的前5个结果.
  • id, name, title, and message 的属性是 Post 我们希望包含在结果中的模型. 其他属性将被过滤掉.

正如你已经看到的,它非常强大. Check out this page 以便更加熟悉GraphQL查询.

interface Post {
  id: string
  name: string
  title: string
  message: string
}
 
接口PostsQueryResult {
  allPosts: Array
}

是的,作为优秀的TypeScript公民,我们为GraphQL结果创建接口.

Angular

@Injectable()
导出类PostsService {
  posts = []
 
  构造函数(private apollo: apollo) {}
 
  initializePosts() {
    this.apollo.query({
      query: PostsQuery,
      fetchPolicy:“网络”
    }).subscribe(({ data }) => {
      this.posts = data.allPosts
    })
  }
}

GraphQL查询是一个RxJS可观察对象,我们订阅了它. 它的工作原理有点像一个承诺,但不完全是,所以我们没有运气使用 async/await. 当然,还有 toPromise,但这似乎不是Angular的方式. We set fetchPolicy:“网络” 因为在这种情况下,我们不想缓存数据,而是每次都重新取回.

React

导出类PostsStore {
  appStore: AppStore
 
  @observable posts: Array = []
 
  constructor() {
    this.appStore = AppStore.getInstance()
  }
 
  async initializePosts() {
    Const result = await this.appStore.apolloClient.query({
      query: PostsQuery,
      fetchPolicy:“网络”
    })
    this.posts = result.data.allPosts
  }
}

React版本几乎是相同的,但作为 apolloClient 这里用了promises,我们可以利用 async/await syntax. React中还有其他方法,只是将GraphQL查询“磁带”到 高阶分量但在我看来,它似乎把数据层和表示层混在一起有点太多了.

Summary: RxJS订阅的思想vs. Async /await其实是一样的.

GraphQL -保存数据

Common Code

同样,一些GraphQL相关代码:

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

突变的目的是创建或更新记录. 因此,声明一些带有突变的变量是有益的,因为这些是将数据传递给它的方式. So we have name, title, and message 变量,类型为 String,我们每次调用这个突变时都需要填充它. The createPost 函数也是由Graphcool定义的. We specify that the Post 模型的键将具有来自我们的突变变量的值,而且我们只需要 id 要发送的新创建的Post的值.

Angular

@Injectable()
导出类FormService {
  constructor(
    私人阿波罗:阿波罗,
    private router: router;
    private appService: appService
  ) { }
 
  addPost(value) {
    this.apollo.mutate({
      变异:AddPostMutation,
      variables: {
        name: this.appService.username,
        title: value.title,
        message: value.message
      }
    }).subscribe(({ data }) => {
      this.router.导航([' /帖子'])
    }, (error) => {
      console.日志('发送查询出错',错误)
    })
  }
 
}

When calling apollo.mutate,我们需要提供我们调用的突变和变量. We get the result in subscribe 回调并使用注入 router 导航回帖子列表.

React

导出类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(
      {
        变异: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.

Summary: 同样,这里没有太大区别. subscribe vs. Async /await基本上都是不同的.

Forms

我们希望在这个应用程序中实现以下目标:

  • 字段到模型的数据绑定
  • 每个字段的验证消息,多个规则
  • 支持检查整个表单是否有效

React

export const check = (validator, message, options) =>
  (value) => (!验证器(价值,选项) && message)
 
export const checkRequired = (msg: string) => check(nonEmpty, msg)
 
导出类PostFormState {
  title = new FieldState(").validators(
    checkRequired('Title is required'),
    标题长度必须至少为4个字符.', { min: 4 }),
    标题长度不能超过24个字符.', { max: 24 }),
  )
  message = new FieldState(").validators(
    checkRequired('消息不能为空.'),
    消息太短,最小长度为50个字符.', { min: 50 }),
    check(isLength, '消息太长,最多1000个字符。'.', { max: 1000 }),
  )
  form = new FormState({
    title: this.title,
    message: this.message
  })
}

So the formstate 对于表单的每个字段,定义一个 FieldState. 传递的参数是初始值. The validators 属性接受一个函数, 当值有效时,哪个返回“false”, 并在值无效时显示验证消息. With the check and checkRequired Helper函数,它看起来都是声明性的.

要对整个表单进行验证,将这些字段用 FormState 实例,然后该实例提供聚合有效性.

@ inject(“应用商店”、“formStore”)
@observer
导出类FormComponent扩展React.Component {
  render() {
    const {appStore, formStore} =这个.props
    const {postFormState} = formStore
    return 

Create a new post

You are now posting as {appStore.username}

The FormState instance provides value, onChange, and error 属性,它可以很容易地与任何前端组件一起使用.

      
} }

When form.hasError is true,我们保持按钮禁用. 提交按钮将表单发送到前面介绍的GraphQL变体.

Angular

在Angular中,我们将使用 FormService and FormBuilder的一部分 @angular/forms package.

@Component({
  选择器:“app-form”,
  templateUrl: './form.component.html',
  providers: [
    FormService
  ]
})
导出类FormComponent {
  postForm: FormGroup
  validationMessages = {
    'title': {
      'required': 'Title是必需的.',
      'minlength': '标题长度必须至少为4个字符.',
      'maxlength': '标题长度不能超过24个字符.'
    },
    'message': {
      'required': '消息不能为空.',
      'minlength': '消息太短,最小长度为50个字符',
      'maxlength': '消息太长,最多1000个字符'
    }
  }

首先,让我们定义验证消息.

  constructor(
    private router: router;
    private 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,创建表单结构非常容易,甚至比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(例子)
        Object.keys(control.errors).forEach(error => {
          Errors [key] += messages[error] + ' '
        })
      }
    })
    return errors
  }

为了将可绑定的验证消息放到正确的位置,我们需要进行一些处理. 这段代码取自官方文档,做了一些小改动. Basically, in FormService, 字段只保留对活动错误的引用, 由验证器名称标识, 因此,我们需要手动将所需的消息与受影响的字段配对. 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.导航([' /帖子'])
  }
}

同样,当表单有效时,可以将数据发送到GraphQL进行更改.

Create a new post

You are now posting as {{appService.username}}

{{validationErrors['title']}}

{{validationErrors['message']}}



最重要的是引用我们用FormBuilder创建的formGroup, which is the [formGroup] = " postForm " assignment. 控件将表单中的字段绑定到表单模型 formControlName property. 同样,当表单无效时,我们禁用“提交”按钮. 我们还需要添加脏检查,因为在这里,非脏表单仍然可能无效. 我们希望按钮的初始状态是“enabled”.

Summary: 在React和Angular中,这种处理表单的方法在验证和模板方面都有很大的不同. Angular的方法涉及到更多的“魔法”,而不是直接的绑定, but, on the other hand, 是否更完整和彻底.

Bundle size

Oh, one more thing. 生产环境缩小了JS包的大小, 使用应用生成器的默认设置:特别是React中的摇树和Angular中的AOT编译.

  • Angular: 1200 KB
  • React: 300 KB

这没什么好惊讶的. Angular一直是体积更大的一个.

当使用gzip时,大小分别下降到275kb和127kb.

请记住,这基本上是所有供应商的库. 相比之下,实际应用程序代码的数量是最少的, 在实际应用程序中不是这样的吗. 在那里,比例可能更像是1:2而不是1:4. Also, 当你开始在React中包含大量第三方库时, 包的大小也往往增长得相当快.

图书馆的灵活性vs. 框架的鲁棒性

看来我们不能再……了!)来给出一个明确的答案,到底React和Angular哪个更适合web开发.

事实证明,React和Angular的开发工作流非常相似, 这取决于我们选择使用哪些库. 这主要是个人喜好的问题.

如果你喜欢现成的堆栈, 强大的依赖注入,并计划使用一些RxJS的好东西, chose Angular.

如果你喜欢自己动手做的话, 您喜欢JSX的直接性,更喜欢简单的可计算属性, choose React/MobX.

同样,您可以从本文获得该应用程序的完整源代码 here and here.

或者,如果你喜欢更大的,现实世界的例子:

首先选择你的编程范式

使用React/MobX编程实际上比使用React/Redux更类似于Angular. 在模板和依赖管理方面有一些明显的区别,但它们是相同的 可变/数据绑定 paradigm.

React/Redux with its 不可变/单向 范式是一个完全不同的野兽.

不要被Redux库的小内存所迷惑. 它可能很小,但它毕竟是一个框架. 目前大多数Redux最佳实践都集中在使用与Redux兼容的库上,比如 Redux Saga 对于异步代码和数据获取, Redux Form for form management, Reselect 用于记忆选择器(Redux的计算值). and Recompose 其中包括更细粒度的生命周期管理. 此外,Redux社区也有一个转变 Immutable.js to Ramda or lodash/fp,它使用普通的JS对象,而不是转换它们.

现代Redux的一个很好的例子是众所周知的 React Boilerplate. 这是一个强大的开发堆栈, 但如果你仔细看, it is really very, 与我们在这篇文章中看到的非常不同.

我觉得Angular受到了JavaScript社区中一些直言不讳的人的不公平对待. 许多对它表示不满的人可能没有意识到旧的AngularJS和现在的Angular之间发生的巨大变化. In my opinion, 这是一个非常清晰和富有成效的框架,如果它早出现1-2年,将会席卷全球.

Still, Angular正在获得稳固的立足点, 尤其是在企业界, 拥有庞大的团队,需要标准化和长期支持. 或者换一种说法, Angular是谷歌工程师认为web开发应该采用的方式, 如果这还有什么意义的话.

对于MobX,也适用类似的评估. 真的很棒,但是被低估了.

总而言之:不要一开始就问“React还是Angular ??相反,首先选择你的编程范式. 或者尝试在React中使用Angular ngimport

可变/数据绑定 or 不可变/单向这似乎才是真正的问题.

了解基本知识

  • React是一个框架还是一个库?

    React是一个用于构建用户界面的JavaScript库. 它处理视图,并允许您选择前端架构的其余部分. However, 围绕它形成了一个强大的图书馆生态系统, 通过添加一些库,你可以围绕React构建一个完整的框架.

  • 我如何在Angular中实现计算属性?

    将作为计算属性基础的所有值定义为BehaviorSubject(可通过RxJS获得), 并手动订阅属性所依赖的每个主题.

聘请Toptal这方面的专家.
Hire Now
托马斯·霍拉斯的头像
Tomas Holas

Located in 布拉格,捷克共和国

Member since February 10, 2015

About the author

Tomas最初是一个Ruby on Rails爱好者, 但在2010年,他转向了JavaScript,从此更喜欢使用Angular, React, and NodeJS.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

IBM

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.