Game of life in Angular.js

di Andrea Zani, in .NET,

Nel 1970 John Horton Conway inventò il "e;Gioco della vita"e;. E' molto semplice: una griglia bidimensionabile di dimensione non prefissata è composta da x celle. Ogni cella può avere due stati: acceso o spento. Il gioco - anche se il termine gioco non è adatto - si basa su snapshot della situazione attuale della scacchiera e seguendo quattro semplici regole si cambia lo stato di tutte le celle. Come se si creasse una nuova tabella da capo, si controllano tutte le celle e per ognuna di esse si avranno quattro possibilità:

  • Se una cella con stato attivo che ha meno di celle adiacenti con stato attivo passa allo stato spento (nel gioco viene definita come morte per isolamento).
  • Se una cella con stato attivo ha adiacente due o tre celle con stato attivo, mantiene il suo stato attivo.
  • Se una cella con stato attivo ha adiacente più di tre celle attive il suo stato passa allo stato spento (come se morisse per effetto del sovraffolamento).
  • Se una cella ha stato spento ma ha adiacente tre celle, e solo tre celle attive, il suo stato passa ad attivo.

Fine delle regole. Come scritto sopra ad ogni ciclo si conrolla lo stato di tutta la tabella e si crea la nuova con le regole qui esposte. Vediamo qualche semplice esempio con un pattern semplicissimo:

La cella attiva in altro rimane accesa perché ha due celle attive adiacenti, quelle sottostanti, avendono solo una come adiacente, muoiono per solitudine. La cella al di sotto di quella in alto, avendo tre celle adiacenti, passa il stuo stato ad attivo. Ovviamente con pattern più complessi si avranno animazioni più interessanti e che proseguono per moltissimi step prima di spegnersi completamente o bloccarsi, come il seguente:

O che sono in grado di muoversi prima di bloccarsi contro il bordo, come uno dei pattern per primi scoperti con il nome "glider":

O passare ad uno stato intermittente senza fine come i seguenti che sono chiamati semplicemente "lampeggianti":

Oppure in pattern che creano sequenze che si ripetono all'infinito. Da dieci celle attive continue si ha:

Oppure:

Sono stati creati pattern per più complicate animazioni che si evolvono, come la seguente che, come un'astronave, percorre la tabella in continue cicliche evoluzioni fino a toccare il bordo dove si trasforma e inizia la sua discesa:

Una delle sfide che dagli anni '70 hanno affrontato gli appassionati di questo gioco pseudo matematico, era trovare un pattern in grado di evolversi all'infinito in curiose animazioni; una dei più famosi pattern conosciuti fu il gosper glider gun:

In internet si possono trovare pattern spaventosamente complessi in grado di evolversi anche in lettere o in tabella dalle dimensioni gigantesche. Rimando ai link principali su wikipedia per altre info.

Ora, come possiamo realizzare un semplice simulatore di questo gioco? Senza scomodare chissà quale tecnologia potremo fare il tutto da browser e da javascript. E già che ci siamo scomodiamo pure angular.js per vedere quanto la sua realizzazione sia semplice.

Ecco il risultato finale:

La parte superiore dove l'utente può modificare la dimensione della tabella e usare i pulsanti per avviare l'animazione o selezionare un pattern di esempio. Il codice html è il seguente:

  <div ng-controller="formController as frm">
    <fieldset>
      <form name="form1" novalidate="" ng-submit="frm.createTableForm()">
        <div>
          <input type="text" name="numberOfColumns" ng-model="frm.numberOfColumns" ng-pattern="frm.matchPattern" required="" />
          <label>Number of columns (20-90)</label>
          

          <input type="text" name="numberOfRows" ng-model="frm.numberOfRows" ng-pattern="frm.matchPattern" required="" />
          <label>Number of rows (20-90)</label>
        </div>
        <div>
          <label>Speed:</label>
          <span ng-repeat="item in frm.speedValues">
            <input id="rb{{item.value}}" type="radio" ng-model="frm.speed" ng-checked="frm.speed == item.value" ng-change="frm.speedChanged()"
                   value="{{item.value}}" />
            <label for="rb{{item.value}}">{{item.label}}</label>
          </span>
        </div>
        <div>
          <select ng-model="frm.pattern" ng-change="frm.patternChanged()" ng-disabled="frm.tables.started"
                  ng-options="c.Name for c in frm.patternList"></select>
          <label>Pattern</label>
        </div>
        <div>
          <button type="submit" ng-disabled="form1.$invalid || frm.tables.started" class="btn btn-primary">Create table</button>
          <button type="button" ng-disabled="form1.$invalid || frm.tables.started" ng-click="frm.singleStep()">Step</button>
          <button type="button" ng-disabled="form1.$invalid || frm.tables.started" ng-click="frm.start()">Start</button>
          <button type="button" ng-disabled="!frm.tables.started" ng-click="frm.stop()">Stop</button>
          <button type="button" ng-disabled="form1.$invalid || frm.tables.started" ng-click="frm.reset()">Reset</button>
        </div>
      </form>
    </fieldset>
  </div>

Per chi conosce già angular.js non c'è niente di straordinario in questo codice: vengono inseriti delle textbox e altri oggetti form e fatto il bind su oggetti definiti nel codice javascript del controller:

  return ['lifeService', 'patternService', function (lifeService, patternService) {
  var self = this;

  self.numberOfColumns = 40;
  self.numberOfRows = 30;
  self.numberOfCells = 0;
  self.widthMainTable = 300;
  self.matchPattern = new RegExp("^[\s]*[2-8][0-9][\s]*$|^[\s]*90$[\s]*$");
  self.speedValues = speedValueList.collection;
  self.speed = speedValueList.collection[1].value;
  patternService.remoteRequest().then(function (response) {
  self.patternList = response.data;
  self.reset();
  });
  self.tables = lifeService.getTables;

  self.singleStep = function () { lifeService.singleStep(); }
  self.start = function () { lifeService.start(self.speed); }
  self.stop = function () { lifeService.stop(); }
  self.speedChanged = function () { lifeService.speedChanged(self.speed); }

  self.reset = function () {
  self.pattern = self.patternList[0];
  self.createTableForm();
  }

  self.patternChanged = function () {
  var data = self.pattern.Data;
  lifeService.setPattern(data);
  }

  self.createTableForm = function () {
  lifeService.createTable(self.numberOfColumns, self.numberOfRows);
  self.widthMainTable = self.numberOfColumns * 20;
  }
  }]

Al controller vengono aggiunti, grazie all'inject automatico di angular, due service: patternService e lifeService. Il resto del codice è la definizione degli eventi degli oggetti della form e la loro integrazione con i service.

patternService è usato per richiedere ad una api rest l'elenco dei pattern:

  return ["$http", function ($http) {
  return {
  remoteRequest: function () { return $http.get('/api/patterns'); }
  }
  }]

