个性化阅读
专注于IT技术分析

Angular vs. React:Web开发哪个更好?

本文概述

有无数的文章讨论了React还是Angular是Web开发的更好选择。我们还需要一个吗?

我写这篇文章的原因是, 尽管已经发表了许多文章(尽管它们都包含了深刻的见解), 但它们都没有深入到足够的深度, 因此实际的前端开发人员可以决定哪一个适合他们的需求。

在本文中, 你将学习Angular和React如何通过不同的理念来解决相似的前端问题, 以及选择一个还是另一个仅是个人喜好问题。为了比较它们, 我们将两次构建相同的应用程序, 一次使用Angular, 然后再次使用React。

Angular的不合时宜的公告

两年前, 我写了一篇有关React生态系统的文章。文章还指出, Angular已成为”预先宣布死亡”的受害者。当时, 对于不希望项目在过时的框架上运行的任何人来说, 在Angular和几乎其他任何东西之间进行选择都是一件容易的事。 Angular 1已过时, 而Angular 2甚至没有alpha版本。

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

两年后, 从现在开始, 我们有了Angular 4, 并保证了相对的稳定性。

怎么办?

Angular与React:比较苹果和橙子

有人说, 比较React和Angular就像比较苹果和橘子。一个是处理视图的库, 另一个是成熟的框架。

当然, 大多数React开发人员都会向React添加一些库以将其变成一个完整的框架。再说一次, 此堆栈的最终工作流程通常与Angular仍然有很大的不同, 因此可比性仍然有限。

最大的区别在于状态管理。 Angular捆绑了数据绑定, 而今天的React通常通过Redux进行增强, 以提供单向数据流并处理不可变数据。那些方法本身就是对立的方法, 现在正在进行无数讨论, 关于可变/数据绑定是比不变/单向更好还是更坏。

一个公平竞争的环境

由于React很容易被黑客攻击, 因此出于比较的目的, 我决定构建一个React设置, 以合理地紧密镜像Angular, 以允许并排比较代码段。

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

特征 角包 反应库
数据绑定, 依赖项注入(DI) @角/核心 手机
计算属性 rxjs 手机
基于组件的路由 @角度/路由器 反应路由器v4
材料设计组件 @角度/材料 反应工具箱
CSS范围仅限于组件 @角/核心 CSS模块
表单验证 @角度/形式 形式的国家
项目生成器 @ angular / cli 反应脚本TS

数据绑定

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

计算属性

在性能方面, Angular中的普通吸气剂在每次渲染时都会被调用。可以使用RsJS的BehaviorSubject来完成这项工作。

使用React, 可以使用MobX的@computed, 它可以实现相同的目标, 而且可以说是更好的API。

依赖注入

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

DI(Angular支持)的另一个优势是能够拥有不同商店的不同生命周期。当前的大多数React范例都使用某种映射到不同组件的全局应用程序状态, 但是根据我的经验, 在清除组件卸载的全局状态时引入错误很容易。

拥有一个在组件安装上创建的商店(并且可以无缝地供该组件的子级使用)的商店似乎是非常有用的, 而且常常被忽略。

开箱即用的Angular, 但也很容易用MobX重现。

路由

基于组件的路由允许组件管理自己的子路由, 而不用进行大型的全局路由器配置。这种方法最终使其成为了版本4中的react-router。

材料设计

从一些更高级别的组件开始总是很高兴, 而且即使在非Google项目中, 材料设计也已成为普遍接受的默认选择。

我故意选择了React Toolbox而不是通常推荐的Material UI, 因为Material UI的内联CSS方法存在严重的自认性能问题, 他们计划在下一个版本中解决。

此外, React Toolbox中使用的PostCSS / cssnext无论如何都开始取代Sass / LESS。

范围CSS

CSS类类似于全局变量。组织CSS来防止冲突(包括BEM)的方法很多, 但是使用库来处理CSS来防止冲突的趋势很明显, 而无需前端开发人员设计复杂的CSS命名系统。

表格验证

表单验证是一项非常重要且用途非常广泛的功能。最好将它们覆盖在库中以防止代码重复和错误。

项目生成器

为项目配备CLI生成器仅比从GitHub复制样板要方便得多。

相同的应用程序, 内置两次

