Minificazione Risorse
Nell'ambito della issue #1404, rilasciata nella versione 4.7.2, è stato introdotto un nuovo meccanismo di minificazione delle risorse javascript e CSS. Questo ha permesso di dare un boost alla velocità di apertura del progetto/funzioone.
Procedura precedente
Precedentemente all'intervento la minificazione delle risorse aveva questi vincoli:
- per il codice di geoweb, venivano minificati solo i file .js (e non i .css) e SOLO dei seguenti moduli principali:
- gw-commons-web (transfer-objects, pre 4.6.*)
- gw-webclient (webclient, pre 4.6.*)
- gw-webadmin (webadmin, pre 4.6.*)
- per la minificazione, veniva utilizzato il tool di build, rilasciata con lo stesso dojo, che era configurato per:
- generare un unico file compresso con le librerie dojo utilizzate (da esplicitare preventivamente in un file di build)
- generare un unico file compresso con le librerie customDojo, sviluppate ad hoc per geoweb (che per convenzione venivano tutti messi dentro gw-commons-web)
- minificare tutte le risorse sotto la folder debug/js, senza concatenarle, e posizionarle sotto la folder compressed/js
- i file minificati erano generati prima di rilasciare una versione di geoweb lanciando le varie procedure configurate in eclipse (Webclient Dojo Builder, etc.., come da pagina dedicata nella wiki)
- la scelta del path dove cercare le risorse veniva fatta dimanicamente in base al flag useCompressed del configuration.properties
- le risorse necessarie epr la minificaizone di dojo pesano cica 10 MB e andavano tenute in copie multiple per ogni plugin sul quale si voleva implementare una procedura di minificazione
- gli altri plugin di geoweb tendenzialmente non avevano procedure di minificazione, e caricavano le risorse sempre dalla folder debug/js
- le librerie esterne (.js e .css) in genere venivano incluse gia minificate
- ogni plugin del framework specificava nell'initializer java quali risorse css e js avessero dovute essere caricate in automatico all'avvio del progetto
- altre volte la singola scheda caricava alla sua apertura (e non all'avvio del progetto), le risorse esclusive di cui necessitava
Da tutti questi vincoli risulta che ci fossero enormi margini di ottimizzazione, in quanto:
- le risorse erano frammentate in moltissimi file (circa 200 file in progetti con molti plugin)
- le risorse tirate giù dal browser nel loro complesso pesavano più di 10 MB
Nuova procedura: Grunt
Si è scelto di introdurre nel codice uno strumento dedicato: Grunt. Caratteristiche:
- permette di minificare facilmente sia risorse .js che .css
- permette di concatenare risorse in un'unico file
- permette di configurare una pipeline di build personalizzata
Modalità di utilizzo
Grunt è stato configurato per tutti i moduli attualmente presenti sotto il repo geowebframeowrk.
La configurazione ha lo scopo di accorpare risorse, minificandole, e produrre per ogni modulo un numero standard di file: 4. La separazione fra .js e .css è imposta. Si è scelto poi di separare i file prodotti da geoweb da quelli derivanti da librerie esterne. Inoltre, data la natura modulare del framework, questo schema è replicato per ogni modulo (plugin).
Quindi, per ogni modulo vengono generati i seguenti:
- [nome-modulo].min.js
- [nome-modulo].min.css
- [nome-modulo]-externals.min.js
- [nome-modulo]-externals.min.css
Per esempio:
- gw-webclient.min.js
- gw-webclient-externals.min.css
- gw-webclient.min.js
- gw-webclient-externals.min.css
I file -externals inglobano tutte le librerie esterne, non codice di geoweb, in un'unico file.
Vecchi meccanismi generali mantenuti
- la build dei file dojo/customDojo è stata mantenuta come prima, gestita dal tool dojo, ma è rimasta solo nel plugin gw-commons-web (non c'è più ne per il webclient ne per il webadmin).
- l'utilizzo di risorse compresse si abilita sempre del flag useCompressed nel configuration.properties
- ogni plugin dichiara nel suo initializer quali risorse vanno caricate all'inizio (compresse o meno che siano)
Nuovi Meccanismi introdotti
- essendo Grunt basato su Node, sotto la folder principale geowebframework c'è ora una cartella comune node_modules, che contiene tutte le dipendenze utilizzate per i vari build
- c'è inoltre il file package.json e relativo package-lock.json, che specifica le varie dipendenze
- ogni modulo ha un suo file di configurazione della minificazione GruntFile.js sotto static-resources (Es: /gw-webclient/src/main/resources/META-INF/static-resources/Gruntfile.js). Questo file va configurato seguendo le specifiche esigenze del modulo da minificare. Non ci sono regole ferree, ma buone linee guida
- ogni configurazione esporta i 4 file minificati nella nuova folder /build, sotto le risorse statiche (solo gw-commons-web mantiene anche compressed per le risorse dojo/customDojo)
- la procedura di build di ogni modulo può essere lanciata singolarmente, posizionando cmd sulla folder del modulo e lanciando il comando grunt
- Al fine di lanciarle tutte insieme, nel file package.json, c'è specificato in scripts di utilizzare npm-run-all per buildare in parallelo, tutti i vari GruntFile.js dei vari moduli (che vanno quindi esplicitamente censiti)
{ "devDependencies": { "grunt": "^1.6.1", "grunt-contrib-clean": "^2.0.1", "grunt-contrib-concat": "^2.1.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-cssmin": "^5.0.0", "grunt-contrib-uglify": "^5.2.2", "grunt-terser": "^2.0.0", "npm-run-all": "^4.1.5" }, "scripts": { "build": "npm-run-all --parallel build:gw-webclient build:gw-commons-web build:gw-advanced build:gw-workflow build:gw-3d-visualizer build:gw-approvals build:gw-cde build:gw-cms build:gw-furnitures build:gw-google-street-view build:gw-mnemonic-code build:gw-print-map-legend build:gw-scenarios build:gw-thematism build:gw-webadmin", "build:gw-webclient": "grunt --gruntfile ./gw-webclient/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-webadmin": "grunt --gruntfile ./gw-webadmin/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-commons-web": "grunt --gruntfile ./gw-commons-web/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-advanced": "grunt --gruntfile ./gw-advanced/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-workflow": "grunt --gruntfile ./gw-workflow/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-3d-visualizer": "grunt --gruntfile ./gw-3d-visualizer/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-approvals": "grunt --gruntfile ./gw-approvals/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-cde": "grunt --gruntfile ./gw-cde/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-cms": "grunt --gruntfile ./gw-cms/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-furnitures": "grunt --gruntfile ./gw-furnitures/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-google-street-view": "grunt --gruntfile ./gw-google-street-view/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-mnemonic-code": "grunt --gruntfile ./gw-mnemonic-code/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-print-map-legend": "grunt --gruntfile ./gw-print-map-legend/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-scenarios": "grunt --gruntfile ./gw-scenarios/src/main/resources/META-INF/static-resources/Gruntfile.js", "build:gw-thematism": "grunt --gruntfile ./gw-thematism/src/main/resources/META-INF/static-resources/Gruntfile.js" } }
Eclispe New External Tools Configuration
Analogamente a quanto era stato fatto per le build dojo, ogni sviluppatore deve creare una nuova procedura di build.
Run > External Tools > External Tools Condifurations..
Nel dialog che si apre RClick su Program e click su New Configuration... Impostare questa configurazione
Name: grunt-build Location: C:\nodejs\npm.cmd Working directory: ${workspace_loc:\geowebframework\} Arguments: run build
Alla fine click su Apply ed opzionalmente Run.
Questo comando va lanciato in occasione di ogni rilascio di versione/tag. Il risultato della build va committato con il messaggio grunt build.
Nota: deve continuare ad essere lanciara la procedura di build dei file compressi dojo/customDojo di gw-commons-web, che è l'unica rimasta.
Nella fase di build possono essere generati alcuni errori e warning che non pregiudicano la corretta riuscita dell'operazione attuale.
Warning/Errori da non considerare
Alcuni errori e segnalazioni non sono da considerarsi problematiche da eliminare
Alcuni import possono essere segnalati come già effettuati, soprattutto in codice dojo
Running "cssmin:concat" (cssmin) task >> Ignoring local @import of "../icons/printMapLegendIcons.css" as it has already been imported. >> 1 file created. 48 B → 0 B
Su alcuni file css ci sono notazioni del tipo '#zoom', che non sono attualmente valide, ma erano notaziooni speciali supportate solo da alcuni browser in passato (IE).
>> Invalid property name '#zoom' at css/dojo-flat/dojo/dijit.css:17:1. Ignoring.,Invalid property name '#display' at css/dojo-flat/dojo/dijit.css:18:1. Ignoring.,Invalid property name '#vertical-align' at css/dojo-flat/dojo/dijit.css:22:1. Ignoring.,Invalid property name '#zoom' at css/dojo-flat/dojo/dijit.css:41:1. Ignoring.,Invalid property name '#display' at css/dojo-flat/dojo/dijit.css:198:1. Ignoring.,Invalid property name '#vertical-align' at css/dojo-flat/dojo/dijit.css:253:1. Ignoring.,Invalid property name '#overflow' at css/dojo-flat/dojo/dijit.css:283:1. Ignoring.,Invalid property name '#filter' at css/dojo-flat/dojo/dijit.css:308:1. Ignoring.,Invalid property name '#text-indent' at css/dojo-flat/dojo/dijit.css:349:1. Ignoring.,Invalid property name '#letter-spacing' at css/dojo-flat/dojo/dijit.css:350:1. Ignoring.,Invalid property name '#text-align' at css/dojo-flat/dojo/dijit.css:351:1. Ignoring.,Invalid property name '#bottom' at css/dojo-flat/dojo/dijit.css:492:1. Ignoring.,Invalid property name '#zoom' at css/dojo-flat/dojo/dijit.css:1176:1. Ignoring.,Invalid property name '#display' at css/dojo-flat/dojo/dijit.css:1177:1. Ignoring.,Invalid property name '#display' at css/dojo-flat/dojo/dijit.css:1741:4. Ignoring.
Resoconto finale della build dojo compressed gw-commons-web, da considerarsi perfettamente valido
0 error(s), 1 warning(s) starting cleaning up... starting reporting... Report written to C:/wsGW_git_j11/geowebframework/gw-commons-web/src/main/resources/META-INF/static-resources/compressed/build-report.txt Process finished normally. errors: 10 warnings: 65
Esempio GruntFile.js
Di seguito viene analizzato il GruntFile.js di gw-webclient. Sebbene molti aspetti di impostazione siano mantenuti negli altri GruntFile.js è bene ricordare che ogni modulo ha bisogno, in base alle librerie da gestire, delle sue specifiche configurazioni.
//issue #1404 module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('./../../../../../../package.json'), // resources concatenation concat: { js: { files: [ { src: ['debug/js/**/*.js'], dest: 'build/gw-webclient.js', filter: function (dest) { return !dest.endsWith('profile.js'); //exclude js.profile.js }, }, { src: [ 'html2canvas/html2canvas.min.js', //issue #1267 'jquery/jquery-3.7.1.min.js', //issue #1267 //'openlayers/OpenLayers.js', // (handled inside tab) 'orgchart/dist/js/jquery.orgchart.min.js', //issue #1267 'pdfjs/dist/jspdf.umd.min.js', //issue #1267 'proj4js/dist/proj4.js' ], dest: 'build/gw-webclient-externals.js' }, ] }, css: { //handled in cssmin } }, // js minification /*uglify: { options: { compress: { //'hoist_funs': false, //'unsafe': true, 'drop_debugger': true, 'keep_fargs': true, 'keep_infinity': true, // (default: false) — Pass true to prevent Infinity from being compressed into 1/0, which may cause performance issues on Chrome. } }, build: { src: 'build/gw-webclient.js', dest: 'build/gw-webclient.min.js' } },*/ terser: { options: { ecma: 5, // Specifica la versione ECMAScript compress: true, mangle: true }, build: { files: { 'build/gw-webclient.min.js': [ 'build/gw-webclient.js' ]/*, 'build/gw-webclient-externals.min.js': [ //already minified 'build/gw-webclient-externals.js' ]*/ } } }, //copia i file gia compressi esterni copy: { main: { files: [ { src: ['build/gw-webclient-externals.js'], dest: 'build/gw-webclient-externals.min.js' } ], }, }, // css minification cssmin: { options: { rebase: true, relativeTo: './' }, concat: { files: [ { src: [ 'css/**/*.css', 'icons/**/*.css' ], dest: 'build/gw-webclient.css', filter: function (dest) { return !dest.endsWith('gwClientsPack.css'); // exclude gwClientsPack.css }, }, { src: [ 'orgchart/dist/css/jquery.orgchart.min.css' ], dest: 'build/gw-webclient-externals.css', }, ] }, min: { files: [ { src: 'build/gw-webclient.css', dest: 'build/gw-webclient.min.css' }, { src: 'build/gw-webclient-externals.css', dest: 'build/gw-webclient-externals.min.css' } ] } }, // cleaning not minified files clean: { postbuild: { src: [ 'build/**/*.js', '!build/**/*.min.js', 'build/**/*.css', '!build/**/*.min.css', ] } } }); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-copy'); //grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-terser'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-clean'); // Task predefinito grunt.registerTask('default', [ 'concat', 'copy', //'uglify', //mutually exclusive with to terser 'terser', //mutually exclusive with uglify 'cssmin', 'clean:postbuild' //at last ]); };
Import delle dipendenze
grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-copy'); //grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-terser'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-clean');
Pipeline Task
Questa parte determina l'ordine di esecuzione della pipeline. L'ordine può essere modificato in base alle esigenze del singolo plugin.
grunt.registerTask('default', [ 'concat', 'copy', //'uglify', //mutually exclusive with to terser 'terser', //mutually exclusive with uglify 'cssmin', 'clean:postbuild' //at last ]);
Inoltre in ogni fase della pipeline si può scegliere di intervenire su alcuni file e non su altri. Per esempio si può scegliere di unire insieme dei file e poi minificarli. Oppure in presenza di file già minificati si potrebbe configurare di unirli senza ulteriori elaborazioni. La fase di clean rimuove eventuali file di lavorazione intermedia
Per il resto le opzioni dei vari task sono auto esplicanti.
Compatibilità vecchi plugin geoweb
La nuova procedura è stata resa compatibile con la vecchia. In generale quindi i vecchi plugin di geoweb, specificando ognuno le proprie risorse, non compresse, continuano a funzionare correttamente anche adesso.
Nuovi plugin geoweb
La creazione di nuovi plugin geoweb, idealmente comporta la definizione anche di un relativo GruntFile.js, opportunamente configurato.
Linee Guida
Tenendo sempre presente che ci sono più strade di combinare (ordinandoli, filtrando risorse) i vari task, è sempre bene ricordarsi queste linee guida:
- cssmin ha specifiche opzioni per la concatenazione e la minificazione (quindi è preferibile evitare di configurare questi aspetti altrove)
- terser, da preferire ad uglify, ha specifiche opzioni per la concatenazione e la minificazione
- concat dovrebbe essere usato per raggruppare i file js di goeweb prima della minificazione, e per raggruppare i file js delle librerie esterne (gia minificati, e che poi possono essere esclusi dalla minificazione ulteriore grunt)
Initializer Java
Inoltre nell'initializer andrà di fatto fatto un import delle risorse in base al flag useCompressed. Nel caso di useCompressed true, il codice è abbastanza standard, mentre in caso contrartio varia in base ai file necessari nel plugin. Esempio:
//issue #1404 //----------- Boolean useCompressed = ConfigurationProperties.getInstance().getBoolean("useCompressed", false); if(useCompressed) { //these files are produced with grunt executing from the geowebframework folder the command: npm run build //MAPPING PLUGIN JS RESOURCES gwRegistry.addJsResource("build/gw-advanced-externals.min.js"); //geoweb stuff after external libs gwRegistry.addJsResource("build/gw-advanced.min.js"); // MAPPING PLUGIN CSS RESOURCES gwRegistry.addCssResource("build/gw-advanced-externals.min.css"); //geoweb stuff after external libs gwRegistry.addCssResource("build/gw-advanced.min.css"); }else { //MAPPING PLUGIN JS RESOURCES //note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js //note: use moment-with-locales.js to enable vis timetable localization support (for some languages) //gwRegistry.addJsResource("moment/moment.min.js"); gwRegistry.addJsResource("moment/moment-with-locales.min.js"); gwRegistry.addJsResource("moment-round/moment-round.min.js"); gwRegistry.addJsResource("vis/dist/vis.min.js"); gwRegistry.addJsResource("vis/dist/vis-timeline-graph2d.min.js"); gwRegistry.addJsResource("async/dist/async.min.js"); //geoweb stuff after external libs gwRegistry.addJsResource("debug/js/gwCalendar.js"); gwRegistry.addJsResource("debug/js/gwSelectionListWidget.js"); gwRegistry.addJsResource("debug/js/gwLabelValuesXmlWidget.js"); gwRegistry.addJsResource("debug/js/gwFinalBalanceJobsWidget.js"); gwRegistry.addJsResource("debug/js/gwAssignmentWP.js"); gwRegistry.addJsResource("debug/js/gwTimeTable.js"); gwRegistry.addJsResource("debug/js/gwDynamicTimeLine.js"); gwRegistry.addJsResource("debug/js/gwGantt.js"); gwRegistry.addJsResource("debug/js/leafItemGHFMC.js"); // MAPPING PLUGIN CSS RESOURCES gwRegistry.addCssResource("vis/dist/vis.min.css"); gwRegistry.addCssResource("vis/dist/vis-timeline-graph2d.min.css"); //geoweb stuff after external libs gwRegistry.addCssResource("css/gwEnterprice/gwCalendar.css"); gwRegistry.addCssResource("css/gwEnterprice/gwTimeTable.css"); gwRegistry.addCssResource("css/gwEnterprice/gwDynamicTimeLine.css"); gwRegistry.addCssResource("css/gwEnterprice/gwGantt.css"); } //-----------