Angular 与 React:哪个更适合 Web 开发?
已发表: 2022-03-11有无数文章讨论 React 或 Angular 是否是 Web 开发的更好选择。 我们还需要另一个吗?
我写这篇文章的原因是因为没有一篇已经发表的文章——尽管它们包含了很好的见解——深入到足以让一个实际的前端开发人员决定哪一篇可能适合他们的需求。
在本文中,您将了解 Angular 和 React 如何以截然不同的理念解决相似的前端问题,以及是否选择其中一个只是个人喜好问题。 为了比较它们,我们将构建相同的应用程序两次,一次使用 Angular,另一次使用 React。
Angular 不合时宜的公告
两年前,我写了一篇关于 React 生态系统的文章。 除其他要点外,该文章认为 Angular 已成为“预先宣布死亡”的受害者。 那时,对于不希望他们的项目在过时的框架上运行的任何人来说,在 Angular 和几乎其他任何东西之间进行选择是一件容易的事。 Angular 1 已经过时,Angular 2 甚至没有 alpha 版本。
事后看来,这种担忧或多或少是有道理的。 Angular 2 发生了巨大的变化,甚至在最终发布之前进行了重大的重写。
两年后,我们有了 Angular 4,并承诺从现在开始相对稳定。
怎么办?
Angular vs. React:比较苹果和橘子
有人说比较 React 和 Angular 就像比较苹果和橘子。 一个是处理视图的库,另一个是成熟的框架。
当然,大多数 React 开发人员都会在 React 中添加一些库,以将其变成一个完整的框架。 话又说回来,这个堆栈的最终工作流程通常仍然与 Angular 有很大不同,因此可比性仍然有限。
最大的区别在于状态管理。 Angular 捆绑了数据绑定,而今天的 React 通常由 Redux 增强,以提供单向数据流并处理不可变数据。 这些本身就是对立的方法,现在无数的讨论正在讨论可变/数据绑定是否比不可变/单向更好或更差。
一个公平竞争的环境
由于 React 以更容易破解而著称,为了进行比较,我决定构建一个 React 设置,该设置可以合理地密切反映 Angular,以允许并排比较代码片段。
某些突出但默认情况下不在 React 中的 Angular 功能是:
特征 | 角包 | 反应库 |
---|---|---|
数据绑定、依赖注入 (DI) | @角/核心 | MobX |
计算属性 | rxjs | MobX |
基于组件的路由 | @角/路由器 | 反应路由器 v4 |
材料设计组件 | @角/材料 | 反应工具箱 |
CSS 作用于组件 | @角/核心 | CSS 模块 |
表单验证 | @角/形式 | 表单状态 |
项目生成器 | @角/cli | 反应脚本 TS |
数据绑定
数据绑定可以说比单向方法更容易开始。 当然,也可以完全相反,将 Redux 或 mobx-state-tree 与 React 结合使用,将 ngrx 与 Angular 结合使用。 但这将是另一篇文章的主题。
计算属性
虽然关注性能,但 Angular 中的普通 getter 在每次渲染时都会被调用,这是毫无疑问的。 可以使用 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 中创建相同的应用程序。 没什么了不起的,只是一个允许任何人将消息发布到公共页面的留言板。
您可以在此处试用应用程序:
- 角灯板
- 枪手反应
如果您想拥有完整的源代码,可以从 GitHub 获取:
- Shoutboard 角源
- Shoutboard React 源码
你会注意到我们也为 React 应用程序使用了 TypeScript。 TypeScript 中类型检查的优势是显而易见的。 现在,随着对导入的更好处理、async/await 和 rest spread 终于出现在 TypeScript 2 中,它让 Babel/ES7/Flow 尘埃落定。
另外,让我们将 Apollo Client 添加到两者,因为我们想使用 GraphQL。 我的意思是,REST 很棒,但是大约十年之后,它就变老了。
引导和路由
首先,让我们看一下这两个应用程序的入口点。
角
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' }
基本上,我们想要在应用程序中使用的所有组件都需要进行声明。 所有第三方库都可以导入,所有全球商店都可以提供。 子组件可以访问所有这些,并有机会添加更多本地内容。
反应
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 的路由器是可注入的,因此它可以在任何地方使用,而不仅仅是组件。 为了在 react 中实现同样的效果,我们使用 mobx-react-router 包并注入routerStore
。
简介:引导这两个应用程序非常简单。 React 有一个更简单的优势,它只使用导入而不是模块,但是,正如我们稍后将看到的,这些模块可能非常方便。 手动制作单例有点麻烦。 至于路由声明语法,JSON 与 JSX 只是一个偏好问题。
链接和命令式导航
所以切换路由有两种情况。 声明式,使用<a href...>
元素,命令式,直接调用路由(以及定位)API。
角
<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 类型有一半的魔力),然后private
声明将它存储在实例上,而无需显式分配。 使用navigate
方法切换 URL。
反应
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 模块编译器制作的,我们需要使用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 Router 具有更直接的可组合性。
依赖注入
已经证明将数据层与表示层分开是有益的。 我们在这里尝试使用 DI 实现的是使数据层的组件(此处称为模型/存储/服务)遵循可视组件的生命周期,从而允许制作此类组件的一个或多个实例而无需触及全局状态。 此外,应该可以混合和匹配兼容的数据和可视化层。
本文中的示例非常简单,因此所有 DI 的东西可能看起来都有些矫枉过正,但随着应用程序的增长,它会派上用场。
角
@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
注册到组件的providers
,我们可以让该组件独占使用它。 现在它不是单例了,但是组件的每个实例都会收到一个新的副本,在组件的挂载上是新鲜的。 这意味着没有以前使用过的数据。
相比之下, AppService
已注册到app.module
(见上文),因此它是一个单例,并且对于所有组件都保持不变,尽管在应用程序的生命周期内。 能够从组件控制服务的生命周期是一个非常有用但未被充分认识的概念。
DI 通过将服务实例分配给由 TypeScript 类型标识的组件的构造函数来工作。 此外, 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 路数据绑定,但在后台,它实际上是属性绑定 + 事件。 正如我们服务的生命周期所决定的那样,每次我们离开/home
时homeService.counter
都会重置,但appService.username
保持不变,并且可以从任何地方访问。
反应
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
中,它会在每次挂载时接收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
扩展组件的渲染函数时,这些属性 getter 会被调用,并且它们会保留对调用它们的组件的引用。
然后,当调用 setter 并更改值时,调用上次渲染时使用该属性的组件的渲染函数。 现在,关于哪些属性在哪里使用的数据被更新,整个周期可以重新开始。
一个非常简单的机制,也非常高效。 更深入的解释在这里。
@inject
装饰器用于将appStore
和homeStore
实例注入到HomeComponent
的 props 中。 此时,这些商店中的每一个都有不同的生命周期。 appStore
在应用程序的生命周期中是相同的,但homeStore
是在每次导航到“/home”路径时新创建的。
这样做的好处是不需要手动清理属性,因为所有商店都是全局的,如果路由是某个“详细”页面,每次都包含完全不同的数据,这会很痛苦。
总结:作为 Angular 的 DI 的固有特性中的提供者生命周期管理,当然,在那里实现它更简单。 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 中达到同样的效果,我们需要更有创造力。
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 仅用于计算属性的这种琐碎使用比反应示例更冗长,并且您需要手动管理订阅(就像在构造函数中一样)。
<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
来引用 RxJS 主题。 | async
管道。 这是一个很好的接触,比需要订阅你的组件要短得多。 input
组件由[(ngModel)]
指令驱动。 尽管看起来很奇怪,但它实际上非常优雅。 只是一个语法糖,用于将值数据绑定到appService.username
,并从用户输入事件中自动分配值。
总结:计算属性在 React/MobX 中比在 Angular/RxJS 中更容易实现,但 RxJS 可能会提供一些更有用的 FRP 功能,稍后可能会受到赞赏。
模板和 CSS
为了展示模板如何相互叠加,让我们使用显示帖子列表的帖子组件。
角
@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-card
组件。
本地 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 中,我们可以看到本地 CSS 已通过使用[_ngcontent-c1]
属性选择器限定在渲染组件的范围内。 每个渲染的 Angular 组件都有一个这样的生成类,用于 CSS 作用域。
这种机制的优点是我们可以正常引用类,并且作用域是“在后台”处理的。
反应
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
依赖“瞬态”。 我们还导入 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 的框架,但实际上 TypeScript 真正闪耀的地方是 JSX。 通过添加 CSS 模块(上面导入),它真的把你的模板编码变成了代码完成禅。 每件事都经过类型检查。 组件、属性,甚至 CSS 类( appStyle.floatRight
和style.messageCard
,见下文)。 当然,与 Angular 的模板相比,JSX 的精简特性更鼓励拆分成组件和片段。
本地 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 并对其进行扩充。 另一方面,这实际上是一件好事,因为它迫使您正确封装样式。
总结:我个人更喜欢 JSX 比 Angular 模板好一点,尤其是因为它支持代码完成和类型检查。 这确实是一个杀手级功能。 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
、name
、title
和message
是我们希望包含在结果中的Post
模型的属性。 其他属性将被过滤掉。
正如您已经看到的,它非常强大。 查看此页面以进一步熟悉 GraphQL 查询。
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }
是的,作为优秀的 TypeScript 公民,我们为 GraphQL 结果创建接口。
角
@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,我们订阅了它。 它有点像一个承诺,但不完全是,所以我们不走运使用async/await
。 当然,还有 toPromise,不过好像也不是 Angular 的方式。 我们设置fetchPolicy: 'network-only'
因为在这种情况下,我们不想缓存数据,而是每次都重新获取。
反应
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 subscribe 和 async/await 的思路其实是一模一样的。
GraphQL - 保存数据
通用代码
同样,一些 GraphQL 相关代码:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
突变的目的是创建或更新记录。 因此,声明一些带有突变的变量是有益的,因为这些是如何将数据传递给它的方式。 所以我们有name
、 title
和message
变量,类型为String
,每次调用这个突变时都需要填充它们。 createPost
函数同样由 Graphcool 定义。 我们指定Post
模型的键将具有来自突变变量的值,并且我们希望只发送新创建的 Post 的id
作为回报。
角
@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
时,我们需要提供我们调用的突变和变量。 我们在subscribe
回调中得到结果,并使用注入的router
导航回帖子列表。
反应
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') } }
与上面非常相似,不同之处在于更多的“手动”依赖注入,以及async/await
的使用。
摘要:同样,这里没有太大区别。 subscribe 与 async/await 基本上是不同的。
形式
我们希望通过此应用程序中的表单实现以下目标:
- 字段与模型的数据绑定
- 每个字段的验证消息,多个规则
- 支持检查整个表单是否有效
反应
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 /> <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }
当form.hasError
为true
时,我们保持按钮禁用。 The submit button sends the form to the GraphQL mutation presented earlier.
Angular
In Angular, we are going to use FormService
and FormBuilder
, which are parts of the @angular/forms
package.
@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' } }
First, let's define the validation messages.
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)] ], }) }
Using FormBuilder
, it's quite easy to create the form structure, even more succintly than in the React example.
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 }
To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. 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.navigate(['/posts']) } }
Again, when the form is valid, data can be sent to GraphQL mutation.
<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>
The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm"
assignment. Fields inside the form are bound to the form model through the formControlName
property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.
Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.
捆绑大小
哦,还有一件事。 The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.
- Angular: 1200 KB
- React: 300 KB
Well, not much surprise here. Angular has always been the bulkier one.
When using gzip, the sizes go down to 275kb and 127kb respectively.
Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.
Flexibility of Libraries vs. Robustness of Framework
So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.
It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.
If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.
If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.
Again, you can get the complete source code of the application from this article here and here.
Or, if you prefer bigger, RealWorld examples:
- RealWorld Angular 4+
- RealWorld React/MobX
Choose Your Programming Paradigm First
Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.
React/Redux with its immutable/unidirectional paradigm is a completely different beast.
Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.
A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.
I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.
Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.
As for MobX, similar assessment applies. Really great, but underappreciated.
In conclusion: before choosing between React and Angular, choose your programming paradigm first.
mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.