因此, 我们将在React和Angular中创建相同的应用程序。没什么特别的, 只有一个Shoutboard, 任何人都可以将消息发布到公共页面。

你可以在此处尝试应用程序:

  • out角板
  • 留言板反应
留言板应用

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

  • Shoutboard角源
  • Shoutboard React源

你会注意到我们也将TypeScript用于React应用程序。 TypeScript中类型检查的优点是显而易见的。现在, 随着更好地处理导入, 异步/等待和剩余传播最终进入TypeScript 2, Babel / ES7 / Flow尘埃落定。

另外, 让我们同时添加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
  ], bootstrap: [AppComponent]
})
@Injectable()
export class AppService {
  username = 'Mr. User'
}

基本上, 我们要在应用程序中使用的所有组件都需要进行声明。所有要导入的第三方库, 以及所有提供程序的全球商店。子组件可以访问所有这些内容, 并有机会添加更多本地内容。

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中的依赖项注入。它将存储保存到上下文中, 以便React组件可以在以后注入它们。是的, 可以安全地使用React上下文(可以说)。

由于没有模块声明, 因此React版本要短一些-通常, 你只需导入即可使用。有时这种硬性依赖是不需要的(测试), 因此对于全球单例商店, 我不得不使用这种已有数十年历史的GoF模式:

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

Angular的Router是可注射的, 因此可以在任何地方使用, 而不仅仅是组件。为了达到相同的反应, 我们使用mobx-react-router包并注入routerStore。

简介:引导两个应用程序非常简单。 React的优势是更简单, 只使用导入而不是模块, 但是, 正如我们将在后面看到的那样, 这些模块非常方便。手动制作单身人士有点麻烦。至于路由声明语法, JSON与JSX只是一个优先选择的问题。

链接和命令式导航

因此, 有两种情况可以切换路由。使用<a href…>元素进行声明式, 并且命令式直接调用路由(以及位置)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类, 以便对其进行样式设置。

路由器使用特殊的<router-outlet>元素来呈现当前路径指示的内容。随着我们对应用程序子组件的深入研究, 可能会有许多<router-outlet>。

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

路由器模块可以注入到任何服务中(通过其TypeScript类型可以神奇地实现一半), 然后私有声明将其存储在实例上, 而无需显式分配。使用导航方法切换URL。

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模块编译器已将其命名为唯一, 因此我们需要使用样式帮助器。以后再说。

如上所示, React Router在<App>元素内使用<Switch>元素。由于<Switch>元素仅包装并安装了当前路由, 这意味着当前组件的子路由就是this.props.children。这也是可组合的。

export class FormStore {
  routerStore: RouterStore
  constructor() {
    this.routerStore = RouterStore.getInstance()
  }
  goBack = () => {
    this.routerStore.history.push('/posts')
  }
}

mobx-router-store软件包还允许轻松进行注入和导航。

简介:两种路由选择方法都具有可比性。 Angular似乎更直观, 而React Router具有更直接的可组合性。

依赖注入

已经证明将数据层与表示层分离是有益的。我们在这里要通过DI来实现的目的是使数据层的组件(在这里称为模型/存储/服务)遵循可视化组件的生命周期, 从而无需接触全局即可创建此类组件的一个或多个实例。州。同样, 应该可以混合和匹配兼容的数据和可视化层。

本文中的示例非常简单, 因此所有DI内容似乎都有些过头, 但是随着应用程序的增长, 它派上了用场。

Angular

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

因此, 任何类都可以设置为@injectable, 其属性和方法可供组件使用。

@Component({
  selector: 'app-home', templateUrl: './home.component.html', providers: [
    HomeService
  ]
})
export class HomeComponent {
  constructor(
    public homeService: HomeService, public appService: AppService, ) { }
}

通过将HomeService注册到组件的提供程序, 我们可以使其专用于此组件。现在不是单例, 但是组件的每个实例将收到一个新副本, 该副本在组件的安装架上是全新的。这意味着以前使用过的数据不会过时。

相比之下, AppService已注册到app.module(请参见上文), 因此它是单例的, 并且在应用程序的整个生命周期中, 所有组件均保持不变。能够从组件控制服务的生命周期是一个非常有用但未被重视的概念。