Server side è questo codice che risponde:

  public class PatternsController : ApiController
  {
  private Dictionary<string, string="">
    patternCollection = new Dictionary<string, string="">
      {
      { "Clear", "[]" },
      { "Glider", "[[1, 0], [2, 1], [2, 2], [1, 2], [0, 2]]" },
      ...
      };

      public List<PatternClass>
        Get()
        {
        return patternCollection.Select(t => {
        var coll = new List<int[]>
          ();
          var elements = t.Value.Split("], [".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
          for (int i = 0; i < elements.Length; i+=2)
                {
                    coll.Add(new int[] {int.Parse(elements[i]), int.Parse(elements[i+1])});
                }
                return new PatternClass { Name = t.Key, Data = coll };
            }).ToList();
        }
    }

Ed ecco il codice del lifeService:

  return ['$interval', function ($interval) {
  var tables = { table: [], maxWidth: 0, started: false };
  var numberOfColumns, numberOfRows, numberOfCells;
  var interval;
  var cacheArrayZeroFill;

Nel servizio viene passato l'oggetto $interval con l'inject e vengono definite varie variabili che saranno usate in seguito.

  var createTable = function (numColumns, numRows, pattern) {
  if (!tables.started) {
  numberOfColumns = numColumns;
  numberOfRows = numRows;
  numberOfCells = numberOfRows * numberOfColumns;
  tables.maxWidth = numberOfColumns * 20;
  var buff = new Array(numberOfCells);
  cacheArrayZeroFill = new Array(numberOfCells);

  for (var i = 0; i < numberOfCells; i++) {
                    buff[i] = { id: i, state: false };
                    cacheArrayZeroFill[i] = 0;
                }

                if (!_.isUndefined(pattern)) {
                    var maxX = 0, maxY = 0;
                    _.each(pattern, function (value, key) {
                        if (value[1] > maxY) { maxY = value[1]; }
                        if (value[0] > maxX) { maxX = value[0]; }
                    });

                    maxX++; maxY++;

                    if (maxX > numberOfColumns) {
                        alert("Patter width error")
                        return;
                    }

                    if (maxY > numberOfRows) {
                        alert("Patter height error")
                        return;
                    }

                    var centerX = Math.floor(numberOfColumns / 2 - maxX / 2);
                    var centerY = Math.floor(numberOfRows / 2 - maxY / 2);

                    _.each(pattern, function (value, key) {
                        var y = centerY + value[1];
                        var x = centerX + value[0];

                        buff[x + y * numberOfColumns].state = true;
                    });
                }

                tables.table = [].concat(buff);
            }
        }

createTable crea nell'oggetto table (che è utilizzato nel controller nel codice html come source per la visualizzazione della tabella) e se è presente anche il parametro pattern vengono attivate le celle coinvolte. Ed ecco il codice che esegue sulla tabella quelle quattro regole per l'attivazione e la disattivazione delle celle:

  var elaborate = function () {
  var cacheTable = checkAllCell();
  var tableTemp = tables.table;

  for (var i = 0; i < numberOfCells; i++) {
                var around = cacheTable[i];
                var cell = tableTemp[i];
                if (!cell.state) {
                    if (around == 3) {
                        cell.state = true;
                    }
                }
                else {
                    if (around != 2 && around != 3) {
  cell.state = false;
  }
  }
  }
  }

  var checkAllCell = function () {
  var cacheTable = cacheArrayZeroFill.concat([]);

  for (var y = 0; y < numberOfRows; y++) {
                for (var x = 0; x < numberOfColumns; x++) {
                    var cell = tables.table[x + y * numberOfColumns];
                    if (cell.state) {
                        if (y > 0) {
                            if (x > 0) {
                                cacheTable[x - 1 + (y - 1) * numberOfColumns]++;
                            }
                            cacheTable[x + (y - 1) * numberOfColumns]++;
                            if (x < numberOfColumns - 1) {
                                cacheTable[x + 1 + (y - 1) * numberOfColumns]++;
                            }
                        }
                        if (x > 0) {
                            cacheTable[x - 1 + y * numberOfColumns]++;
                        }
                        if (x < numberOfColumns - 1) {
                            cacheTable[x + 1 + y * numberOfColumns]++;
                        }
                        if (y < numberOfRows - 1) {
                            if (x > 0) {
                                cacheTable[x - 1 + (y + 1) * numberOfColumns]++;
                            }
                            cacheTable[x + (y + 1) * numberOfColumns]++;
                            if (x < numberOfColumns - 1) {
                                cacheTable[x + 1 + (y + 1) * numberOfColumns]++;
                            }
                        }
                    }
                }
            }
            return cacheTable;
        }

Innanzitutto la funzione checkAllCell non fa altro che prendere la tabella attualmente visualizzata e contare, per ogni cella, il numero di celle adiacenti attive. Avendo questo dato, la funzione elaborate, esegue materialmente quelle quattro regole prima descritte attivando/disattivando nella nuova tabella le celle. Tralasciando le funzioni collegate ai button nella pagina, importante per l'esecuzione dell'animazione le funzioni seguenti:

  var start = function (ms) {
  if (!tables.started) {
  tables.started = true;
  interval = $interval(function () {
  elaborate();
  }, ms);
  }
  }

  var stop = function () {
  if (tables.started) {
  $interval.cancel(interval);
  tables.started = false;
  }
  }

start usa l'oggetto $interval prima caricato con la dependency injection per eseguire a intervalli regolari la funzione elaborate. stop blocca l'animazione. Altro service utilizzato è il patternService già visto prima. Il codice nel controller formController che utilizza questo oggetto è il seguente (già mostrato prima):

  patternService.remoteRequest().then(function (response) {
  self.patternList = response.data;
  self.reset();
  });

Mettendo insieme tutto questo codice abbiamo il nostro bel gioco funzionante. Ma questo post è nato per mostrare solo questo e il suo uso con angular.js? No, sincermente è stata solo una scusa per approfondire un aspetto che avevo solo sfiorato: l'utilizzo di require.js con angular.js. Avevo iniziato ad apprezzare require.js quando avevo iniziato a studiare backbone.js. Studiando molto dopo angular.js mi sono chiesto subito se ci fosse qualche vantaggio a usare insieme queste tecnologie o se era una complicazione inutile.

Faccio un passo indietro. Per chi mastica già la tecnologia angular.js, sa che si può fare tutto in modo semplice ed non ci sono problemi nel separare in modo semplice in più file. Nel caso della web app di esempio ci sono due controller e due service. Per semplificare avrei potuto creare quattro file distinti e li avrei potuto includere nella pagina html con una banale inclusione:

  <script src="service1.js"></script>
  <script src="service2.js"></script>
  <script src="controller1.js"></script>
  <script src="controller2.js"></script>

E avremmo avuto tutto il necessario. Perché complicare il tutto con require.js? Innanzitutto perché possiamo utilizzare un robusto strumento per le dipendenze e possiamo caricarle in maniera asincrona, il pattern AMD ricorda qualcosa? Inoltre la suddivisione - così come è anche fatta con le semplici istruzioni sopra - in più file, permette una migliore manutenzione e lettura del codice.

Vediamo come realizzare il tutto. Lasciamo il codice html con i due controller e carichiamo require.js in questo modo:

  <script data-main="/scripts/main" src="/scripts/vendor/require.min.js"></script>

Se si apre il codice che si può scaricare con il link sul fondo di questo post, si troverà il codice nella directory private (il motivo sarà spiegato tra poco), con il nome main.js (usando require.js si può evitare di scrivere ".js" per la definizine del nome del modulo):

  require.config({
  paths: {
  angular: ['//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min','/scripts/vendor/angular.min'],
  underscore: ['//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min','/scripts/vendor/underscore.min']
  },
  shim: {
  angular: {
  exports: 'angular'
  }
  },
  deps: ['app']
  });

In config possiamo specificare degli alias per le nostre dipendenze: con "angular" specifichiamo che vogliamo caricare il file da cloudfire (è un CDN) o se non è possibile lo carica dal server dov'è presente la web app (in questo modo se lo abbiamo già scaricato anche da altri siti web che usano lo stesso CDN lo avremo già in memoria senza doverlo riscaricare). La stessa cosa anche per il modulo "underscore.js". Essendo angular non rispettoso dello standard AMD, dobbiamo specificare nella sezione shim il nome con cui sarà possibile utilizzarlo mentre con underscore.js questo non è necessario. Alla fine con deps viene specificato il modulo principale della nostra app, giustappunto "app.js". Prima di vedere il codice reale vediamo come sono definiti i moduli e le loro dipendenze con un esempio semplicissimo:

  // a1.js
  define(function () {
  return "Io sono A1";
  });

  // a2.js
  define(function () {
  return "Io sono A2";
  });

Potremo caricarli con:

  require(["a1", "a2"],
  function (a1Value, a2Value) { alert(a1Value); alert(a2Value);}
  );

requirejs caricherà i due modili a1.js e a2.js. Eseguiti come funzioni assegneranno il valore di ritorno (in questo caso stringhe ma potrebbero essere anche oggetti complessi) alle variabili a1Value e a2Value e saranno visualizzati eseguendo il codice di questa funzione. I metodi define e require lavorano bene in coppia: la prima definisce di base una funzione e possiamo anche definire eventuali dipendenze necessarie per il funzionamento di quel blocco di codice. Avremmo potuto scrivere:

  // a1.js
  define(["modulo1", "modulo2"], function (modulo1, modulo2) {
  return "Io sono A1"+modulo1.Value+" "+module.Value;
  });

In questo modo quando requirejs caricherà questo modulo, automaticamente, prima di eseguirlo, caricherà le due dipendenze. Nelle dipendenze si possono mettere sia alias che il nome completo di path del file javascript. Ecco il caso reale del nostro esempio (app.js):

  require(["angular", "controllers/formController", "controllers/gridController", "services/lifeService", "services/patternService"],
  function (angular, formController, gridController, lifeService, patternService) {
  angular
  .module('requireModule', [])
  .service('lifeService', lifeService)
  .service('patternService', patternService)
  .controller('formController', formController)
  .controller('gridController', gridController)
  ;
  angular.bootstrap(document, ['requireModule']);
  });

Qui carichiamo tutto ciò che abbiamo bisogno e si vedono entrambi i tipi di dichiarazione delle dipendenze. angular è l'alias definito nel file main.js, mentre i vari controller e service vengono definiti direttamente come nome del file completo di path. Quindi, una volta caricati e eseguiti, la loro instanza sono passate ai parametri della funzione e li utilizzerà per definire i controller e i service in angular.js. Già solo con questi due file si può notare l'ordine e la chiarezza nella definizione delle varie dipendenze, e anche sotto l'occhio di un developer che non hai mai visto questa web app, troverebbe immediatamente i riferimenti agli oggetti interessati.

Ora manca solo vedere come definire i controller prima visti. Il gridcontroller:

  define(function () {
  return ['lifeService', function (lifeService) {
  this.tables = lifeService.getTables;
  }]
  });

In define c'è solo function, quindi non sono riechieste dipendenze a require.js. Il controller ha bisogno del service "lifeService" e lo dichiariamo come la sintassi di angular.js. Il formController è definito così:

  define(['speedValueList'], function (speedValueList) {
  return ['lifeService', 'patternService', function (lifeService, patternService) {
  var self = this;
  [... codice già visto in precedenza]
  }]
  });

Questo è più interessante. Qui viene definita una dipendenza a require.js: "speedValueList". Require.js richiederà il file speedValueList.js che ha questo contenuto:

  define(function () {
  var collection = [
  { label: "Slow", value: 300 },
  { label: "Medium", value: 120 },
  { label: "Fast", value: 30 }];

  return {
  collection: collection
  }
  });

Ritorna una semplice collection con le possibili velocità della nostra animazione. Il codice precedente, appena caricato questo file, passa la collectin al formController che avrà da angular.js i due service di qui avrà bisogno con l'inject. Lo stesso giro lo fa il service lifeService.js:

  define(["underscore"], function (_) {
  return ['$interval', function ($interval) {
  [... codice già visto in precedenza]
  }]
  });

Qui è richiesto underscore.js la cui istanza potrà essere utilizzata all'interno del codice (in questo caso utilizzeremo !_.isUndefined per controllare se un parametro è stato passato e _.each per controllare il contenuto di un array) e sarà fatto l'inject dell'oggetto $interval per il controllo della velocità dell'animazione - il primo sarà eseguito da require.js, il secondo da angular.js.

patternService è semplice:

  define([], function () {
  return ["$http", function ($http) {
  return {
  remoteRequest: function () { return $http.get('/api/patterns'); }
  }
  }]
  });

Anche qui si specifica che non sono necessarie dipendenze (si può evitare il parametro come nell'esempio visto poco fa che inserire la definizione di un array vuoto). L'oggeto $http sarà utilizzato per eseguire una richiesta ad un api rest. Tutto il codice è visibile in questa struttura:

Abbiamo finito. Facendo girare il tutto funziona alla perfezione. Ma esaminando il traffico di questa semplice web app ci si può accorgere di una cosa:

Requirejs ha lavorato sì correttamente ma il problema è che ha eseguito moltissime richieste http per caricare tutte le dipendenze: solo per questa app semplicissima sono state eseguite una decina di richieste js. Questo rallenta tutto l'avvio dell'applicativo ed esegue una marea di connessioni inutili (i dispositivi mobile ringraziano). Senza tirare per le lunghe, possiamo ottimizzare il tutto. Possiamo concatenare in un unico file e minificare tutto il codice; risultato finale: un'unica richiesta dalla nostra web app lasciando il codice sorgente suddiviso, come fatto finora, per renderlo maggiormente gestibile. Per fare questo possiamo utilizzare due strumenti che sono stati introdotti di default all'interno di visual studio 2015: grunt o gulp. Con entrambi possiamo gestire e modificare come vogliamo il nostro codice (javascript, css, c#, immagini...); il primo, grunt, grazie ad un file di configurazione; il secondo, gulp, con uno script in javascript. Per i miei gusti ho preferito usare il secondo. Nei progetti web in visual studio 2015 abbiamo quanto ci occorre:

Quindi in package.json possiamo definire i plugin di cui abbiamo bisogno:

  {
  "version": "1.0.0",
  "name": "ASPNET",
  "private": true,
  "devDependencies": {
  "gulp": "^3.9.1",
  "gulp-amd-optimizer": "^0.4.0",
  "gulp-clean-css": "^2.0.3",
  "gulp-concat": "^2.6.0",
  "gulp-rename": "^1.2.2",
  "gulp-sourcemaps": "^1.6.0",
  "gulp-uglify": "^1.5.3"
  }
  }

gulp è il tool di cui abbiamo bisogno; gulp-amd-optimizer è il modulo che ci permette di ottimizzare i nostri file javascript creati con require.js; gulp-clean-css può essere utilizzato per ripulire il css; gulp-concat per concatenare più file; gulp-rename se volessimo fare il rename dei file; gulp-sourcemaps per creare il sourcemap comodo in casi di codice minificato per il debug;gulp-uglify per minificare effettivamente il codice.

Avendo creato il file gulp.js possiamo inserire gli script di cui abbiamo bisogno:

  var gulp = require('gulp');
  var uglify = require("gulp-uglify");
  var concat = require("gulp-concat");
  var rename = require("gulp-rename");
  var minifycss = require("gulp-clean-css");
  var amdOptimize = require('gulp-amd-optimizer');
  var sourcemaps = require('gulp-sourcemaps');

  var amdConfig = {
  baseUrl: './private/scripts/app/',
  path: {
  'lib': './private/scripts/app/'
  },
  exclude: [
  'jQuery', 'angular', 'underscore'
  ]
  };

  gulp.task('amdOpt', function () {
  return gulp.src('./private/scripts/app/*.js', { base: amdConfig.baseUrl })
  .pipe(sourcemaps.init())
  .pipe(amdOptimize(amdConfig))
  .pipe(concat('app.js'))
  .pipe(uglify())
  .pipe(sourcemaps.write('./'))
  .pipe(gulp.dest('./Scripts/'));
  });

  gulp.task('css1', function () {
  console.log("start css1");
  gulp.src(["./private/css/*.css"])
  .pipe(concat("styles.css"))
  .pipe(minifycss())
  .pipe(gulp.dest("./css/"));
  });

  gulp.task('startjs', function () {
  console.log('startjs!');

  gulp.src(["./wwwroot/lib/angular/angular.js"])
  .pipe(concat("angular.min.js"))
  .pipe(uglify())
  .pipe(gulp.dest("./Scripts/vendor/"));

  gulp.src(["./wwwroot/lib/requirejs/require.js"])
  .pipe(concat("require.min.js"))
  .pipe(uglify())
  .pipe(gulp.dest("./Scripts/vendor/"));

  gulp.src(["./wwwroot/lib/underscore/underscore.js"])
  .pipe(concat("underscore.min.js"))
  .pipe(uglify())
  .pipe(gulp.dest("./Scripts/vendor/"));
  });

  gulp.task('watch1', function () {
  return gulp.watch('./private/scripts/app/*.js', ['amdOpt']);
  });

  gulp.task('watch2', function () {
  return gulp.watch('./private/css/*.css', ['css1']);
  });

Con i vari require carichiamo i moduli quindi ho creato cinque task:

  • ampOpt: questo prende tutti i file javascript coinvolti con require.js e li concatena minificandoli in un nuovo file.
  • css1: prende il file css utilizzato da questa web app e lo comprime creando un nuovo file.
  • startjs: prende le librerie javascript angular.js, underscore.js e require.js e le copa minificate in una directory pubblica.
  • watch1 e watch2: permetteno di lasciare un processo collegato che controlla determinati file. Quando questi subiscono delle modifiche, rieseguono determinati script; se dovessimo modificare un css, per vederlo poi dal vivo, dovremo lanciare manualmente il task css1; il watch ci permette l'avvio automatico dello stesso.

Qualche parola in più su amdOpt. Questo task utilizza una sua configurazione:

  var amdConfig = {
  baseUrl: './private/scripts/app/',
  path: {
  'lib': './private/scripts/app/'
  },
  exclude: [
  'jQuery', 'angular', 'underscore'
  ]
  };

Con baseUrl viene specificato dove sono salvati i nostri file javascript della nostra web app (ho inserito questi file in una directory private: saranno poi i task appena esposti a copiarli in una directory pubblica dopo la loro elaborazione). path specifica il percorso dove amdOpt cercherà i file. exclude serve per escludere i file dalla minificazione e concatenazione. In questo caso ho evitato che le librierie angular.js, jquery e underscore venissero incluse (jquery è un mio refuso visto che non viene utilizzato all'interno di questa web app).

Ora possimo richiamare questi task (o collegarmi direttamente alla compilazione), modificare la nostra pagina html in modo che venga caricato il file javascript minificato:

  <script data-main="/scripts/app" src="/scripts/vendor/require.min.js"></script>

Rilanciamo il tutto e vediamo se ha funzionato il tutto:

Il nostro file app.js pesa solo 2 KB, e il contenuto è come ci aspettiamo:

Anche per il css il lavoro è stato eseguito:

h1{text-align:center}body{font-size:.8em}fieldset{width:32em;margin:1em auto;background-color:#ccc}.button-container{background-color:#a9a9a9;overflow-y:auto;margin:1em auto}.button-container>button{width:20px;height:20px;float:left;margin:0;border:1px solid #999}.cellSelected{background-color:#000}.cellUnselected{background-color:#d3d3d3}

Inoltre, avendo usando anche gulp-sourcemaps, possiamo fare il debug e ritrovare il nostro codice in chiaro:

Ed ecco il risultato finale:

Il codice sorgente è disponibile a questo link:
https://bitbucket.org/sbraer/lifegame

Maggiori info sul "gioco della vita":

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese