[译]Angular vs React:谁更适合前端开发
我之所以写这篇文章,是因为这些发表的文章 —— 虽然它们包含不错的观点 —— 并没有深入讨论:作为一个实际的前端开发者,应该选取哪种框架来满足自己的需求。
在本文中,我会介绍 Angular 与 React 如何用不同的哲学理念解决相同的前端问题,以及选择哪种框架基本上是看个人喜好。为了方便进行比较,我准备编写同一个 app 两次,一次使用 Angular 一次使用 React。
Angular 之殇两年前,我写了一篇有关 React 生态系统的文章。在我看来,Angular 是“预发布时就跪了”的倒霉蛋(victim of “death by pre-announcement”)。那个时候,任何不想让自己项目跑在过时框架上的开发者很容易在 Angular 和 React 之间做出选择。Angular 1 就是被时代抛弃的框架,(原本的)Angular 2 甚至没有活到 alpha 版本。
不过事后证明,这种担心是多多少少有合理性的。Angular 2 进行了大幅度的修改,甚至在最终发布前对主要部分进行了重写。
两年后,我们有了相对稳定的 Angular 4。
怎么样?
Angular vs React:风马牛不相及 (Comparing Apples and Oranges)把 React 和 Angular 拿来比较是件很没意义的事情(校对逆寒: Comparing Apples and Oranges 是一种俚语说法,比喻把两件完全不同的东西拿来相提并论)。因为 React 只是一个处理界面(view)的库,而 Angular 是一个完整齐备的全家桶框架。
当然,大部分 React 开发者会添加一系列的库,使得 React 成为完整的框架。但是这套完整框架的工作流程又一次和 Angular 完全不同,所以其可比性也很有限。
两者最大的差别是对状态(state)的管理。Angular 通过数据绑定(data-binding)来将状态绑在数据上,而 React 如今通常引入 Redux 来提供单向数据流、处理不可变的数据(译者:我个人理解这句话的意思是 Angular 的数据和状态是互相影响的,而 React 只能通过切换不同的状态来显示不同的数据)。这是刚好互相对立的解决问题方法,而开发者们则不停的争论可变的/数据绑定模式与不可变的/单向的数据流两者间谁更优秀。
公平竞争的环境既然 React 更容易理解,为了便于比较,我决定编写一份 React 与 Angular 的对应表,来合理的并排比较两者的代码结构。
Angular 中有但是 React 没有默认自带的特性有:
特性 — Angular 包 — React 库
相对单向数据流来说,数据绑定可能更适合入门。当然,也可以使用完全相反的做法(指单向数据流),比如使用 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。
带有作用域的 CSSCSS 的类比较像是全局变量一类的东西。有许多方法来组织 CSS 以避免互相起冲突(包括BEM),但是当前的趋势是使用库辅助处理 CSS 以避免冲突,而不是需要前端开发者煞费苦心的设计精密的 CSS 命名系统。
表单校验是非常重要而且使用广泛的特性,使用相关的库可以有效避免冗余代码和 bug。
程序生成器(Project Generator,也就是命令行工具)使用一个命令行工具来创建项目比从 Github 上下载样板文件要方便的多。
分别使用 React 与 Angular 实现同一个 app那么我们准备使用 React 和 Anuglar 编写同一个 app。这个 app 并不复杂,只是一个可以供任何人发布帖子的公共贴吧(Shoutboard)。
你可以在这里体验到这个 app:
如果想阅读本项目的完整源代码,可以从如下地址下载:
贴吧源码 Angular 版 贴吧源码 React 版你瞧,我们同样使用 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 中。子组件可以访问到已声明的变量,而且有机会可以添加一些自己的东西。
Reactconst 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 更好只是单纯的个人喜好。
连接(Links)与命令式导航现在有两种方法来进行页面跳转。声明式的方法,使用超链接 a href... 标签;命令式的方法,直接调用 routing (以及 location)API。
Angularh1 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,为其加载适当的 routerLinkActiveCSS 样式,方便在页面中凸显。
router 使用特殊的 router-outlet 标签来渲染当前路径对应的视图(不管是哪种)。当 app 的子组件嵌套的比较深的时候,便可以使用很多 router-outlet 标签。
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate([/posts]) }
路由模块可以注入进任何服务(一半是因为 TypeScript 是强类型语言的功劳),private 的声明修饰可以将路由存储在组件的实例上,不需要再显式声明。使用 navigate 方法便可以切换路径。
Reactimport * 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会保留,而且可以在任何页面访问到。
Reactimport { 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 homeStore.increment} Click! /button /div }
HomeComponent 使用 @observer 装饰器监听被 @observable 装饰器修饰的属性变化。
其底层机制很有趣,所以我们简单的介绍一下。@observable 装饰器通过替换对象中(被观察)属性的 getter 和 setter 方法,拦截对该属性的调用。当被 @observer 修饰的组件调用其渲染函数(render function)时,这些属性的 getter 方法也会被调用,getter 方法会将对属性的引用保存在调用它们的组件上。
然后,当 setter 方法被调用、这些属性的值也改变的时候,上一次渲染这些属性的组件会(再次)调用其渲染函数。这样被改变过的属性会在界面上更新,然后整个周期会重新开始(译者注:其实就是典型的观察者模式啊...)。
这是一个非常简单的机制,也是很棒的特性。更深入的解释在这里.
@inject 装饰器用来将 appStore 和 homeStore 的实例注入进 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 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" button md-fab md-icon add /md-icon /button 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 {{post.message}} /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 文件)的样式, 。
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”)。
Reactimport * 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) name, title, message `
比起传统的 REST 风格的接口,GraphQL 是一种为了提供函数性富集合的查询语言。让我们分析一下这个特定的查询。
allPosts 是最重要的部分:它是查询所有帖子数据函数的引用。这是 Graphcool 创建的名字。
orderBy 和 first 是 allPost 的参数,createdAt 是帖子数据模型的一个属性。first: 5 意思是返回查询结果的前 5 条数据。
id、name、title、以及 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 来保证在这种情况不进行缓存操作,而是每次都从服务端获取最新数据。
Reactexport 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 `
修改(mutations,GraphQL 术语)的目的是为了创建或者更新数据。在修改中声明一些变量是十分有益的,因为这其实是传递数据的方式。我们有 name、title、以及 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)以及修改中所包含的变量值。然后在订阅的回调函数中获得返回结果,使用注入的路由来跳转帖子列表页面。
Reactexport 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 中用表单达到以下目标:
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。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 formStore.goBack} raised accent / nbsp; Button label=Submit formStore.submit} raised disabled={postFormState.form.hasError} primary /
当 form.hasError 的返回值是 true 的时候,我们让按钮控件保持禁用状态。提交按钮发送表单数据到之前编写的 GraphQL 修改(mutation)上。
Angular在 Angular 中,我们会使用 @angular/formspackage 中的 FormService 和 FormBuilder。
@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 md-input-container textarea mdInput placeholder="Message" formControlName="message" /textarea md-error {{validationErrors[message]}} /md-error /md-input-container 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 /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 吧。
或者,如果你喜欢大一点的真实项目:
RealWorld Angular 4+ RealWorld React/MobX 先选择自己的编程习惯使用 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,并且已经获得转载授权。 原文发布时间为:2017年9月3日 本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。掌握React的基本使用,重塑前端开发 React change the way that Web apps should be build. UI - Web apps 四步: Break The UI Into A Component Hierarchy 将UI结构拆解成组件结构 Build A Static Version in React It s best to decouple these processes because building a static version requires a lot of typing and no thinking, and adding interactivity requi
带你读《React+Redux前端开发实战》之一: React入门 本书是一本React入门书,也是一本React实践书,更是一本React企业级项目开发指导书。全书系统地介绍了以React.js为中心的各种前端开发技术,可以帮助前端开发人员系统地掌握这些知识,提升自己的开发水平。
带你读《React+Redux前端开发实战》之二:React的组件 本书是一本React入门书,也是一本React实践书,更是一本React企业级项目开发指导书。全书系统地介绍了以React.js为中心的各种前端开发技术,可以帮助前端开发人员系统地掌握这些知识,提升自己的开发水平。
相关文章
- angular js 指令的数据传递 及作用域数据绑定
- 从Java角度理解Angular之入门篇:npm, yarn, Angular CLI
- Angular 复习与进阶系列 – Component 组件 の Template Binding Syntax
- Angular – ESLint
- Angular Material 学习笔记 Chips
- Angular 学习笔记 immer 使用
- Asp.net core Identity + identity server + angular 学习笔记 (第五篇)
- Angular 学习笔记 ( CDK - Portal )
- 比较vue.js react.js angular.js
- JavaScript MVC框架PK:Angular、Backbone、CanJS与Ember
- angular中的ng-bind-html指令和$sce服务
- Angular Material 按钮图标系列
- Angular material mat-icon 资源参考_Places
- Windows 安装Angular CLI
- angular双向绑定与单向绑定的写法区别
- 颜值安全都在线,施耐德电气 Angular 智意系列排插体验