DI通过将服务实例分配给由TypeScript类型标识的组件的构造函数来工作。此外, 公共关键字会自动为此分配参数, 因此我们不再需要编写无聊的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路数据绑定, 但实际上, 它实际上是属性绑定+事件。正如我们服务的生命周期所规定的那样, 每次我们离开/ home时, homeService.counter都会重置, 但是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示例还要多。我们将HomeComponent包装在Provider中, 该Provider在每次安装时都会收到一个HomeStore的新实例。

interface HomeComponentProps {
  appStore?: AppStore, homeStore?: HomeStore
}
 
@inject('appStore', 'homeStore')
@observer
export class HomeComponent extends React.Component<HomeComponentProps, any> {
  render() {
    const { homeStore, appStore } = this.props
    return <div>
      <h3>Dashboard</h3>
      <Input
        type='text'
        label='Edit your name'
        name='username'
        value={appStore.username}
        onChange={appStore.onUsernameChange}
      />
      <span>Clicks since last visit: {homeStore.counter}</span>
      <button onClick={homeStore.increment}>Click!</button>
    </div>
  }
}

HomeComponent使用@observer装饰器来侦听@observable属性中的更改。

它的幕后机制非常有趣, 因此让我们在此简要介绍一下。 @observable装饰器用getter和setter替换对象中的属性, 从而允许其拦截调用。调用@observer增强组件的render函数时, 将调用这些属性getter, 并保留对调用它们的组件的引用。

然后, 当调用setter并更改值时, 将调用使用最后一个渲染上的属性的组件的渲染功能。现在, 有关在何处使用哪些属性的数据将更新, 并且整个循环可以重新开始。

一个非常简单的机制, 并且性能也很高。这里有更深入的解释。

@inject装饰器用于将appStore和homeStore实例注入HomeComponent的props中。在这一点上, 每个商店都有不同的生命周期。 appStore在应用程序的生命周期内是相同的, 但是homeStore是在每次导航到” / home”路线的导航上全新创建的。

这样做的好处是, 不必像所有存储都是全局存储时那样手动清除属性, 如果路由是某个”详细”页面且每次包含完全不同的数据, 则很麻烦。

简介:作为Angular DI固有功能中的提供商生命周期管理, 在那实现它当然更简单。 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的结果将被缓存, 并且仅在计数器更改时才重新计算。

<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模板引用属性(和增量方法)。通过绑定到值来驱动输入字段, 然后让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
    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>

注意我们如何用|引用RxJS主题。异步管道。这很不错, 比需要订阅组件要短得多。输入组件由[(ngModel)]指令驱动。尽管看起来很奇怪, 但实际上还是很优雅的。只是将值数据绑定到appService.username并从用户输入事件自动分配值的语法糖。

简介:在React / MobX中, 比在Angular / RxJS中更容易实现计算属性, 但是RxJS可能会提供一些更有用的FRP功能, 稍后将对此进行介绍。

模板和CSS

为了展示模板之间如何堆叠, 让我们使用显示帖子列表的帖子组件。

Angular

@Component({
  selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [
    PostsService
  ]
})
 
export class PostsComponent implements OnInit {
  constructor(
    public postsService: PostsService, public appService: AppService
  ) { }
 
  ngOnInit() {
    this.postsService.initializePosts()
  }
}

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

<a routerLink="/form" class="float-right">
  <button md-fab>
    <md-icon>add</md-icon>
  </button>
</a>
<h3>Hello {{appService.username}}</h3>
<md-card *ngFor="let post of postsService.posts">
  <md-card-title>{{post.title}}</md-card-title>
  <md-card-subtitle>{{post.name}}</md-card-subtitle>
  <md-card-content>
    <p>
      {{post.message}}
    </p>
  </md-card-content>
</md-card>

在HTML模板中, 我们主要引用Angular Material中的组件。为了使它们可用, 有必要将它们包括在app.module导入中(请参见上文)。 * ngFor指令用于为每个帖子重复md卡组件。

本地CSS:

.mat-card {
  margin-bottom: 1rem;
}

本地CSS只是增加了md-card组件上存在的类之一。

全局CSS:

.float-right {
  float: right;
}

此类在全局style.css文件中定义, 以使其可用于所有组件。可以以标准方式class =” float-right”进行引用。

编译的CSS:

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

在已编译的CSS中, 可以看到使用[_ngcontent-c1]属性选择器将本地CSS的作用域限定为呈现的组件。每个渲染的Angular组件都有一个生成的类, 用于CSS作用域。

