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

https://github.com/johnpapa/angular-styleguide

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,878