Usare Gulp per sviluppare temi e plugin WordPress
Dev

Usare Gulp per sviluppare temi e plugin WordPress

Gulp è un toolkit JavaScript di cui ho già ampiamente discusso nella mia mini guida all'ottimizzazione dello sviluppo frontend e nel tutorial per generare il Critical CSS. Eviterò quindi di presentarlo una terza volta, se vuoi saperne di più puoi leggere i due articoli che ti ho linkato.

In questo articolo scriveremo ed analizzeremo un Gulpfile completo per lo sviluppo di temi e plugin WordPress.

Struttura delle cartelle

Partiamo da un aspetto fondamentale per far funzionare tutto a dovere, la struttura delle directory. Solitamente nei miei progetti ne adotto una simile:

1inc 2languages 3src 4 |- images 5 |- js 6 |- sass 7template-parts 8...file vari...

Ho volutamente omesso i vari file, perché non sono altro che file PHP utili a WordPress per mostrare correttamente le informazioni in pagina. Nell'immagine puoi osservare le cartelle inctemplate-parts e languagues, a parte quest'ultima le altre due non verranno manipolate da Gulp, contengono solamente codici utili al tema. Languages, al momento vuota, conterrà successivamente il file pot per tradurre il tema in altre lingue.

Avrai già capito quindi che la cartella alla quale fare maggior attenzione è src, la quale conterrà tutti i nostri file non compilati. Essa contiene un'altra cartella, denominate assets, che a sua volta contiene tre cartelle: imagesjs e sass.

Ora che hai strutturato correttamente il tuo progetto possiamo focalizzarci sull'installazione dei vari packages necessari.

Nel terminale apri la directory di lavoro e digita il seguente comando

1npm init

Segui le istruzioni mostrate nel prompt. Una volta inizializzato correttamente il progetto, esegui il seguente comando, sempre da terminale:

1npm i --save-dev autoprefixer babel-loader babel-preset-env babel-register @babel/core browser-sync cssnano del gulp gulp-if gulp-imagemin gulp-penthouse gulp-postcss gulp-rename gulp-replace gulp-sass gulp-sourcemaps gulp-uglify gulp-wp-pot gulp-zip vinyl-named webpack-stream yargs

Questo comando installerà una sfilza di roba. Vedrai che nella tua working directory sarà comparsa la cartella node_modules, più i file package.json e package-lock.json.

Ti ricordo che la cartella node_modules non deve essere mai trasferita, non perché ciò non è possibile, ma perché date le dimensioni è sempre meglio passare esclusivamente il file package.json ed eseguire il comando npm install che si occuperà di leggere suddetto file e di installare tutte le dipendenze del progetto.

Prima di proseguire con la creazione del file gulp. Devi sapere che scriveremo in JavaScript "moderno", il quale deve essere compilato in JavaScript ES2015. Per il momento ti basterà creare un file .babelrc nella directory principale del progetto e scrivere quanto segue:

1{ 2 "presets": ["babel-preset-env"] 3}

Creiamo il file gulp

Bene, ora che le dependencies sono state installate con successo è giunto il momento di creare un nuovo file nella root directory del progetto chiamato gulpfile.babel.js. Inanzitutto importiamo la dipendenza principale: gulp

1import gulp from "gulp";

Da adesso in poi quando importerò una nuova dipendenza con il comando import dovrai inserirla subito dopo la riga che hai appena scritto, così il tuo file sarà più ordinato.

Importiamo immediatamente le informazioni contenute nel file package.json, ci serviranno più tardi.

1import info from "./package.json";

Il prossimo step è quello di creare una costante con i vari percorsi, i quali recupereremo di volta in volta quando ci serviranno.