这种机制的优点是我们可以正常地引用类, 并且作用域是在”后台”处理的。

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依赖关系”短暂”。我们还导入了称为style和appStyle的CSS样式, 以便能够使用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>
  }
}

自然, 与Angular的HTML模板相比, JSX感觉更像JavaScript, 根据你的喜好, 这可能是好事, 也可能是坏事。代替* ngFor指令, 我们使用map构造遍历帖子。

现在, Angular可能是吹捧TypeScript最多的框架, 但实际上TypeScript真正发挥作用的是JSX。通过添加CSS模块(上面导入), 确实可以将你的模板编码变成代码完成禅。每件事都经过类型检查。组件, 属性, 甚至CSS类(appStyle.floatRight和style.messageCard, 请参见下文)。当然, JSX的精简特性鼓励将组件拆分成多个片段, 而不仅仅是Angular的模板。

本地CSS:

.messageCard {
  margin-bottom: 1rem;
}

全局CSS:

.floatRight {
  float: right;
}

编译的CSS:

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

如你所见, CSS模块加载器使用随机后缀对每个CSS类进行后缀, 以确保唯一性。避免冲突的直接方法。然后通过webpack导入的对象引用类。这样做的一个可能的缺点是, 你不能像在Angular示例中那样, 仅使用类创建CSS并对其进行扩充。另一方面, 这实际上可能是一件好事, 因为它迫使你正确封装样式。

简介:我个人比Angular模板更喜欢JSX, 尤其是因为代码完成和类型检查支持。这确实是一个杀手feature。 Angular现在拥有AOT编译器, 它也可以发现一些东西, 代码完成也可以在其中完成一半的工作, 但它的完成程度不如JSX / TypeScript。

GraphQL-加载数据

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

通用密码

由于与GraphQL相关的某些代码在两种实现方式中都是100%相同, 因此我们不再重复两次:

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

GraphQL是一种查询语言, 旨在提供比传统RESTful终结点更丰富的功能。我们来剖析这个特定的查询。

  • PostsQuery只是此查询以后要引用的名称, 可以命名为任何名称。
  • allPosts是最重要的部分-它引用函数以Post模型查询所有记录。该名称是由Graphcool创建的。
  • orderBy和first是allPosts函数的参数。 createdAt是Post模型的属性之一。 first:5表示它将仅返回查询的前5个结果。
  • id, 名称, 标题和消息是我们要包含在结果中的Post模型的属性。其他属性将被过滤掉。

如你所见, 它非常强大。请查看此页面, 以进一步熟悉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, 我们对此进行了订阅。它的工作原理有点像一个承诺, 但并不完全一样, 因此我们使用async / await感到不走运。当然, 仍然有Promise, 但这似乎并不是Angular的方式。我们将fetchPolicy设置为”仅限网络”是因为在这种情况下, 我们不想缓存数据, 但是每次都要重新提取。

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语法。在React中, 还有其他方法只是将GraphQL查询”贴”到高阶组件上, 但是在我看来, 将数据层和表示层混合得太多了。

简介:RxJS订阅与异步/等待的思想实际上是完全相同的。

GraphQL-保存数据

通用密码

同样, 一些GraphQL相关代码:

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

突变的目的是创建或更新记录。因此, 声明一些带有突变的变量是有益的, 因为这些是将数据传递到其中的方式。因此, 我们有名称, 标题和消息变量, 均以字符串形式键入, 每次调用此突变时都需要填充。同样, createPost函数由Graphcool定义。我们指定Post模型的键将具有out突变变量的值, 并且我们只希望新创建的Post的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时, 我们需要提供所调用的突变以及变量。我们在订阅回调中得到结果, 并使用注入的路由器导航回发布列表。

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')
  }
}

与上面的非常相似, 区别在于更多的”手动”依赖项注入以及异步/等待的使用。

简介:再次, 这里没有太大区别。订阅与异步/等待基本上是所有不同的地方。

形式

我们希望通过此应用程序中的表单实现以下目标:

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

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库的工作方式如下:为表单的每个字段定义一个FieldState。传递的参数是初始值。 Validators属性采用一个函数, 该函数在值有效时返回” false”, 在值无效时返回验证消息。使用check和checkRequired辅助函数, 它们看起来都可以很好地声明。

