已经有数不尽的文章讨论React和Angular哪个更适合网站开发。那我为什么写这一篇文章呢?
写这篇文章,是因为虽然已有的文章都含有很棒的观点,但没有一篇足够深入地为前端开发者评判哪一款可能满足他们的需求。
在这篇文章里,你将了解: Angular和React是怎么通过不同的方案来解决相似的前端问题的? 选择Angular或React是否仅仅只是个人喜好问题? 为了对比它们,我们分别用Angular和React构建同一个应用。
Angular的事先声明
两年前,我写了一篇关于React生态系统的文章。该文章表述,Angular已经成为了“事先宣布死亡”的受害者。那些不希望自己的项目搭建在过时框架上的人,选择Angular或还是其他任何框架,是很容易的。那时,Angular1已经过时,而Angular2处于 alpha版本,甚至都不可用。
事后看来,这些担心或多或少是有道理的。Angular2发生了戏剧性地变化,甚至在最终发布之前进行了重大改写。
两年后,我们有了Angular4,并且Google承诺从Angular4起相对稳定。
接下来,我们对比Angular和React。
Angular vs. React:比较苹果和橘子
有些人说比较React和Angular就像比较苹果和橘子。一个是处理视觉层的库,另一个是完整的框架。
当然,多数React开发人员会添加一些库将React转化为完整的框架。然而,这个React堆栈的工作流与Angular的工作流是非常不同的,所以可比性仍然是有限的。
Angular和React最大的区别在于状态管理。Angular与数据绑定捆绑在一起,而React通常使用redux增强自己,提供单向数据流并处理不可变的数据。这两方法本身就是对立的,现在仍有数不尽的争论在进行:哪一种方案更优。
公平的竞争环境
由于React是很容易上手,出于比较的目的,我决定,先建立一个React应用,再改写成一个Angular应用,以便我们能并行地比较代码片段。
Angular内置添加一些突出的特征,但React没有:
依赖注入(DI) 计算属性 rxjs 基于组件的路由 Material design UI框架 组件范畴样式 表单校验 项目生成器
数据绑定
一个有争议的观点:比起单向绑定,双向数据绑定使开发更容易。当然,也可能完全相反,比如React搭配Redux或mobx-state-tree,Angular搭配ngrx。但这需要另写一篇文章来讨论了。
计算属性
在视图层上,Angular的每次渲染都调用getter方法,属性计算极其简单。Rxjs的BehaviorSubject对象可以做这项任务。
在React生态中,Mobx的@computed装饰器,可以不改变对象的引用,可能是更好的API。
依赖注入
依赖注入是具有争议的特征,它违背了当前响应式范式的函数式编程和不可改变性。事实证明,某种依赖注入在数据绑定环境中几乎是不可或缺的。因为没有单独的数据层架构,它有助于解耦(和模拟数据与测试)。
依赖注入(DI)的另一个优势是,不同的存储库,能够拥有独立的生命周期。当前多数的响应范式使用某种全局应用状态,它们能映射到不同的组件。但,根据我的经验,在组件卸载时清理全局状态,太容易引入bug。
在组件挂载时创建一个存储库,并且该组件的子组件无缝地可用,这似乎是非常有用的,但常常被忽略。
脱离Angular的作用域,依赖注入很容易被mobx复制。
路由
基于组件的路由允许组件管理自己的子路由,而不需要一个大的全局路由器配置。这种方法最终在版本4中通过“react-router”实现了。
Material Design
使用高级组件构建项目,总是不错的。material design已经成为一种普遍接纳的默认选择,即使是在非谷歌项目中。
我特意选择了React工具箱,而不是通常推荐的Material UI,因为在内联CSS上,Material UI已经承认,有严重的 性能问题,他们计划在下一版本解决。
此外,React工具箱使用的Post/cssnext 已经开始取代Sass/LESS了。
局部作用CSS
CSS类名像全局变量。仅有几种可数的方法能防止CSS冲突,包括BEM。但,现今有一个清晰可见的趋势,用第三方库处理CSS防止冲突,而不需要前端开发者去处理CSS的命名空间。
表单校验
表单验证是一个非常重要且广泛使用的特性。推荐使用防止代码重复和错误的库。
项目生成器
使用cli生成器生成项目比从github克隆模板要方便一点。
相同的项目,构建两次
我们将用React和Angular创建相同的应用程序。没什么特别的,只是一个允许任何人将消息发布到公共页面的Shoutboard。
你可以在这里查看效果:
如果你想获取源码,可以从Github获取:
你会注意到,我们使用了TypeScript构建应用程序。TypeScript语法检测的优点很明显。现在, 随着TypeScript2引入了Imports优化内容、async/await和rest spread ,TypeScript2遗弃了Babel/ES7/Flow。
另外,因为我们要使用GraphQL,我们将 Apollo Client添加在示例项目中。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'
}
基础地,在应用程序使用的所有组件都需要添加到declarations,所有第三方库添加到imports,所有全局存储添加到providers。子组件可以访问所有这些内容,并有机会添加更多的本地内容。
React
const appStore = AppStore.getInstance()
const routerStore = RouterStore.getInstance()
const rootStores = {
appStore,
routerStore
}
ReactDOM.render(
<Provider {...rootStores} >
<Router history={routerStore.history} >
<App>
<Switch>
<Route exact path='/home' component={Home as any} />
<Route exact path='/posts' component={Posts as any} />
<Route exact path='/form' component={Form as any} />
<Redirect from='/' to='/home' />
</Switch>
</App>
</Router>
</Provider >,
document.getElementById('root')
)
该组件用于mobx中的依赖注入。它将存储保存到上下文,以便响应组件稍后可以注入它们。React上下文可以安全使用。
export class AppStore {
static instance: AppStore
static getInstance() {
return AppStore.instance || (AppStore.instance = new AppStore())
}
@observable username = 'Mr. User'
}
Angular的 Router是可注入的,所以它在任何地方都能使用,不限于组件。为了在React中获得同样的效果,我们使用 mobx-react-router包,并注入routerStore
。
总结:启动这两个应用程序非常简单。React有一个优势:更简单,使用imports替代modules。但是,正如将在后面看到的,modules可以非常方便。亲自制作单件有点麻烦。至于路由声明语法,JSON vs. JSX只是一个偏好问题。
Links and Imperative Navigation
有两种情况可以切换路由。声明式,使用“elements和 imperative”,直接调用路由API。
Angular
<h1> Shoutboard Application </h1>
<nav>
<a>Home</a>
<a>Posts</a>
</nav>
<router-outlet></router-outlet>
Angular路由器会自动检测到哪个“routerLink”是激活的,并在激活的元素上添加一个 “routerLinkActive”类名,这样它就可以附带相应的样式了。
路由使用特殊的“元素”来呈现当前路径所规定的任何内容。当我们深入研究应用程序的子组件时,可能会有很多的。
@Injectable()
export class FormService {
constructor(private router: Router) { }
goBack() {
this.router.navigate(['/posts'])
}
}
路由模块可以注入到任何服务中,private
将其存储在实例中,而无需显式分配。使用navigate
方法切换URLs。
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路由也可以在激活的链接上添加一个类名“activeClassName”。
在这里,我们不能直接提供类名称,因为css模块编译器使它变得唯一,我们需要使用style
helper。以后再说吧。
如上所见,React路由是嵌入在其它元素内的元素。由于元素只是包裹和挂载当前的路由,这意味着当前组件的子路由就是‘this.props.children’。这也是可以合成的。
export class FormStore {
routerStore: RouterStore
constructor() {
this.routerStore = RouterStore.getInstance()
}
goBack = () => {
this.routerStore.history.push('/posts')
}
}
mobx-router-store
包也可以注入和导航。
总结:两种路由方法是相当相似的。Angular似乎更直观,而React路由器有一些更直接的组合。
Dependency Injection
事实证明,将数据层与表示层分开是有益的。我们在这里试图使用依赖注入让数据层的组件(这里称为model/store/service)跟踪可视化组件的生命周期,从而创建一个或多个这样的组件的实例,而不需要接触全局状态。此外,还可以混合和匹配兼容的数据和可视化层。
本文中的示例非常简单,所以所有的依赖注入可能看起来都是多余的,但是随着应用程序的增长,它将变得非常有用。
Angular
@Injectable()
export class HomeService {
message = 'Welcome to home page'
counter = 0
increment() {
this.counter++
}
}
任何类都能使用’@inputable’装饰,然后它的属性和方法可以提供给组件。
@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
,因此在应用程序的生命周期,它是一个单例,并且对所有组件保持不变。利用组件控制服务的生命周期是一个非常有用的,但却没有得到充分的重视。
依赖注入将服务实例注册到组件的构造函数constructor
来工作,并由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的模板语法,可以说相当优雅。我喜欢’[()]'快捷方式,它像双向数据绑定一样工作,但它实际上是一个属性绑定 + 事件。按服务的生命周期所述,在我们每次从“/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
中,在每次挂载时,接受一个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替换对象中的属性,允许拦截调用。当调用@observable
增强组件的渲染函数时,调用那些属性的getter,并保持调用的引用。
然后,当修改属性值时,调用setter,调用应用这些属性的组件的渲染函数。现在,这些数据被更新,整个周期重新开始。
一个非常简单的机制,而且性能也很好。这里有深入的解释。
@inject
装饰器将appStore
和homeStore
的实例注入到HomeComponent
的props对象中。此时,这些存储都有不同的生命周期。在应用程序的生命周期中appStore
是不变的,但是在每一次导航到“/home”上时,homeStore
是新建的。
这样做的好处是不必手动清理属性。当存储都是全局的,如果路由是包含不同数据的详情页面,这是一个痛苦。
总结:在Angular依赖注入的特性下,provider生命周期管理更容易实现。React版本也是可用的,但涉及更多的样板。
计算属性
React
这个问题让我们以React开始,它有一个更直接的解决方案。
import { observable, computed, action } from 'mobx'
export class HomeStore {
import { observable, computed, action } from 'mobx'
export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
@computed get counterMessage() {
console.log('recompute counterMessage!')
return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit`
}
}
因此,我们有一个计算的属性,它绑定到counter
并返回一个正确的复数消息。缓存counterMessage
的结果,并只在counter
更改时重新计算.
<Input
type='text'
label='Edit your name'
name='username'
value={appStore.username}
onChange={appStore.onUsernameChange}
/>
<span>{homeStore.counterMessage}</span>
<button onClick={homeStore.increment}>Click!</button>
然后,我们引用JSX模板中的属性和increment
方法。表单控件的值绑定到一个变量驱动,并且用appStore
对象的方法来处理用户更新值的事件。
Angular
在Angular中,为了获得相同的效果,我们需要多做一点点创新。
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs/BehaviorSubject'
@Injectable()
export class HomeService {
message = 'Welcome to home page'
counterSubject = new BehaviorSubject(0)
// Computed property can serve as basis for further computed properties
counterMessage = new BehaviorSubject('')
constructor() {
// Manually subscribe to each subject that couterMessage depends on
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”可以做的不仅仅是这个,但这将是另一篇文章的主题。一个小缺陷:比起react示例,仅仅因为计算属性而使用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 subject。比起在组件中订阅的要求,这是一个很好的处理,简短得多。input
组件由[(ngModel)]
指令驱动。尽管看起来很奇怪,但它确实很优雅。[(ngModel)]
只是一个语法糖,将值绑定到appService.username
,用户触发输入事件自动更新表单值。
总结:与Angular/RxJS相比,在React/MobX中的计算属性更容易实现,但RxJS可能提供一些更有用的FRP特性,这些特性稍后可能会被欣赏。
模板与样式
为了显示模板如何相互层叠,让我们创建展示posts列表的PostsComponent
。
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加载posts。AppService
是应用程序中定义的单例,而AppService
是暂态的,在每次组件构造时,创建一个新实例。这个组件引用的CSS是针对这个组件的,这意味着该样式不能影响该组件之外的任何内容。
<a>
<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
的imports属性引入这些模块。*ngFor
指令用于遍历post重复生成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范围。
这种机制的优点是我们可以正常地引用类,而范围是在暗箱下处理的。
React
import * as style from './posts.css'
import * as appStyle from '../app.css'
@observer
export class Posts extends React.Component<any, any> {
postsStore: PostsStore
componentWillMount() {
this.postsStore = new PostsStore()
this.postsStore.initializePosts()
}
render() {
return <Provider postsStore={this.postsStore}>
<PostsComponent />
</Provider>
}
}
在React中,我们需要使用Provider
方法让PostsStore
依赖“transient”。我们还导入了CSS样式,引用为style
和appStyle
,以便能够使用JSX中那些CSS文件中的类。
interface PostsComponentProps {
appStore?: AppStore,
postsStore?: PostsStore
}
@inject('appStore', 'postsStore')
@observer
export class PostsComponent extends React.Component<PostsComponentProps, any> {
render() {
const { postsStore, appStore } = this.props
return <div>
<NavLink to='form'>
<Button icon='add' floating accent className={appStyle.floatRight} />
</NavLink>
<h3>Hello {appStore.username}</h3>
{postsStore.posts.map(post =>
<Card key={post.id} className={style.messageCard}>
<CardTitle
title={post.title}
subtitle={post.name}
/>
<CardText>{post.message}</CardText>
</Card>
)}
</div>
}
}
当然,JSX的JavaScript-y比Angular的HTML模板要多得多,这是好是坏,取决于你的品味。我们使用map
方法来迭代文章,而不是*ngFor
指令.
现在,Angular可能是最吹捧TypeScript的框架,但实际上,JSX才是TypeScript真正闪亮的平台。随着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类并对其进行增强。另一方面,这实际上是件好事,因为它迫使您正确地封装样式。
总结:由于代码完成和类型检查支持,我个人更喜欢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
是最重要的部分——它引用函数查询所有记录。这个名字是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结果创建接口。
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可观察对象,我们订阅它。它工作有点像promise,但不是很好,所以,不幸地,我们使用了async/await
。当然,还有 toPromise,但这似乎不是一个Angular 写法。我们设置了fetchPolicy: 'network-only'
,因为在这种情况下,我们不想缓存数据,而是每次都重新获取数据。
React
export class PostsStore {
appStore: AppStore
@observable posts: Array<Post> = []
constructor() {
this.appStore = AppStore.getInstance()
}
async initializePosts() {
const result = await this.appStore.apolloClient.query<PostsQueryResult>({
query: PostsQuery,
fetchPolicy: 'network-only'
})
this.posts = result.data.allPosts
}
}
React版本几乎是相同的,但是由于apolloClient
在这里使用promises,我们能使用async/await
语法。在React中还有一些其他的方法,它们只是将GraphQL查询“录制”到高阶组件上,但在我看来,它似乎把数据层和表示层混合得太紧密了。
总结:RxJS订阅与async/await的想法是完全相同的。
GraphQL—保存数据
通用代码
一些GraphQL相关代码:
const AddPostMutation = gql`
mutation AddPostMutation($name: String!, $title: String!, $message: String!) {
createPost(
name: $name,
title: $title,
message: $message
) {
id
}
}
`
mutations
的目的是创建或更新记录。因此,用mutation
声明一些变量是有帮助地,因为这是将数据传递到其中的方法。所以,我们有name
, title
, 和message
变量,类型为String
,我们每次mutation声明时都需要填充这些变量。同样, createPost
函数也是由Graphcool定义的。我们指定Post
模型的键绑定来自外部变化变量的值,并且我们希望只发送新创建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
时,我们需要提供我们调用的变化和变量。我们得到subscribe
回调的结果,并使用注入的router
导航回post列表。
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')
}
}
和上面非常相似,有更多的“手动”依赖注入的区别,以及async/await
的用法。
总结:同样,这里没有什么区别。subscribe与async/await从基本上是不同的。
Forms
我们希望在此应用程序中使用表格来实现以下目标:
-
字段与模型的数据绑定
-
每个字段的验证消息,多个规则
-
支持检查整个表格是否有效
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
/>
<Button
label='Submit'
onClick={formStore.submit}
raised
disabled={postFormState.form.hasError}
primary
/>
</div>
}
}
当form.hasError
是true
时,我们保持按钮不可用。“提交”按钮将表单提交给前面出现的GraphQL mutation。
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'
}
}
首先,让我们定义验证消息。 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)]
],
})
}
使用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 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>
最重要的事情是引用我们用FormBuilder创建的formGroup,即在模板中用[formGroup]="postForm"
分配的。表单上的字段通过formControlName
属性绑定到表单数据上。当表单无效时,我们禁用“提交”按钮。我们还需要添加脏检查,因为在这里,非肮脏的表单仍然是无效的。我们希望按钮的初始状态被“启用”。
总结:验证和模板方面,React和Angular这种形式的方法是相当不同的。Angular的方法涉及更多的“魔术”,而不是直截了当的绑定,但另一方面,更完整和彻底。
打包尺寸
使用应用程序生成器的默认设置:特别是React的Tree Shaking和Angular AOT编译,压缩的JS生产包大小:
-
Angular: 1200 KB
-
React: 300 KB
好吧,这没什么好惊讶的。Angular一直是更大的一个。
使用gzip时,尺寸分别降至275kb和127kb。
记住,这里基本上都是供应商库。相比之下,实际应用程序代码的数量是最小的,在real-world应用程序中并非如此。在那里,这个比率可能是1:2,而不是1:4。另外,当你开始包含许多具有React的第三方库时,包大小也会增长很快。
库的灵活性 vs 框架的稳健性
关于Angular或React是否更适合web开发,看来我们还是不能找到一个的明确答案。
结果表明,依赖于我们使用React的库,React和Angular上的开发工作流可能非常相似。这主要是个人偏好的问题。
如果你喜欢现成的堆栈,强大的依赖注入和计划使用一些RxJS的好东西,选择Angular。
如果你喜欢自己组装和构建堆栈,那么你喜欢JSX的直截了当,并且更喜欢更简单的可计算属性,选择React/MobX。
或者,如果你喜欢完整的,RealWorld例子:
先选择你的编程范式
用React/Mobx编程实际上比用React/Redux编程更接近Angular。虽然在模板和依赖关系管理方面存在一些显著的差异,但它们具有相同的可变数据绑定范式。
具有不变或单一范式的React/Redux是一个完全不同的模式。
别被Redux库的小巧给愚弄了。它可能很小,但却是一个框架。Redux的多数最佳实践都集中在应用Redux兼容的库,如Redux Saga用于同步代码和数据提取,Redux From用于表单管理,Reselect 用于记忆选择器(Redux的计算值),Recompose 包括其他库用于生命周期管理。在Redux社区中,从Immutable.js 到 Ramda 或 lodash/fp,存在一个工具使用普通的js对象而不是转换它们。
Redux一个很好的例子是众所周知的 React Boilerplate。它是一个强大的开发栈,但是如果你看看它,就会发现它与我们在这篇文章中看到的其它东西非常不同。
从javascript社区中较活跃的部分来看,我觉得Angular受到了不公平的对待。许多对它表示不满的人可能不会欣赏到在古老的AngularJS和今天的Angular之间发生的巨大变化。在我看来,这是一个非常干净和富有成效的框架,如果它出现在1-2年前,它将会给世界带来风暴。
然而,Angular正在夺取坚实的立足点,特别是在拥有大团队,需要标准化和长期支持的企业界。或者用另一种方式来说,如果还有什么意义的话,Angular是谷歌工程师认为的web应该如何开发的一种模式。
对于mobx,也适用类似的评估。本身很好,但没得到充分的赞赏。
最后:在选择React和Angular之前,先选择你的编程范式。
可变/数据绑定,或不变/单向,这似乎是真正的问题。
译者:众成翻译
热门评论
非常好