1const paths = { 2 styles: { 3 src: ["src/assets/sass/bundle.scss"], 4 dest: "dist/assets/css", 5 }, 6 images: { 7 src: "src/assets/images/**/*.{jpg,jpeg,png,svg,gif,webp}", 8 dest: "dist/assets/images", 9 }, 10 scripts: { 11 src: ["src/assets/js/bundle.js"], 12 dest: "dist/assets/js", 13 }, 14 other: { 15 src: [ 16 "src/assets/**/*", 17 "!src/assets/{images,js,sass}", 18 "!src/assets/{images,js,sass}/**/*", 19 ], 20 dest: "dist/assets", 21 }, 22 languages: { 23 src: "**/*.php", 24 dest: `languages/${info.name}.pot`, 25 }, 26 package: { 27 src: [ 28 "**/*", 29 "!.vscode", 30 "!node_modules{,/**}", 31 "!packaged{,/**}", 32 "!src{,/**}", 33 "!.babelrc", 34 "!.gitignore", 35 "!gulpfile.babel.js", 36 "!package.json", 37 "!package-lock.json", 38 ], 39 dest: "packaged", 40 }, 41};

Wow! Questo sì che è tanto codice... Cerchiamo di fare chiarezza.

La costante paths è un oggetto contenente varie proprietà. Tali proprietà sono a loro volta oggetti i quali contengono due proprietà srcdest, le quali rispettivamente indicano la sorgente dalla quale recuperare il file e la destinazione dove sarlvare il file processato.

In alcuni casi si può notare che la proprietà srccontiene un array (contraddistinto da parentesi quadre) ciò significa che possiamo creare più bundle contemporaneamente, ad esempio se desideriamo un file JavaScript per il frontend e uno per l'area di amministrazione.

Un'altra peculiarità la puoi trovare nella proprietà languages. Nella sua proprietà dest vengono usate le stringhe template (template literals) per interpolare in maniera più elegante la proprietà name di info, il quale ti ricordo è il nostro file package.json, importato qualche paragrafo fa. Cosa singifica? Il file che verrà generato dalla task che scriveremo, farà uso della proprietà languages di paths, che genererà un file con il nome del tuo progetto (l'hai scelto dopo aver scritto npm init nel terminale) con estensione .pot.

Ultima nota è per la proprietà package la quale contiene un array particolarmente popoloso. Il primo elemento indica tutti gli elementi presenti nella working directory comprese le cartelle, i quali verranno trasferiti nella cartella packaged, ad esclusione dei file con il punto esclamativo anteposto, tali file non sono necessari nella versione finale del tema, quindi li escludiamo dal bundle.

Task per generare il foglio di stile

La prima task che andremo a scrivere è quella necessaria a compilare i file SCSS. Importiamo quindi le varie dipendenze:

1import yargs from "yargs"; 2import sass from "gulp-sass"; 3import gulpif from "gulp-if"; 4import sourcemaps from "gulp-sourcemaps"; 5import postcss from "gulp-postcss"; 6import autoprefixer from "autoprefixer"; 7import cssnano from "cssnano";

Le dipendenze rispettivamente servono a:

  • Prelevare argomenti dai comandi da terminale. Li andremo a implementare successivamente;
  • Compilare i file da SCSS a CSS
  • Eseguire dei metodi solo in base ad alcune specifiche condizioni
  • Generare la sourcemap. Risulterà molto utile in fase di sviluppo per individuare in quale file SCSS sono riportate le proprietà che stiamo esaminando, invece di visualizzare un anonimo file minificato.
  • Processare il CSS con i plugin configurati nella task
  • Il plugin che useremo con postcss per inserire dove necessario i prefissi alle varie proprietà CSS che ne necessitano.
  • Un altro plugin che useremo con postcss per minificare il CSS.

Terminate le importazioni creiamo una nuova costante, la quale conterrà un valore booleano in base alla presenza o meno di un argomento nel comando che inizializza la task, il quale identificherà se la compilazione è per l'ambiente di sviluppo o di produzione.

1const PRODUCTION = yargs.argv.prod;

Ora che c'è tutto possiamo procedere a scrivere la task.

1export const styles = () => { 2 let processors = [autoprefixer]; 3 if (PRODUCTION) processors.push(cssnano); 4 return gulp 5 .src(paths.styles.src) 6 .pipe(gulpif(!PRODUCTION, sourcemaps.init())) 7 .pipe(sass().on("error", sass.logError)) 8 .pipe(postcss(processors)) 9 .pipe(gulpif(!PRODUCTION, sourcemaps.write())) 10 .pipe(gulp.dest(paths.styles.dest)); 11};

Per prima cosa definiamo un'array di processori (plugin) che daremo in pasto a postcssquando sarà il momento. Di base c'è autoprefixer, se la costante PRODUCTION è vera, verrà aggiunto anche cssnano per la minificazione.

Dopo di che recuperiamo il file SCSS da processare (ricorda che può essere anche un array). Eseguiamo quindi i seguenti passaggi:

  • Se non siamo in produzione, inizia la generazione della sourcemap
  • Compila i file SCSS
  • Processa il codice CSS generato con postcss e i relativi plugin
  • Se non siamo in produzione, scrivi la sourcemap
  • Salva il file compilato nella destinazione specificata nella proprietà dell'oggetto styles, a sua volta proprietà della costante paths.

In dest riporto la cartella dist, la quale non esiste. Verrà creata in automatico dallo script.

Task per processare le immagini

La prossima task serve a minimizzare le immagini, allegerendole. Prima di tutto importiamo la dipendenza:

1import imagemin from "gulp-imagemin";

Scriviamo quindi la task:

1export const images = () => { 2 return gulp 3 .src(paths.images.src) 4 .pipe(gulpif(PRODUCTION, imagemin())) 5 .pipe(gulp.dest(paths.images.dest)); 6};

Se hai capito il meccanismo prima, qui è ancora più semplice. Ogni immagine, se la costante PRODUCTION è uguale a true, viene minificata e salvata nel percorso di destinazione. Nella nostra costante paths ho definito i seguenti formati jpg, jpeg, png, svg, gif, webp. Se te ne servono altri aggiungili.

Task per processare JavaScript

Forse la task più complessa di tutto il file, ma la spunteremo anche stavolta.

Come sempre importiamo le dipendenze:

1import webpack from "webpack-stream"; 2import uglify from "gulp-uglify";

e scriviamo la task:

1export const scripts = () => { 2 return gulp 3 .src(paths.scripts.src) 4 .pipe(named()) 5 .pipe( 6 webpack({ 7 mode: !PRODUCTION ? "development" : "production", 8 module: { 9 rules: [ 10 { 11 test: /\.js$/, 12 use: { 13 loader: "babel-loader", 14 options: { 15 presets: ["babel-preset-env"], 16 }, 17 }, 18 }, 19 ], 20 }, 21 output: { 22 filename: "[name].js", 23 }, 24 externals: { 25 jquery: "jQuery", 26 }, 27 devtool: !PRODUCTION ? "inline-source-map" : false, 28 }) 29 ) 30 .pipe(gulpif(PRODUCTION, uglify())) 31 .pipe(gulp.dest(paths.scripts.dest)); 32};

Non è niente di impossibile, solo un mucchio di parentesi da aprire e chiudere. Analizziamola con ordine:

  • Vengono recuperati i file JavaScript da compilare
  • Viene eseguito webpack, noto module bundler, il quale tradurrà i file ES2019 in ES2015 grazie al plugin Babel
    • Si esplicita il nome del file di output che manterrà lo stesso nome del file originale
    • Si fa presente che come libreria esterna si usa jQuery (ancora molto utilizzata da WordPress)
    • Nel caso non ci trovassimo in produzione verrà generata una sourcemap
  • Se siamo in produzione il file appena compilato verrà minificato
  • Il file compilato è pronto e viene salvato nella cartella di destinazione dei file JavaScript.

Easy, no?

Task per creare i file di traduzione

La vera peculiriatà di questo file è proprio questa task che scansiona i file PHP alla ricerca di tutte le stringhe di testo hardcoded, dove si fa uso delle funzioni di WordPress __()_e()_x(), ecc.

Come sempre, prima le dipendenze:

1import wpPot from "gulp-wp-pot";

e poi la task:

