Introdurre Vue in un progetto esistente
Vue.js viene definito come una libreria progressiva, una libreria che può essere introdotta incrementalmente in un progetto esistente.
Ho deciso di provare questa caratteristica introducendo Vue in Resting, un’estensione per browser rilasciata con licenza Open Source che permette di eseguire chiamate verso API HTTP/REST.
Resting è nato qualche anno fa ed è stato implementato usando Knockout.js e Bootstrap. L’intento di questo esperimento è introdurre Vue nel modo più conservativo possibile e valutare i problemi di convivenza con altre tecnologie.
Trovare un buon candidato
Quale componente di Resting ha la giusta complessità per provare la conversione?
Un buon pretendente è il pulsante che copia negli appunti la response di una chiamata:
- la logica della funzionalità è isolata.
- il codice dipende da un altro modulo, quindi permette di valutare la comunicazione con il codice esistente.
Ho deciso di utilizzare Vue 2.6
invece della più nuova 3.x
. Non ho esperienza con la 3.x
e ho preferito evitare un’ulteriore livello di incertezza nell’esperimento.
Le difficoltà
Primo scoglio: Resting utilizza require.js
per gestire le dipendenze. Come si comporta Vue con questa modalità di caricamento?
Le operazioni da eseguire:
-
Copiare il file di
vue.js
(nel mio caso full, compilatore + runtime) nella cartella delle dipendenze terze. -
Definire la dipendenza Vue nella configurazione di
require.js
requirejs.config({
baseUrl: 'js/vendor',
paths: {
app : '../app',
component : '../app/components',
'jquery': 'jquery-3.3.1.min',
'knockout': 'knockout-3.4.2',
'knockout-secure-binding': 'knockout-secure-binding',
'localforage': 'localforage.nopromises.min',
'hjls': 'highlight.pack',
---> 'Vue': 'vue'
}
});
- Scrivere il codice del componente
component/clip.js
.
define(['Vue','app/clipboard'],function(Vue, clipboard) {
Vue.component('ClipboardButton', {
created: function() {
clipboard.copyFrom('#highlighted-response', 'copy-n-paste');
clipboard.onCopy(function() {
$('.alert').removeClass('hide');
setTimeout(function () { $('.alert').addClass('hide'); }, 2000);
});
},
methods: {
push() {
document.execCommand('copy')
}
},
template: `
<button @click.prevent.stop="push" title="Copy to clipboard" class="copy-n-paste">
<i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
`
})
})
- Iniettare la dipendenza a Vue e a
component/clip
nella parte di applicazione che utilizza il componente.
define(['knockout','jquery','hjls','app/clipboard', 'app/bacheca','Vue','component/clip'],function(ko,$,hjls,clipboard,bacheca, Vue) {
- Definire un’applicazione Vue che si occupi di gestire il workflow del nuovo componente.
var vueApp = new Vue({
el: '#vue-stuff'
})
- Introdurre nella
view
un elementodiv
che contenga il componente.
<div id="vue-stuff">
<div class="btn-group" style="float: left" role="group">
<clipboard-button></clipboard-button>
</div>
I passaggi appena descritti sono sufficienti per introdurre Vue in un’applicazione che utilizza require.js
.
Secondo scoglio: Resting è però un’estensione per browser, deve quindi sottostare ad una serie di limitazioni ulteriori, una di serie di policy CSP (Content Security Policy). Provando a caricare l’estensione nel browser non visualizzaremo ancora il pulsante creato, e nella console verrà mostrato il seguente messaggio di errore:
Content Security Policy: The page’s settings blocked the loading of a resource at eval (“script-src”).
Soluzione rapida: introdurre nel file manifest.json
delle regole CSP più permissive.
"content_security_policy" : "default-src * 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'self'",
Con questo permesso il componente viene visualizzato e funziona correttamente.
La soluzione appena descritta non mi soddisfa: inserire delle clausole così permissive nel manifest presuppone l’insorgere di problemi durante il processo di review sui vari marketplace.
A cosa è dovuto l’errore?
L’errore è dovuto all’utilizzo della versione full di Vue che contiene sia il compilatore dei template HTML che il runtime dell’applicazione. Il compilatore utilizza la funzione eval
per creare il codice Javascript a partire dai template HTML del componente.
La descrizione di questo problema è descritta nella documentazione ufficiale di Vue, qui.
La soluzione: compilare preventivamente i template HTML presenti nei componenti Vue. Per farlo possiamo utilizzare gli strumenti presenti nell’ecosistema Vue come vue-cli
.
I passi per arrivare a questa soluzione:
- Sostituire la libreria
vue.min.js
con la libreriavue.runtime.min.js
fra le dipendenze di progetto e modificare la dipendenza caricata darequire.js
.
'Vue': 'vue.runtime.min'
- Installare nel progetto
vue-cli
.
npm install @vue/cli
- Scrivere un comando per la compilazione dei componenti Vue utilizzando
vue-cli
.
${CLI_PATH}/vue-cli-service build --target lib --formats umd --dest ${DIST_FOLDER} --no-clean --name clipboard-button ${COMPONENTS_FOLDER}/ClipboardButton.vue
vue-cli-service
compila solo i file con estensione .vue
, quindi rinominare il componente clip.js
in ClipboardButton.vue
. Ho colto l’occasione per dare un nome più in linea con le best practise di Vue.
- Riscrivere il codice del componente.
<template>
<button @click.prevent.stop="push" title="Copy to clipboard" class="copy-n-paste">
<i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</template>
<script>
import bacheca from 'Services/bacheca'
export default {
name: 'ClipBoardButton',
methods: {
push(event) {
bacheca.publish('copyResponse')
}
}
}
</script>
Ora il componente è descritto come un SFC (single file component) e non contiene più riferimenti a Vue.
Verrà compilato dal plugin webpack utilizzato da vue-cli-service
.
Come compilo correttamente le dipendenze al codice definire in require.js?
Introducendo un mapping nella configurazione di webpack
, all’interno del file vue.config.js
.
Il contenuto del file:
const path = require('path')
module.exports = {
configureWebpack: {
resolve: {
alias: {
Components: path.resolve(__dirname, 'src/js/app/components'),
Services: path.resolve(__dirname, 'src/js/app')
}
},
externals: {
'Services/clipboard': 'app/clipboard',
'Services/storage': 'app/storage',
'Services/bacheca': 'app/bacheca',
'Components/RDialog.vue': 'vuecomp/r-dialog.umd'
}
}
}
Nel caso descritto grazie a externals
si può dire a webpack di considerare delle dipendenze come esterne, quindi di non compilarle.
Il mapping avviene tra la stringa che rappresenta la dipendenza che si vuole importare, Services/clipboard
ad esempio, e il path require.js
che carica tale dipendenza, nell’esempio app/clipboard
.
Per semplificare la gestione ho definito anche degli alias
come Services
per non dover scrivere path molto lunghi.
Il comando di compilazione produe un file compilato per ogni ogni componente con il nome in questa forma clipboard-button.umd.js
Ho creato una nuova cartella in vendor
chiamata vue-stuff
per contenere i componenti Vue compilati e l’ho aggiunta alla configurazione require.js
requirejs.config({
baseUrl: 'js/vendor',
paths: {
app : '../app',
component : '../app/components',
vuecomp : 'vue-stuff',
'jquery': 'jquery-3.3.1.min',
'knockout': 'knockout-3.4.2',
'knockout-secure-binding': 'knockout-secure-binding',
'localforage': 'localforage.nopromises.min',
'hjls': 'highlight.pack',
'Vue': 'vue.runtime.min'
}
});
Per non incappare nuovamente nel problema CSP ho modificato il codice di inizio dell’applicazione Vue andando a sostituire il template HTML con la sua rappresentazione compilata.
new Vue({
el: '#v-response-b-group',
components: {
ClipboardButton
},
render: function(createElement) {
return createElement(
'div',
{
class: 'btn-group',
role: 'group'
},
[
createElement('clipboard-button')
]
)
}
})
Le modifiche apportare alla parte di applicazione che carica il componente.
define(['knockout','jquery','hjls', 'app/bacheca','Vue','app/clipboard', 'vuecomp/clipboard-button.umd'],function(ko,$,hjls, bacheca, Vue, clipboard, ClipboardButton) {
Il risultato finale.
Come comunicano tra loro i componenti?
Utilizzando un provider pub/sub
molto grezzo che ho scritto per Resting, bacheca. I componenti scritti in Knockout.js già comunicano tra loro attraverso questo provider e lo stesso sistema è perfetto anche per la comunicazione fra componenti di diverse tecnologie dato che mantiene disaccoppiate le logiche.
Prossimi passi
L’esperimento ha avuto successo e non ho avuto problemi durante la review nel marketplace di Firefox e di Chrome: Vue convive bene nell’applicazione esistente.
Nel prossimo futuro:
- Proverò a passare da Vue 2 a 3 (che è diventata la versione ufficiale del progetto).
- Riscriverò i componenti esistenti in Vue.
- Le nuove funzionalità saranno scritte in Vue.
Gli spezzoni di codice presentati in questo articolo seguono la licenze di Resting: GPLv3.