Hay innumerables artículos por ahí debatiendo si React o Angular es la mejor opción para el desarrollo web. ¿Necesitamos otro?
La razón por la que escribí este artículo es porque ninguno de los artículos ya publicados –aunque contienen grandes ideas– van en profundidad para un práctico desarrollador de front-end para decidir cuál puede satisfacer sus necesidades.
En este artículo, aprenderás cómo Angular y React apuntan a resolver problemas front-end similares aunque con filosofías muy diferentes, y si elegir uno o el otro es simplemente una cuestión de preferencia personal. Para compararlos, vamos a construir la misma aplicación dos veces, una vez con Angular y luego otra vez con React.
Anuncio Intempestivo de Angular
Hace dos años, escribí un artículo sobre React Ecosystem. Entre otros puntos, el artículo argumentaba que Angular había sido víctima de la “muerte por el anuncio previo”. En aquel entonces, la elección entre Angular y casi cualquier otra cosa era fácil para cualquiera que no quisiera que su proyecto funcionara con un marco obsoleto. Angular 1 era obsoleto, y Angular 2 ni siquiera estaba disponible en versión alfa.
En retrospectiva, los temores eran más o menos justificados. Angular 2 cambió dramáticamente e incluso pasó por una reescritura importante justo antes del lanzamiento final.
Dos años más tarde, tenemos Angular 4 con una promesa de estabilidad relativa de aquí en adelante.
¿Y ahora qué?
Angular vs. Reaccionar: Comparando Manzanas y Naranjas
Algunas personas dicen que comparar React y Angular es como comparar las manzanas con las naranjas. Mientras que uno es una biblioteca que se ocupa de opiniones, el otro es un framework completo.
Por supuesto, la mayoría de los Desarrolladores React añadirán algunas librerías a React para convertirlo en un framework completo. Por otra parte, el flujo de trabajo resultante de esta pila a menudo sigue siendo muy diferente de Angular, por lo que la comparabilidad sigue siendo limitada.
La mayor diferencia radica en la administración estatal. Angular viene con el enlace de datos incluido, mientras que React hoy en día suele ser aumentado por Redux para proporcionar flujo de datos unidireccional y trabajar con datos inmutables. Esos son enfoques opuestos en su propio derecho, y las discusiones incontables ahora están encendiendo si la unión mutable/de datos es mejor o peor que inmutable/unidireccional.
Un Campo de Juego Nivelado
Como React es famoso más fácil de hackear, he decidido, con el propósito de esta comparación, construir una configuración de React que refleja Angular razonablemente cerca para permitir la comparación de lado a lado de fragmentos de código.
Algunas características angulares que se destacan pero no se encuentran en Reaccionar por defecto son:
Feature | Angular package | React library |
---|---|---|
Data binding, dependency injection (DI) | @angular/core | MobX |
Computed properties | rxjs | MobX |
Component-based routing | @angular/router | React Router v4 |
Material design components | @angular/material | React Toolbox |
CSS scoped to components | @angular/core | CSS modules |
Form validations | @angular/forms | FormState |
Project generator | @angular/cli | React Scripts TS |
Enlace de Datos
La vinculación de datos es discutiblemente más fácil de comenzar que el enfoque unidireccional. Por supuesto, sería posible ir en dirección completamente opuesta, y usar Redux o mobx-state-tree con React, y ngrx con Angular.
Propiedades Calculadas
Mientras que el funcionamiento se refiere, los adquiridores llanos en Angular están simplemente fuera de la pregunta mientras que se llaman en cada procesamiento. Es posible utilizar BehaviorSubject de RsJS, que realiza el trabajo.
Con React, es posible usar @computed de MobX, que logra el mismo objetivo, con una API algo mejor.
Inyección de Dependencia
La inyección de dependencia es un poco polémica porque va en contra del actual paradigma React de programación funcional e inmutabilidad. Como resulta, algún tipo de inyección de dependencia es casi indispensable en entornos de enlace de datos, ya que ayuda con el desacoplamiento (y, por lo tanto, burla y pruebas) donde no hay una arquitectura de capa de datos independiente.
Una ventaja más de DI (apoyado en Angular) es la capacidad de tener diferentes ciclos de vida de diferentes tiendas. La mayoría de los paradigmas actuales de React utilizan algún tipo de estado de aplicación global que mapea diferentes componentes, pero por mi experiencia, es demasiado fácil introducir errores al limpiar el estado global en el desmontaje de componentes.
Tener una tienda que se crea en el montaje de componentes (y estar perfectamente disponible para los hijos de este componente) parece ser realmente útil, ya menudo se pasa por alto el concepto.
Fuera de la caja en Angular, pero muy fácilmente reproducible con MobX también.
Enrutamiento
El enrutamiento basado en componentes permite a los componentes gestionar sus propias rutas secundarias en lugar de tener una configuración de enrutador global grande. Este enfoque finalmente lo ha hecho a
react-router
en la versión 4.Material Design
Siempre es bueno comenzar con algunos componentes de alto nivel, y el diseño de materiales se ha convertido en algo así como una opción predeterminada universalmente aceptada, incluso en proyectos que no son de Google.
He elegido deliberadamente React Toolbox sobre el recomendado Material UI, ya que la interfaz de usuario de material tiene serias auto-confesados problemas de rendimiento con su CSS-en-línea, que planean resolver en la próxima versión.
Además, PostCSS/cssnext fue utilizado en React Toolbox y está empezando a reemplazar Sass/LESS de todos modos.
CSS con Alcance
Las clases CSS son algo así como las variables globales. Existen numerosos enfoques para la organización de CSS para evitar conflictos (incluyendo BEM), pero hay una clara corriente tendencia en el uso de bibliotecas que ayudan a procesar CSS para evitar esos conflictos sin la necesidad de un desarrollador front-end para elaborar elaborados sistemas de nombres CSS.
Validación de Formularios
Las validaciones de formulario son una característica no trivial y muy ampliamente utilizada. Es bueno tener los cubiertos por una librería para evitar la repetición de código y errores.
Generador de Proyecto
Tener un generador CLI para un proyecto es sólo un poco más conveniente que tener que clonar chapas de calderas de GitHub.
La Misma Aplicación, Construída Dos Veces
Así que vamos a crear la misma aplicación en React y Angular. Nada espectacular, solo un Shoutboard que permite a cualquiera publicar mensajes en una página común.
Puedes probar las aplicaciones aquí:
Si deseas tener todo el código fuente, puedes obtenerlo desde GitHub:
Observará que también hemos utilizado TypeScript para la aplicación React. Las ventajas de la comprobación de tipos en TypeScript son evidentes. Y ahora, con un mejor manejo de las importaciones, async/espera y propagación de descanso finalmente llegaron a TypeScript 2, y deja a Babel/ES7/Flow en el polvo.
Además, vamos a agregar Apollo Client a ambos porque queremos usar GraphQL. Quiero decir, REST es genial, pero después de una década aproximadamente, se hace viejo.
Bootstrap y Enrutamiento
Primero, echemos un vistazo a los puntos de entrada de ambas aplicaciones.
Angular
{ 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'
}
Básicamente, todos los componentes que queremos utilizar en la aplicación necesitan ir a declaraciones. Todas las librerías de terceros a las importaciones y todas las tiendas globales a los proveedores. Los componentes de los hijos tienen acceso a todo esto, con la oportunidad de agregar más cosas locales.
React
const appStore = AppStore.getInstance()
const routerStore = RouterStore.getInstance()
const rootStores = {
appStore,
routerStore
}
ReactDOM.render(
<provider rootstores="">
<router history="{routerStore.history}">
<app>
<switch>
<route any="" as="" component="{Home" exact="" path="/home">
<route any="" as="" component="{Posts" exact="" path="/posts">
<route any="" as="" component="{Form" exact="" path="/form">
<redirect from="/" to="/home">
</redirect></route></route></route></switch>
</app>
</router>
</provider>,
document.getElementById('root')
)
const routerStore = RouterStore.getInstance()
const rootStores = {
appStore,
routerStore
}
ReactDOM.render(
<provider rootstores="">
<router history="{routerStore.history}">
<app>
<switch>
<route any="" as="" component="{Home" exact="" path="/home">
<route any="" as="" component="{Posts" exact="" path="/posts">
<route any="" as="" component="{Form" exact="" path="/form">
<redirect from="/" to="/home">
</redirect></route></route></route></switch>
</app>
</router>
</provider>,
document.getElementById('root')
)
El componente
<Provider/>
se utiliza para la inyección de dependencia en MobX. Guarda las tiendas en el contexto para que los componentes de React puedan inyectarlas posteriormente. Sí, el contexto de React puede (discutiblemente) ser utilizado con seguridad.
La versión de React es un poco más corta porque no hay declaraciones de módulo - por lo general, es sólo importar y ya está listo para usar. A veces este tipo de dependencia dura no es deseado (en pruebas), por lo que para las tiendas singleton globales, tuve que usar este GoF patrón de vieja década:
export class AppStore {
static instance: AppStore
static getInstance() {
return AppStore.instance || (AppStore.instance = new AppStore())
}
@observable username = 'Mr. User'
}
static instance: AppStore
static getInstance() {
return AppStore.instance || (AppStore.instance = new AppStore())
}
@observable username = 'Mr. User'
}
Angular’s Router es inyectable, por lo que puede ser utilizado desde cualquier lugar, no sólo los componentes. Para lograr lo mismo en reaccionar, usamos el paquete mobx-react-router e inyectamos el
routerStore
.
Resumen: Hacer bootstrapping en ambas aplicaciones es bastante sencillo. React tiene una ventaja que es más simple, usando sólo importaciones en lugar de módulos, pero, como veremos más adelante, esos módulos pueden ser muy útiles. Hacer semifallos manualmente es un poco molesto. En cuanto a la sintaxis de la declaración de enrutamiento, JSON vs JSX es sólo una cuestión de preferencia.
Enlaces y Navegación Imperativa
Así que hay dos casos para cambiar una ruta. Declarativamente, utilizando elementos
<a href...>
, e imperativamente, llamando directamente a la API de enrutamiento (y, por tanto, de ubicación).
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 detecta automáticamente qué
routerLink
está activo, y pone una clase apropiadarouterLinkActive
en ella, para que pueda ser estilizada.
El enrutador utiliza el elemento especial `, mientras profundizamos en los subcomponentes de la aplicación.
<router-outlet>
para representar cualquier ruta de acceso actual. Es posible tener muchos ‘@Injectable()
export class FormService {
constructor(private router: Router) { }
goBack() {
this.router.navigate(['/posts'])
}
}
El módulo de enrutador puede ser inyectado a cualquier servicio (mitad-mágicamente por su tipo TypeScript), la declaración
private
lo almacena en la instancia sin necesidad de asignación explícita. Utilice el método navigate
para cambiar las 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 también puede establecer la clase de enlace activo con
activeClassName
.
Aquí no podemos proporcionar el nombre de la clase directamente porque se ha hecho único por el compilador de módulos CSS, y tenemos que usar el ayudante
style
. Hablaremos sobre eso más tarde.
Como se ha visto anteriormente, React Router utiliza el elemento
<Switch>
dentro de un elemento <App>
. Como el elemento <Switch>
simplemente envuelve y monta la ruta actual, significa que las sub-rutas del componente actual son simplemente this.props.children
. Así que es componible también.export class FormStore {
routerStore: RouterStore
constructor() {
this.routerStore = RouterStore.getInstance()
}
goBack = () => {
this.routerStore.history.push('/posts')
}
}
El paquete
mobx-router-store
también permite una fácil inyección y navegación.
Resumen: Ambos enfoques de enrutamiento son bastante comparables. Angular parece ser más intuitivo, mientras que React Router tiene un poco más fácil de componer.
Inyección de Dependencia
Ya se ha demostrado que es beneficioso separar la capa de datos de la capa de presentación. Lo que estamos tratando de lograr con DI es hacer que los componentes de las capas de datos (aquí denominados modelo/tienda/servicio) sigan el ciclo de vida de los componentes visuales y, por lo tanto, permitan hacer una o varias instancias de dichos componentes sin necesidad de tocar globales estado. Además, debería ser posible mezclar y combinar datos compatibles y capas de visualización.
Ejemplos en este artículo son muy simples, por lo que todas las cosas DI puede parecer exceso, pero viene muy bien a medida que crece la aplicación.
Angular
@Injectable()
export class HomeService {
message = 'Welcome to home page'
counter = 0
increment() {
this.counter++
}
}
Así que cualquier clase se puede hacer
@inyectable
, y sus propiedades y métodos puestos a disposición de los componentes.@Component({
selector: 'app-home',
templateUrl: './home.component.html',
providers: [
HomeService
]
})
export class HomeComponent {
constructor(
public homeService: HomeService,
public appService: AppService,
) { }
}
Al registrar el
HomeService
aproviders
del componente, lo ponemos a disposición de este componente exclusivamente. No es un semifallo ahora, pero cada instancia del componente recibirá una nueva copia, fresca en el montaje del componente. Eso significa que no hay datos obsoletos de uso previo.
Por el contrario, el
AppService
se ha registrado en elapp.module
(ver arriba), por lo que es un semifallo y se mantiene igual para todos los componentes, aunque la duración de la aplicación. Ser capaz de controlar el ciclo de vida de los servicios de los componentes es un concepto muy útil, pero poco apreciado.
DI funciona asignando las instancias de servicio al constructor del componente, identificadas por tipos TypeScript. Además, las palabras clave
public
asignan automáticamente los parámetros athis
, de modo que ya no necesitamos escribir esas líneas aburridas 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>
Sintaxis de la plantilla de Angular, sin duda bastante elegante. Me gusta el atajo
[()]
, que funciona como un enlace de datos de 2 vías, pero bajo el capó, en realidad es un atributo vinculante + evento. Como dicta el ciclo de vida de nuestros servicios, homeService.counter
va a reiniciarse cada vez que naveguemos lejos de/home
, pero el appService.username
permanece, y es accesible desde cualquier lugar.
React
import { observable } from 'mobx'
export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
}
Con MobX, necesitamos agregar el decorador
@observable
a cualquier propiedad que queramos hacer 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>
}
}
Para gestionar el ciclo de vida correctamente, necesitamos hacer un poco más de trabajo que en el ejemplo Angular. Envolvemos el
HomeComponent
dentro de unProvider
, que recibe una nueva instancia de HomeStore
en cada montaje.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
utiliza el decorador @observer
para escuchar los cambios en las propiedades @observable
.
El mecanismo bajo-la-capucha de esto es bastante interesante, así que vamos a pasar por esto brevemente aquí. El decorador
@observable
reemplaza una propiedad en un objeto con getter y setter, lo que le permite interceptar llamadas. Cuando se llama a la función render de un componente aumentado @observador
, las propiedades getters se llaman y mantienen una referencia al componente que las llama.
A continuación, cuando se llama a setter y se cambia el valor, se llaman las funciones render de los componentes que utilizaron la propiedad en el último procesamiento. Ahora, los datos sobre qué propiedades se utilizan donde se actualizan, y todo el ciclo puede comenzar de nuevo.
Un mecanismo muy simple, y bastante performant también. Más explicación en profundidad aquí.
El decorador
@inyectar 'se utiliza para inyectar instancias
appStorey
homeStoreen los accesorios de
HomeComponent. En este punto, cada una de esas tiendas tiene un ciclo de vida diferente.
appStorees el mismo durante la vida de la aplicación, pero
homeStore` es recién creado en cada navegación a la ruta “/home”.
La ventaja de esto es que no es necesario limpiar las propiedades manualmente, como ocurre cuando todas las tiendas son globales, lo cual es doloroso si la ruta es alguna página de “detalle” que contiene datos completamente diferentes cada vez.
Resumen: Como la gestión del ciclo de vida del proveedor en una característica inherente de la DI de Angular, es, por supuesto, más simple lograrlo allí. La versión de React es también utilizable pero implica mucho más boilerplate.
Propiedades Calculadas
React
Comencemos con Reaccionar en éste, tiene una solución más directa.
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`
}
}
Así que tenemos una propiedad calculada que se une a
counter
y devuelve un mensaje correctamente pluralizado. El resultado de counterMessage
se almacena en caché y se vuelve a calcular sólo cuandocounter
cambia.<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>
Luego, hacemos referencia a la propiedad (y el método
increment
) de la plantilla JSX. El campo de entrada se controla mediante la vinculación a un valor y permite que un método de `appStore ‘maneje el evento de usuario.
Angular
Para lograr el mismo efecto en Angular, necesitamos ser un poco más inventivos.
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)
}
}
Necesitamos definir todos los valores que sirven como base para una propiedad calculada como un
BehaviorSubject
. La propia propiedad calculada es también un BehaviorSubject
, porque cualquier propiedad calculada puede servir como entrada para otra propiedad calculada.
Por supuesto,
RxJS
puede hacer mucho más que sólo esto, pero eso sería un tema para un artículo completamente diferente. El inconveniente menor es que este uso trivial de RxJS para las propiedades calculadas es un poco más detallado que el ejemplo reactivo, y es necesario administrar las suscripciones manualmente (como aquí en el constructor).<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>
Tenga en cuenta cómo podemos hacer referencia al sujeto RxJS con el
|asíncrona
. Eso es un toque agradable, mucho más corto que la necesidad de suscribirse en sus componentes. El componente input
es impulsado por la directiva [(ngModel)]
. A pesar de mirar extraño, es realmente bastante elegante. Sólo un azúcar sintáctico para la vinculación de datos de valor a appService.username
y el valor de asignación automática del evento de entrada del usuario.
Resumen: Las propiedades calculadas son más fáciles de implementar en React/MobX que en Angular/RxJS, pero RxJS podría proporcionar algunas características de FRP más útiles, que podrían apreciarse más tarde.
Plantillas y CSS
Para mostrar cómo las pilas de plantillas entre sí, vamos a utilizar el componente de mensajes que muestra una lista de mensajes.
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()
}
}
Este componente sólo conecta HTML, CSS y servicios inyectados y también llama a la función para cargar los mensajes de la API en la inicialización.
AppService
es un semifallo definido en el módulo de aplicación, mientras quePostsService
es transitorio, con una instancia nueva creada en cada componente de tiempo creado. El CSS que se hace referencia desde este componente tiene un ámbito de este componente, lo que significa que el contenido no puede afectar a nada fuera del componente.<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>
En la plantilla HTML, hacemos referencia principalmente a componentes de Material Angular. Para tenerlos disponibles, era necesario incluirlos en las importaciones
app.module
(véase más arriba). La directiva *ngFor
se utiliza para repetir el componente md-card
para cada puesto.
Local CSS:
.mat-card {
margin-bottom: 1rem;
}
El CSS local sólo aumenta una de las clases presentes en el componente
md-card
.
Global CSS:
.float-right {
float: right;
}
Esta clase se define en el archivo global
style.css
para que esté disponible para todos los componentes. Se puede hacer referencia en la forma estándar, class =" float-right "
.
Compiled CSS:
.float-right {
float: right;
}
.mat-card[_ngcontent-c1] {
margin-bottom: 1rem;
}
En CSS compilado, podemos ver que el CSS local ha sido delimitado al componente renderizado mediante el selector de atributos
[_ngcontent-c1]
. Cada componente Angular representado tiene una clase generada como ésta para fines de alcance CSS.
La ventaja de este mecanismo es que podemos referenciar clases normalmente, y el alcance se maneja “bajo el capó”.
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>
}
}
En React, de nuevo, necesitamos usar el método
Provider
para hacer que la dependencia de PostsStore sea “transitoria”. También importamos estilos CSS, denominados style
y appStyle
, para poder utilizar las clases de esos archivos CSS en JSX.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>
}
}
Naturalmente, JSX se siente mucho más JavaScript-y que las plantillas HTML de Angular, que puede ser una buena o mala cosa dependiendo de sus gustos. En lugar de la directiva
*ngFor
, usamos el constructomap
para iterar sobre los posts.
Ahora bien, Angular podría ser el marco que promociona a TypeScript más, pero en realidad es JSX donde TypScript realmente brilla. Con la adición de módulos CSS (importados anteriormente), realmente convierte la codificación de su plantilla en zen de código. Cada cosa es revisada. Componentes, atributos, incluso clases CSS (
appStyle.floatRight
ystyle.messageCard
, ver más abajo). Y, por supuesto, la naturaleza delgada de JSX fomenta la división en componentes y fragmentos un poco más que las plantillas de Angular.
Local CSS:
.messageCard {
margin-bottom: 1rem;
}
Global CSS:
.floatRight {
float: right;
}
Compiled CSS:
.floatRight__qItBM {
float: right;
}
.messageCard__1Dt_9 {
margin-bottom: 1rem;
}
Como puede ver, el cargador de CSS Módulos postfixes cada clase CSS con un postfix aleatorio, que garantiza la singularidad. Una forma sencilla de evitar conflictos. A continuación, las clases se hacen referencia a través de los objetos importados del paquete web. Un inconveniente posible de esto puede ser que no se puede crear un CSS con una clase y aumentarlo, como lo hicimos en el ejemplo Angular. Por otro lado, esto puede ser realmente una buena cosa, porque le obliga a encapsular estilos correctamente.
Resumen: Personalmente, me gusta JSX un poco mejor que las plantillas Angular, sobre todo debido a la terminación del código y el tipo de control de apoyo. Eso realmente es una característica asesina. Angular ahora tiene el compilador AOT, que también puede detectar algunas cosas, la finalización de código también funciona para alrededor de la mitad de las cosas allí, pero no es casi tan completo como JSX/TypeScript.
GraphQL - Cargando Datos
Así que hemos decidido utilizar GraphQL para almacenar datos para esta aplicación. Una de las maneras más fáciles de crear el back-end de GraphQL es usar algún BaaS, como Graphcool. Así que eso es lo que hicimos. Básicamente, sólo define modelos y atributos, y su CRUD es bueno para ir.
Código Común
Como parte del código relacionado con GraphQL es 100% igual para ambas implementaciones, no lo repetimos dos veces:
const PostsQuery = gql`
query PostsQuery {
allPosts(orderBy: createdAt_DESC, first: 5)
{
id,
name,
title,
message
}
}
`
GraphQL es un lenguaje de consulta dirigido a proporcionar un conjunto más rico de funcionalidad en comparación con los puntos finales RESTful clásicos. Vamos a diseccionar esta consulta en particular.
PostsQuery
es sólo un nombre para esta consulta a la referencia posterior, se puede nombrar nada.allPosts
es la parte más importante - hace referencia a la función para consultar todos los registros con el modelo `Post`. Este nombre fue creado por Graphcool.orderBy
yfirst
son parámetros de la funciónallPosts
.createdAt
es uno de losPost
atributos del modelo.first: 5
significa que devolverá sólo los primeros 5 resultados de la consulta.id
,name
,title
, ymessage
son los atributos del modeloPost
que queremos ser incluidos en el resultado. Otros atributos serán filtrados.
Como ya puedes ver, es bastante potente. Echa un vistazo a esta página para familiarizarse más con las consultas de GraphQL.
interface Post {
id: string
name: string
title: string
message: string
}
interface PostsQueryResult {
allPosts: Array<Post>
}
Sí, como buenos ciudadanos de TypeScript, creamos interfaces para los resultados de 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
})
}
}
La consulta GraphQL es un RxJS observable, y nos suscribimos a ella. Funciona un poco como una promesa, pero no es así, por lo que no tenemos suerte con
async/await
. Por supuesto, todavía hay toPromise, pero no parece ser el camino Angular de todas formas. Establecemos fetchPolicy: 'network-only'
porque en este caso, no queremos almacenar en caché los datos, sino refetch cada vez.
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
}
}
La versión de React es casi idéntica, pero como
apolloClient
aquí usa promesas, podemos aprovechar la sintaxisasync / await
. Existen otros enfoques en React que sólo “graban” las consultas GraphQL a componentes de orden superior, pero me pareció mezclar la capa de datos y presentación a tad demasiado.
En resúmen: Las ideas de la RxJS suscribirse vs async/await son realmente lo mismo.
GraphQL - Guardando Datos
Código Común
Una vez más, algo de código GraphQL relacionado:
const AddPostMutation = gql`
mutation AddPostMutation($name: String!, $title: String!, $message: String!) {
createPost(
name: $name,
title: $title,
message: $message
) {
id
}
}
`
El propósito de las mutaciones es crear o actualizar registros. Por lo tanto, es beneficioso para declarar algunas variables con la mutación, ya que son la forma de cómo pasar los datos en ella. Así que tenemos las variables
name
,title
y message
, escritas como String
, que necesitamos llenar cada vez que llamemos a esta mutación. La función createPost
, de nuevo, está definida por Graphcool. Especificamos que las claves del modelo Post
tendrán valores de las variables de mutación de salida, y también que queremos que elid
del Post recién creado sea enviado a cambio.
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)
})
}
}
Cuando llamamos
apollo.mutate
, necesitamos proporcionar la mutación que llamamos y las variables también. Obtenemos el resultado en la función de devolución de llamada subscribe
y usamos el` roteador ‘inyectado para navegar de nuevo a la lista de correos.
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')
}
}
Muy similar a lo anterior, con la diferencia de más “manual” la inyección de dependencia, y el uso de
async/await
.
En resúmen: Una vez más, no hay mucha diferencia aquí. suscribirse vs async / await es básicamente todo lo que difiere.
Formularios
Queremos lograr los siguientes objetivos con los formularios de esta aplicación:
- Enlace de datos de campos a un modelo
- Mensajes de validación para cada campo, varias reglas
- Soporte para comprobar si todo el formulario es válido
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
})
}
Por lo tanto, la librería formstate funciona de la siguiente manera: para cada campo de su formulario, define un
FieldState
. El parámetro pasado es el valor inicial. La propiedad validators
toma una función, que devuelve “false” cuando el valor es válido, y un mensaje de validación cuando el valor no es válido. Con las funciones de check' y
checkRequired`, todo puede parecer muy declarativo.
Para tener la validación de toda la forma, es beneficioso también envolver esos campos con una instancia
FormState
, que a continuación, proporciona la validez agregada.@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}
/>
La instancia
FormState
proporciona propiedades value
, onChange
y error
, que se pueden utilizar fácilmente con cualquier componente front-end. <Button
label='Cancel'
onClick={formStore.goBack}
raised
accent
/>
<Button
label='Submit'
onClick={formStore.submit}
raised
disabled={postFormState.form.hasError}
primary
/>
</div>
}
}
Cuando
form.hasError
estrue
, mantenemos el botón deshabilitado. El botón Enviar envía el formulario a la mutación GraphQL presentada anteriormente.
Angular
En Angular, vamos a utilizar
FormService
y FormBuilder
, que son partes del paquete @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)]
],
})
}
Usando
FormBuilder
, es muy fácil crear la estructura del formulario, aún más sucintamente que en el ejemplo 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
}
Para obtener mensajes de validación vinculables en el lugar correcto, necesitamos realizar algún procesamiento. Este código se toma de la documentación oficial, con algunos pequeños cambios. Básicamente, en FormService, los campos guardan referencia sólo a errores activos, identificados por nombre de validador, por lo que necesitamos emparejar manualmente los mensajes requeridos a los campos afectados. Esto no es totalmente un inconveniente; por ejemplo, se presta más fácilmente a la internacionalización.
onSubmit({ value, valid }) {
if (!valid) {
return
}
this.formService.addPost(value)
}
onCancel() {
this.router.navigate(['/posts'])
}
}
Una vez más, cuando el formulario es válido, los datos pueden ser enviados a la mutación 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>
Lo más importante es hacer referencia al formGroup que hemos creado con FormBuilder, que es la asignación
[formGroup] = 'postForm'
. Los campos dentro del formulario están enlazados al modelo de formulario a través de la propiedad formControlName
. Nuevamente, desactivamos el botón “Enviar” cuando el formulario no es válido. También debemos agregar el check sucio, porque aquí, el formulario no sucio puede ser inválido. Queremos que el estado inicial del botón esté “habilitado”.
En resúmen: Este enfoque de las formas en React y Angular es bastante diferente en los frentes de validación y de plantilla. El enfoque Angular implica un poco más de “magia” en lugar de un enlace directo, pero, por otro lado, es más completo.
Tamaño del Lote
Oh, una cosa más. La producción redujo los tamaños de paquetes de JS, con ajustes por defecto de los generadores de aplicación: notablemente Tree Shaking en React y AOT compilación en Angular.
- Angular: 1200 KB
- React: 300 KB
Bueno, no hay mucha sorpresa aquí. Angular siempre ha sido el más voluminoso.
Cuando se utiliza gzip, los tamaños bajan a 275kb y 127kb respectivamente.
Sólo ten en cuenta que estas son básicamente todas las librerías de proveedores. La cantidad de código de aplicación real es mínima por comparación, lo que no es el caso en una aplicación del mundo real. Allí, la proporción sería probablemente más parecida a 1:2 que 1:4. Además, cuando empieces a incluir muchas librerías de terceros con React, el tamaño del paquete también tiende a crecer con bastante rapidez.
Flexibilidad de las bibliotecas frente a la robustez del marco
Así que parece que no hemos sido capaces (¡de nuevo!) para dar una respuesta clara sobre si Angular o React es mejor para el desarrollo web.
Resulta que los flujos de trabajo de desarrollo en React y Angular pueden ser muy similares, dependiendo de las bibliotecas que elegimos para usar React with. Entonces es principalmente una cuestión de preferencia personal.
Si te gustan las pilas ya hechas, la poderosa inyección de dependencia y el plan para usar algunas golosinas de RxJS, eligió Angular.
Si te gusta jugar y construir tu propia pila, te gusta la simplicidad de JSX y prefieres propiedades computables más simples, selecciona React / MobX.
O, si prefieres ejemplos más grandes, y de RealWorld:
Primero, Elige tu Paradigma de Programación
La programación con React/MobX es en realidad más similar a Angular que con React/Redux. Hay algunas diferencias notables en las plantillas y el manejo de dependencias, pero tienen el mismo paradigma mutable/data binding.
React / Redux con su paradigma inmutable/unidireccional es una bestia completamente diferente.
No te dejes engañar por la pequeña huella de la librería de Redux. Puede ser pequeña, pero es un framework de todos modos. La mayoría de las mejores prácticas de Redux hoy se centran en el uso de librerías compatibles con redux, como Redux Saga para el código asíncrono y la búsqueda de datos, Redux Form para la gestión de formularios, Reselect para selectores memorizados (valores calculados de Redux). y Recompose, entre otros, para una gestión del ciclo de vida más fina. Además, hay un cambio en la comunidad Redux de Immutable.js a Ramda o lodash/fp, que funcionan con objetos JS sencillos en lugar de convertirlos.
Un buen ejemplo de Redux moderno es el bien conocido React Boilerplate. Es una pila de desarrollo formidable, pero si lo echas un vistazo, es realmente muy, muy diferente de todo lo que hemos visto en este post hasta ahora.
Siento que Angular está recibiendo un poco de tratamiento injusto de la parte más vocal de la comunidad de JavaScript. Muchas personas que expresan su insatisfacción con ella probablemente no aprecian el cambio inmenso que ocurrió entre el antiguo AngularJS y Angular de hoy. En mi opinión, es un marco muy limpio y productivo que tomaría el mundo por la tormenta si hubiera aparecido 1-2 años antes.
Sin embargo, Angular está ganando un punto de apoyo sólido, especialmente en el mundo corporativo, con grandes equipos y necesidades de estandarización y soporte a largo plazo. O para decirlo de otra manera, Angular es la forma en que los ingenieros de Google piensan que el desarrollo web debe hacerse, si eso sigue siendo así.
En cuanto a MobX, se aplica una evaluación similar. Realmente genial, pero no se aprecia mucho.
En conclusión: antes de elegir entre React y Angular, elige primero tu paradigma de programación.
mutable/enlace de datos ó inmutable/unidireccional, ese parece ser el problema real.
No hay comentarios:
Publicar un comentario