AngularJS
con estilo
@gruizdevilla
@adesis
Meetup AngularJS Madrid
23 de junio de 2015
¿Por qué
un buen estilo?
Leemos 100 veces,
corregimos 10 veces
por cada vez que escribimos código
Necesitamos comunicar de forma eficiente
AngularJS Style Guide
by John Papa
& the community
Conversemos sobre las guías sugeridas
Y contestemos a preguntas como
¿Por qué? y ¿cómo?
Un componente por fichero
/* evitar */
angular
.module('app', ['ngRoute'])
.controller('UnControlador', UnControlador)
.factory('unaFactoria', unaFactoria)
.config(appConfigurator);
function UnControlador() {
/*
...
*/
}
function someFactory() {
/*
...
*/
}
/*
...
*/
¿leemos un fichero para saber que hay dentro?
// app.module.js
angular
.module('app', ['ngRoute']);
// unControlador.js
angular
.module('app')
.controller('UnControlador', UnControlador);
function UnControlador() { }
// unaFactoria.js
angular
.module('app')
.factory('unaFactoria', unaFactoria);
function unaFactoria() { }
// app.config.js
angular
.module('app')
.config(appConfigurator);
function appConfigurator() { }
Y ahora en serio,
¿cuantas veces te has saltado esta sencilla regla?
IIFE
acrónimo de Immediately Executed Function Expressions
aka "funciones anónimas autoejecutables"
aunque ni tienen por que ser anónimas y menos autoejecutables siendo anónimas
http://www.aprendiendonodejs.com/2011/11/expresiones-de-funcion-invocadas.html
Hablemos de:
- Cuanto tiempo están las funciones vivas en memoria
- Leak de variables
- Minimización
- Conflictos de nombres
/* evitar */
// logger.js
angular
.module('app')
.factory('logger', logger);
// la función logger queda añadida como variable global
function logger() { }
// storage.js
angular
.module('app')
.factory('storage', storage);
// la función storage queda añadida como variable global
function storage() { }
¿cuanto ensuciamos con esto?
// logger.js
(function() {
'use strict';
angular
.module('app')
.factory('logger', logger);
function logger() { }
})();
// storage.js
(function() {
'use strict';
angular
.module('app')
.factory('storage', storage);
function storage() { }
})();
Módulos
Convenciones sobre submódulos para evitar colisiones de nombres
Declarar módulos sin variables
/* evitar */
var app = angular.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);
/* recomendado */
angular
.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);
si no es necesario, entonces molesta
¿no hay un único elemento declarado por fichero?
De igual manera para todo lo demás
* a partir de ahora presuponemos el IIFE rodeando siempre el código
/* evitar */
var app = angular.module('app');
app.controller('SomeController', SomeController);
function SomeController() { }
/* recomendado */
angular
.module('app')
.controller('SomeController', SomeController);
function SomeController() { }
Nombra las funciones
“El mundo era tan reciente, que muchas cosas carecían de nombre, y para mencionarlas había que señalarlas con el dedo.”
Gabriel García Márquez (Cien años de soledad).
/* evitar */
angular
.module('app')
.controller('Dashboard', function() { })
.factory('logger', function() { });
Problemáticas:
- Difícil de leer
- Difícil de depurar
- Demasiado código dentro de las funciones anidadas en los callbacks
/* recomendado */
// dashboard.js
angular
.module('app')
.controller('Dashboard', Dashboard);
function Dashboard() { }
// logger.js
angular
.module('app')
.factory('logger', logger);
function logger() { }
Controladores
Sintaxis de controller en la vista
<!-- evitar -->
<div ng-controller="Customer">
{{ name }}
</div>
<!-- recomendado -->
<div ng-controller="Customer as customer">
{{ customer.name }}
</div>
- Los controladores se instancian en cada ocasión y esta sintaxis proporciona una mejor idea de lo que pasa que con la cadena de los $scope
- Promover el uso del binding con "." aclara donde se bindean las cosas y evitan problemas de "copias por valor" entre $scopes heredados
- Ayuda a evitar usar $parent en las vistas con controladores anidados, ya que los ancestros van nombrados
Sintaxis controllerAs para el Controlador
- Azucar sintáctico para el $scope, ahí está con menos código.
- Agregamos métodos y propiedades a 'this'
- Evitamos usar métodos del scope directamente desde el controlador
/* evitar */
function Customer($scope) {
$scope.name = {};
$scope.sendMessage = function() { };
}
/* recomendado - pero espera un poco más... */
function Customer() {
this.name = {};
this.sendMessage = function() { };
}
controllerAs con vm
- Usamos una variable para capturar "this"
- Nombre consistente como "vm" de ViewModel
/* evitar */
function Customer() {
this.name = {};
this.sendMessage = function() { };
}
/* recomendado */
function Customer() {
var vm = this;
vm.name = {};
vm.sendMessage = function() { };
}
evitando warnings de linters
/* jshint validthis: true */
var vm = this;
watchers
al estandarizar la nomenclatura con 'vm'
function SomeController($scope, $log) {
var vm = this;
vm.title = 'Some Title';
$scope.$watch('vm.title', function(current, original) {
$log.info('vm.title was %s', original);
$log.info('vm.title is now %s', current);
});
}
<input ng-model="vm.title"/>
/* más típico que la muerte de un personaje de GoT */
angular
.module('app')
.controller('Sessions', Sessions);
function Sessions() {
var vm = this;
function x(){
/*
...
*/
}
vm.gotoSession = function() {
/*
...
*/
};
/* ... y sigue con más líneas... */
¿Qué tiene mi controlador?
function y(){
/*
...
*/
}
function z(){
/*
...
*/
y()
/*
...
*/
}
vm.refresh = function() {
/*
...
*/
};
/* ... y sigue con más líneas... */
function v(){
/*
...
*/
}
function w(){
/*
...
*/
v()
/*
...
*/
}
vm.search = function() { // <- en la línea 1029 encontré la función que interesa
/* ... */
};
vm.sessions = [];
vm.title = 'Sessions';
/* ... y sigue con más líneas... */
Hoisting to the rescue
tan denostado, hoy le veremos utilidad
/* recomendado */
function Sessions() {
var vm = this;
vm.gotoSession = gotoSession;
vm.refresh = refresh;
vm.search = search;
vm.sessions = [];
vm.title = 'Sessions';
////////////
function gotoSession() {
/* */
}
function refresh() {
/* */
}
function search() {
/* */
}
Elementos "bindeados" en primer lugar
- ¡Qué bien se ve todo! :)
- Evitamos funciones anónimas en línea (*)
"one liners" sencillos
/* recomendado */
function Sessions(dataservice) {
var vm = this;
vm.gotoSession = gotoSession;
vm.refresh = dataservice.refresh; // 1 liner can be OK
vm.search = search;
vm.sessions = [];
vm.title = 'Sessions';
Declarar funciones arriba
e implementar abajo
Terminando con controllers:
- difiere la lógica a servicios y focaliza los controladores
- asigna los controladores con la sintaxis controllerAs (tanto en ng-route, ui-router o el nuevo router)
Servicios
Una responsabilidad por servicio
(que no hacer una única función)
function dataService() {
var someValue = '';
function save() {
/* */
};
function validate() {
/* */
};
return {
save: save,
someValue: someValue,
validate: validate
};
}
Formato tradicional de servicio...
function dataService() {
var someValue = '';
var service = {
save: save,
someValue: someValue,
validate: validate
};
return service;
////////////
function save() {
/* */
};
function validate() {
/* */
};
}
La misma estrategia que con los controladores
¿El servicio devuelve datos?
Empaquétalos en una promesa
Directivas
El único sitio donde manipulamos el DOM
Utiliza un prefijo:
mi-proyecto-directiva
controllerAs
y
bindToController
<div my-example max="77"></div>
angular
.module('app')
.directive('myExample', myExample);
function myExample() {
var directive = {
restrict: 'EA',
templateUrl: 'app/feature/example.directive.html',
scope: {
max: '='
},
controller: ExampleController,
controllerAs: 'vm',
bindToController: true
};
return directive;
}
function ExampleController() {
var vm = this;
vm.min = 3;
console.log('CTRL: vm.min = %s', vm.min);
console.log('CTRL: vm.max = %s', vm.max);
}
Activación de controller
function Avengers(dataservice) {
var vm = this;
vm.avengers = [];
vm.title = 'Avengers';
dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
function Avengers(dataservice) {
var vm = this;
vm.avengers = [];
vm.title = 'Avengers';
activate();
////////////
function activate() {
return dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
}
Ciclo de vida de nuevo router
Minificación
angular
.module('app')
.controller('Avengers', Avengers);
function Avengers(storageService, avengerService) {
var vm = this;
vm.heroSearch = '';
vm.storeHero = storeHero;
function storeHero() {
var hero = avengerService.find(vm.heroSearch);
storageService.save(hero.name, hero);
}
}
angular
.module('app')
.controller('Avengers', Avengers);
Avengers.$inject = ['storageService', 'avengerService'];
function Avengers(storageService, avengerService) {
var vm = this;
vm.heroSearch = '';
vm.storeHero = storeHero;
function storeHero() {
var hero = avengerService.find(vm.heroSearch);
storageService.save(hero.name, hero);
}
}
angular
.module('app')
.controller('Avengers', Avengers);
/* @ngInject */
function Avengers(storageService, avengerService) {
var vm = this;
vm.heroSearch = '';
vm.storeHero = storeHero;
function storeHero() {
var hero = avengerService.find(vm.heroSearch);
storageService.save(hero.name, hero);
}
}
minimizando con ng-annotate
/* @ngInject */
¿Cómo se llaman las cosas
y
donde están?
Puntos clave
-
Nomenclatura de ficheros
-
Nomenclatura de componentes
-
Ubicación de ficheros
// controllers
avengers.controller.js
avengers.controller.spec.js
// services/factories
logger.service.js
logger.service.spec.js
// constants
constants.js
// module definition
avengers.module.js
// routes
avengers.routes.js
avengers.routes.spec.js
// configuration
avengers.config.js
// directives
avenger-profile.directive.js
avenger-profile.directive.spec.js
/**
* recommended
*/
avengers.controller.spec.js
logger.service.spec.js
avengers.routes.spec.js
avenger-profile.directive.spec.js
// avengers.controller.js
angular
.module
.controller('AvengersController', AvengersController);
function AvengersController() { }
// logger.service.js
angular
.module
.factory('logger', logger);
function logger() { }
// avenger-profile.directive.js
angular
.module
.directive('xxAvengerProfile', xxAvengerProfile);
// usage is <xx-avenger-profile> </xx-avenger-profile>
function xxAvengerProfile() { }
Estructura de ficheros
Principio LIFT
- Locating our code is easy
- Identify code at a glance
- Flat structure as long as we can
- Try to stay DRY (Don’t Repeat Yourself) or T-DRY
Modularizando tu aplicación
Módulos focalizados, autocontenidos y pequeños
Un módulo "app" para englobarlos a todos
Tipos de módulos
-
Módulo app
-
Módulos verticales
-
Módulos reutilizables
-
Módulos con dependencias
angular
.module('app', [
//modulos compartidos
'app.core',
'app.widgests',
//modulos verticales
'app.customers',
'app.dashboard',
'app.layout'
]);
angular
.module('app.core', [
//modulos de Angular
'ngAnimate',
'ngSanitize',
//modulos horizontales
'blocks.router',
'blocks.logger',
'blocks.exception',
//modulos de terceros
'ui.router',
'angular.moment'
]);
angular
.module('app.dashboard', [
'app.core',
'app.widgests'
]);
JSHint
y
JSCS
Evitamos algunos errores de forma temprana
y
garantizamos uniformidad en los proyectos
Librerías de terceros
// constants.js
/* global toastr:false, moment:false */
(function() {
'use strict';
angular
.module('app.core')
.constant('toastr', toastr)
.constant('moment', moment);
})();
- Aclara las dependencias de otras librerías
- Resulta más fácil mockear librerías de terceros en los tests
- Las constantes se pueden inyectar hasta en los providers
// Constants used only by the sales module
angular
.module('app.sales')
.constant('events', {
ORDER_CREATED: 'event_order_created',
INVENTORY_DEPLETED: 'event_inventory_depleted'
});
Rutas
// customers.routes.js
angular
.module('app.customers')
.run(appRun);
/* @ngInject */
function appRun(routerHelper) {
routerHelper.configureStates(getStates());
}
function getStates() {
return [
{
state: 'customer',
config: {
abstract: true,
template: '<ui-view class="shuffle-animation"/>',
url: '/customer'
}
}
];
}
// routerHelperProvider.js
angular
.module('blocks.router')
.provider('routerHelper', routerHelperProvider);
/* @ngInject */
function routerHelperProvider(
$locationProvider,
$stateProvider,
$urlRouterProvider
) {
/* jshint validthis:true */
this.$get = RouterHelper;
$locationProvider.html5Mode(true);
RouterHelper.$inject = ['$state'];
/* @ngInject */
function RouterHelper($state) {
var hasOtherwise = false;
var service = {
configureStates: configureStates,
getStates: getStates
};
return service;
///////////////
function configureStates(states, otherwisePath) {
states.forEach(function(state) {
$stateProvider.state(state.state, state.config);
});
if (otherwisePath && !hasOtherwise) {
hasOtherwise = true;
$urlRouterProvider.otherwise(otherwisePath);
}
}
function getStates() { return $state.get(); }
}
}
Automatizaciones
Plantillas
Yeoman
¡Gracias!
¿Preguntas?
BTW, deja tu opinión en http://bit.ly/1Fylchz
AngularJS con estilo
By Gonzalo Ruiz de Villa
AngularJS con estilo
Porque las cosas pueden ser más sencillas
- 4,860