1export const pot = () => { 2 return gulp 3 .src("**/*.php") 4 .pipe( 5 wpPot({ 6 domain: `${info.name}`, 7 package: info.name, 8 }) 9 ) 10 .pipe(gulp.dest(`languages/${info.name}.pot`)); 11};

Cosa succede? Niente di più semplice:

  • Tutti i file PHP vengono sottoposti a scansione
  • Vengono individuate le stringhe con dominio uguale al nome del progetto
  • Viene generato il file potnella cartella languagesdel progetto.

Cosa farsene di quel file .pot? Tramite software come Poedit potrai tradurre quelle stringhe in altre lingue, i file generati dal programma dovranno essere salvati nella cartella languages e WordPress si occuperà del resto. Per ulteriori informazioni dai un'occhiata alla sezione dedicata in documentazione.

Altre task utili

Le task principali sono state analizzate, ci sono comunque altre task che possono essere incluse e le vediamo qui di seguito.

Watch

1export const watch = () => { 2 gulp.watch("src/assets/sass/**/*.scss", styles); 3 gulp.watch("src/assets/js/**/*.js", gulp.series(scripts, reload)); 4 gulp.watch("**/*.php", reload); 5 gulp.watch(paths.images.src, gulp.series(images, reload)); 6 gulp.watch(paths.other.src, gulp.series(copy, reload)); 7};

Nessuna dipendenza a sto giro, a parte ovviamente gulp, che abbiamo già importato all'inizio.

La task non farà altro che osservare eventuali cambiamenti nei vari file specificati nel primo parametro del metodo watch e qualora si verificassero, eseguirà la funzione o le funzioni definite nel secondo parametro.

Se osservi con attenzione noterai che due funzioni non esistono ancora nel nostro gulp file. Hai capito quali?

Serve e Reload

La prima funzione che chiamiamo in watch è reload, la quale va in coppia con serve. Cosa fanno? Evitano di farci pigiare ogni volta F5 e ci servono il sito sulla porta 3000 così da poter svolgere il debug anche via dispositivo mobile.

Importiamo le dipenze:

1import browserSync from "browser-sync";

Successivamente creiamo una costante che inizializzerà il tutto:

1const server = browserSync.create();

Creiamo le due task

1export const serve = (done) => { 2 server.init({ 3 proxy: `localhost/${info.name}`, 4 }); 5 done(); 6}; 7export const reload = (done) => { 8 server.reload(""); 9 done(); 10};

Servesi occuperà della creazione del local server. Occhio! Sono comunque necessari software come XAMPP per l'esecuzione di PHP e di WordPress in generale. Mentre reload, beh penso sia chiaro il suo compito.

Dopo la creazione di queste due task è possibile modificare la task degli stili aggiungendo questa riga di codice subito dopo l'ultima pipe(), integrandola con browserSync.

1.pipe(server.stream());

Copy

L'altra task presente in Watch, ma non ancora implementata è copy. Il suo compito è quello di copiare i file da una directory all'altra.

Nessuna dipendenza, solo la task:

1export const copy = () => { 2 return gulp.src(paths.other.src).pipe(gulp.dest(paths.other.dest)); 3};

Critical CSS

Ci ho scritto un articolo a riguardo, te la riporto qui di seguito per completezza, ma per saperne di più vai a leggerti l'articolo.