为了对整个表单进行验证, 将这些字段也包装在一个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实例提供value, onChange和error属性, 可轻松将其与任何前端组件一起使用。

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

当form.hasError为true时, 我们使按钮保持禁用状态。提交按钮将表单发送到前面介绍的GraphQL突变。

Angular

在Angular中, 我们将使用FormService和FormBuilder, 它们是@ angular / forms包的一部分。

@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'])
  }
}

同样, 当表格有效时, 可以将数据发送到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, 这是[formGroup] =” postForm”分配。表单内的字段通过formControlName属性绑定到表单模型。同样, 当表单无效时, 我们将禁用”提交”按钮。我们还需要添加脏检查, 因为在这里, 非脏表格仍然无效。我们希望按钮的初始状态为”启用”。

简介:在React和Angular中, 这种形式的表单方法在验证和模板方面都大不相同。 Angular方法包含更多的”魔力”, 而不是简单的绑定, 但是, 另一方面, 它更加完整和透彻。

捆束尺寸

哦, 还有一件事。生产使JS包的大小最小化, 并具有应用程序生成器的默认设置:特别是React中的Tree Shaking和Angular中的AOT编译。

  • 角度的:1200 KB
  • 反应:300 KB

好吧, 这里没有太多惊喜。 Angular一直是体积更大的产品。

使用gzip时, 大小分别降至275kb和127kb。

请记住, 这基本上是所有供应商库。相比之下, 实际应用程序代码的数量最少, 而在实际应用程序中情况并非如此。在那里, 比率可能更像是1:2而不是1:4。另外, 当你开始在React中包含很多第三方库时, 捆绑包的大小也往往会迅速增长。

库的灵活性与框架的坚固性

因此, 似乎我们无法(再次!)就Angular或React是否更适合Web开发找到明确的答案。

事实证明, 取决于我们选择使用React的库, React和Angular中的开发工作流程可以非常相似。然后, 这主要是个人喜好问题。

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

如果你想自己修改和构建堆栈, 则喜欢JSX的简单性, 并且喜欢更简单的可计算属性, 请选择React / MobX。

同样, 你可以从本文的此处和此处获得应用程序的完整源代码。

或者, 如果你喜欢更大的RealWorld示例:

  • 真实世界Angular 4+
  • RealWorld React / MobX

首先选择你的编程范例

实际上, 使用React / MobX进行编程比使用React / Redux更类似于Angular。模板和依赖项管理之间存在一些显着差异, 但是它们具有相同的可变/数据绑定范例。

React / Redux的不变/单向范式是完全不同的野兽。

不要被Redux库的占用空间所迷惑。它可能很小, 但是仍然是一个框架。当今大多数Redux最佳实践都集中在使用与Redux兼容的库上, 例如用于异步代码和数据提取的Redux Saga, 用于表单管理的Redux Form, 用于存储选择器的Reselect(Redux的计算值)。以及进行重组, 以进行更细化的生命周期管理。另外, Redux社区也从Immutable.js转移到Ramda或lodash / fp, 它们可以处理普通的JS对象, 而无需转换它们。

现代Redux的一个很好的例子是著名的React Boilerplate。它是一个强大的开发堆栈, 但是如果你看一看, 它与迄今为止我们在本文中看到的一切确实非常非常不同。

我觉得Angular从JavaScript社区的声音中得到了一些不公平的对待。许多对此表示不满的人可能不欣赏在旧的AngularJS和今天的Angular之间发生的巨大转变。我认为, 这是一个非常干净且富有成效的框架, 如果早在1-2年前出现, 它将席卷全球。

尽管如此, Angular在庞大的团队以及对标准化和长期支持的需求中, 尤其是在企业界中, 已经获得了坚实的立足点。或者换句话说, Angular是Google工程师认为应该进行Web开发的方式, 即使那仍然有意义。

对于MobX, 适用类似的评估。真的很棒, 但是却没有得到赞赏。

结论:在React和Angular之间进行选择之前, 请先选择你的编程范例。

可变/数据绑定或不可变/单向, 这似乎是真正的问题。

赞(0)
未经允许不得转载:srcmini » Angular vs. React:Web开发哪个更好?

评论 抢沙发

评论前必须登录!