1export const critical = () => { 2 return gulp.src(`${paths.styles.dest}/bundle.css`).pipe( 3 gulpCriticalCss({ 4 out: "critical.php", 5 url: `http://localhost/${info.name}`, 6 width: 1400, 7 height: 900, 8 userAgent: 9 "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 10 }) 11 ); 12};

Compress

Questa task è la mia preferita. In pratica crea una cartella zippata con tutti i file del tema minificati e pronti per la produzione, ma non solo. Ricordi che la task per la generazione del file pot cerca tutte le stringhe con il text domain uguale al nome del progetto? Per poter riutilizzare questo file in altri progetti, puoi scrivere come dominio di testo nei tuoi file php, una stringa tipo '_themename' e farla sostituire a gulp in fase di compilazione. Fico no?

Importiamo le dipendenze:

1import zip from "gulp-zip"; 2import replace from "gulp-replace";

E scriviamo la task:

1export const compress = () => { 2 return gulp 3 .src(paths.package.src) 4 .pipe(replace("_themename", info.name)) 5 .pipe(zip(`${info.name}.zip`)) 6 .pipe(gulp.dest(paths.package.dest)); 7};

Il comportamento è proprio quello riportato qualche riga fa. Sostituisce tutti i text domain placeholder con il nome del progetto, dopo di che zippa tutto e salva nella cartella indicata.

Clean

Prima di rigenerare i file CSS, JS e copiare i vari file necessari è sempre buona cosa eliminare la cartella distprima di farla ricreare allo script. La task clean fa proprio questo.

Importiamo la dipendeza:

1import del from "del";

e scriviamo la task:

1export const clean = () => del(["dist"]);

Semplice semplice.

Ultimi ritocchi

Ci siamo quasi, dobbiamo definire le task che eseguiranno in serie le varie task, così da non doverle eseguire manualmente una per volta. Ne predisporremo tre: una per lo sviluppo, una che compili tutto il progetto e una che compili il progetto e che crei anche la cartellina zippata. Useremo i metodi series e parallel di gulp, quindi niente nuove dipendenze da importare.

1export const dev = gulp.series( 2 clean, 3 gulp.parallel(styles, scripts, images, copy), 4 serve, 5 watch 6); 7export const build = gulp.series( 8 clean, 9 gulp.parallel(styles, scripts, images, copy, pot, critical) 10); 11export const bundle = gulp.series(build, compress); 12 13export default dev;

L'ultima riga indica che la task di default è dev, quella destinata allo sviluppo.

Il tocco finale lo diamo modificando il file package.json dove alla proprietà scripts, elimineremo ciò che è stato scritto di default e lo sostituiremo con quanto segue:

1"scripts": { 2 "starts": "gulp", 3 "build": "gulp build --prod", 4 "bundle": "gulp bundle --prod" 5 }

Salvato il file, potremo scrivere a terminale:

  • npm start per eseguire le task dev,
  • npm build per eseguire le task di build, impostando la costante PRODUCTION a vero
  • npm bundle per eseguire le task di build, più la compressione nella cartella zip e impostando la costante PRODUCTION a vero.

Per completezza ecco il file completo:

1import gulp from "gulp"; 2import yargs from "yargs"; 3import sass from "gulp-sass"; 4import gulpif from "gulp-if"; 5import sourcemaps from "gulp-sourcemaps"; 6import imagemin from "gulp-imagemin"; 7import del from "del"; 8import webpack from "webpack-stream"; 9import uglify from "gulp-uglify"; 10import named from "vinyl-named"; 11import browserSync from "browser-sync"; 12import zip from "gulp-zip"; 13import replace from "gulp-replace"; 14import wpPot from "gulp-wp-pot"; 15import gulpCriticalCss from "gulp-penthouse"; 16import postcss from "gulp-postcss"; 17import autoprefixer from "autoprefixer"; 18import cssnano from "cssnano"; 19import info from "./package.json"; 20const server = browserSync.create(); 21const PRODUCTION = yargs.argv.prod; 22const paths = { 23 styles: { 24 src: ["src/assets/sass/bundle.scss"], 25 dest: "dist/assets/css", 26 }, 27 images: { 28 src: "src/assets/images/**/*.{jpg,jpeg,png,svg,gif,webp}", 29 dest: "dist/assets/images", 30 }, 31 scripts: { 32 src: ["src/assets/js/bundle.js"], 33 dest: "dist/assets/js", 34 }, 35 other: { 36 src: [ 37 "src/assets/**/*", 38 "!src/assets/{images,js,sass}", 39 "!src/assets/{images,js,sass}/**/*", 40 ], 41 dest: "dist/assets", 42 }, 43 languages: { 44 src: "**/*.php", 45 dest: `languages/${info.name}.pot`, 46 }, 47 package: { 48 src: [ 49 "**/*", 50 "!.vscode", 51 "!node_modules{,/**}", 52 "!packaged{,/**}", 53 "!src{,/**}", 54 "!.babelrc", 55 "!.gitignore", 56 "!gulpfile.babel.js", 57 "!package.json", 58 "!package-lock.json", 59 ], 60 dest: "packaged", 61 }, 62}; 63export const compress = () => { 64 return gulp 65 .src(paths.package.src) 66 .pipe(replace("_themename", info.name)) 67 .pipe(zip(`${info.name}.zip`)) 68 .pipe(gulp.dest(paths.package.dest)); 69}; 70export const critical = () => { 71 return gulp.src(`${paths.styles.dest}/bundle.css`).pipe( 72 gulpCriticalCss({ 73 out: "critical.php", 74 url: `http://localhost/${info.name}`, 75 width: 1400, 76 height: 900, 77 userAgent: 78 "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 79 }) 80 ); 81}; 82export const serve = (done) => { 83 server.init({ 84 proxy: `localhost/${info.name}`, 85 }); 86 done(); 87}; 88export const reload = (done) => { 89 server.reload(""); 90 done(); 91}; 92export const clean = () => del(["dist"]); 93export const styles = () => { 94 let processors = [autoprefixer]; 95 if (PRODUCTION) processors.push(cssnano); 96 return gulp 97 .src(paths.styles.src) 98 .pipe(gulpif(!PRODUCTION, sourcemaps.init())) 99 .pipe(sass().on("error", sass.logError)) 100 .pipe(postcss(processors)) 101 .pipe(gulpif(!PRODUCTION, sourcemaps.write())) 102 .pipe(gulp.dest(paths.styles.dest)) 103 .pipe(server.stream()); 104}; 105export const images = () => { 106 return gulp 107 .src(paths.images.src) 108 .pipe(gulpif(PRODUCTION, imagemin())) 109 .pipe(gulp.dest(paths.images.dest)); 110}; 111export const watch = () => { 112 gulp.watch("src/assets/sass/**/*.scss", styles); 113 gulp.watch("src/assets/js/**/*.js", gulp.series(scripts, reload)); 114 gulp.watch("**/*.php", reload); 115 gulp.watch(paths.images.src, gulp.series(images, reload)); 116 gulp.watch(paths.other.src, gulp.series(copy, reload)); 117}; 118export const copy = () => { 119 return gulp.src(paths.other.src).pipe(gulp.dest(paths.other.dest)); 120}; 121export const scripts = () => { 122 return gulp 123 .src(paths.scripts.src) 124 .pipe(named()) 125 .pipe( 126 webpack({ 127 mode: !PRODUCTION ? "development" : "production", 128 module: { 129 rules: [ 130 { 131 test: /\.js$/, 132 use: { 133 loader: "babel-loader", 134 options: { 135 presets: ["babel-preset-env"], 136 }, 137 }, 138 }, 139 ], 140 }, 141 output: { 142 filename: "[name].js", 143 }, 144 externals: { 145 jquery: "jQuery", 146 }, 147 devtool: !PRODUCTION ? "inline-source-map" : false, 148 }) 149 ) 150 .pipe(gulpif(PRODUCTION, uglify())) 151 .pipe(gulp.dest(paths.scripts.dest)); 152}; 153export const pot = () => { 154 return gulp 155 .src("**/*.php") 156 .pipe( 157 wpPot({ 158 domain: `${info.name}`, 159 package: info.name, 160 }) 161 ) 162 .pipe(gulp.dest(`languages/${info.name}.pot`)); 163}; 164 165export const dev = gulp.series( 166 clean, 167 gulp.parallel(styles, scripts, images, copy), 168 serve, 169 watch 170); 171export const build = gulp.series( 172 clean, 173 gulp.parallel(styles, scripts, images, copy, pot, critical) 174); 175export const bundle = gulp.series(build, compress); 176 177export default dev;

Decisamente un tutorial impegnativo. Se sei arrivato fino a qui, grazie. Se hai dubbi o domande, non devi fare altro che scrivere un commento.

Alla prossima.

;