Frontend

Vue Component Library

Lettura 12 minuti

Una libreria di componenti che problema risolve?

In questo articolo andremo ad affrontare il tema della scalabilità di un’applicazione software, in particolare andando a vedere come la creazione di una libreria di componenti sotto forma di repository separato dall’applicazione principale risolva in parte il problema di avere tutto il codice sotto un unico monolite. Suddividere il codice in più repository, pubblicarli come pacchetti NPM sperarati e includerli come dipendenze versionate, rende l’applicazione più snella e scalabile nel tempo. Inoltre gli stessi componenti della libreria possono essere utilizzati su più applicazioni differenti evitando così duplicazione di codice. È stata creata da zero per questo articolo una libreria fatta in Vuejs 2.x di componenti UI (ce ne sono 2 di esempio al suo interno) esportata come plugin.

Obiettivo finale

L’obiettivo finale come abbiamo detto sarà quello di avere una libreria di componenti in cui ogni componente avrà una sua cartella dedicata, che conterrà il componente stesso, il test unitario e un file di stories. Tutto quello che vedremo da qui in avanti e i relativi frammenti di codice fanno riferimento a questo repository di GitHub che ho creato appositamente per questo articolo.

Cosa contiene nel dettaglio

  • 2 componenti UI di esempio (Button - InputText).
  • Store di Vuex dedicato alla libreria salvato sotto un namespace dedicato.
  • Storybook di componenti.
  • Suite di test in Jest.

Get Started

Partiamo con il creare una normalissima app in Vue tramite la CLI. Nel momento in cui sto scrivendo questo articolo la versione di Vue utilizzata è la 2.6 mentre quella della Vue CLI è la 4.5.15. Partiamo con il wizard selezionando a mano le feature che ci interessano.

  • ◉ Babel
  • ◯ TypeScript
  • ◯ Progressive Web App (PWA) Support
  • ◯ Router
  • ◉ Vuex
  • ◯ CSS Pre-processors
  • ◉ Linter / Formatter -> Airbnb (Lint on save)
  • ◉ Unit Testing -> JEST
  • ◯ E2E Testing

Modifichiamo la struttura di file e cartelle

Adesso che abbiamo una classica app in Vue possiamo modificare la struttura di file e cartelle per ottenere quella che sarà alla fine la nostra libreria. Cominciamo con il cancellare la cartella public e la cartella tests dalla root. All’interno della cartella src cancellare anche assets, App.vue e il componente HelloWorld.vue di default. La cartella components, ormai vuota, verrà popolata dai nostri componenti organizzati in utleriori sottocartelle. Nella cartella components aggiungeremo una index.js in cui verranno esportati tutti i singoli componenti:

import Button from './button/Button.vue';
import InputText from './inputText/InputText.vue';

export default { Button, InputText };

Ogni componente, a sua volta, avrà 3 file principali:

  • il file del componente Component.vue
  • Il file relativo allo Storybook Component.stories.js (Lo installeremo tra poco)
  • Il file relativo al Test Component.spec.js

Infine nella root di src, abbiamo lasciato il nostro file main.js che andrà modificato per far sì che nella funzione di install vengano ciclati tutti i componenti presi dall’export della index e venga esportato il tutto (compreso lo store) come plugin registrando globalmente ogni componente sull’istanza di Vue.

import components from './components/index';
import store from './store';

const ComponentLibrary = {
  install(Vue = {}, options) {
    // Lo store Vuex, registrato sotto il namespace 'vueComponentLibraryStore',
    // viene passato come option al momento del suo utilizzo come plugin
    options.store.registerModule('vueComponentLibraryStore', store);

    // Ciclo i componenti esportati da components/index e li registro globalmente sull'istanza di Vue.
    Object.keys(components).forEach((key) => {
      const component = components[key];
      Vue.component(component.name, component);
    });
  },
};

export default ComponentLibrary;

Storybook

Adesso che siamo pronti con la struttura base della nostra libreria, possiamo installare lo Storybook, strumento utilissimo se dobbiamo sviluppare e documentare in modo semplice e veloce (tramite le stories) componenti in un contesto isolato. Una singola storia quindi acquisisce lo stato di rendering del componente a cui quella storia fa riferimento. Si possono scrivere più storie per componente e ogni storia descrive tutti gli stati che un componente può supportare. Per installare lo Storybook su un’app in Vue già esistente, basta seguire le istruzioni indicate sulla documentazione ufficiale.

# Add Storybook:
npx sb init

Il comando installerà in pochi secondi tutto il necessario per avviare in locale lo Storybook, creando nella root di src una cartella chiamata stories, con alcuni esempi all’interno. Non ci serve e possiamo tranquillamente cancellarla. Nella root del progetto abbiamo anche una cartella .storybook con all’interno un file main.js. che andrà modificato affinché vada a pescare tutti i file di stories all’interno di components.

module.exports = {
  "stories": [
    "../src/components/**/*.stories.mdx",
    "../src/components/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ],
  "framework": "@storybook/vue"
}

Siamo pronti per creare il nostro primo componente: un pulsante. Non dimentichiamo che tutto quello che stiamo vedendo è ovviamente molto semplificato e anche il componente stesso di UI è ridotto al minimo per dare più spazio ai concetti generali di libreria di componenti.

Button component

Come abbiamo già detto più volte ogni componente sarà composto da 3 file. È arrivato il momento di vederli nel dettaglio.

Button.vue

Partiamo con il file del componente Vue. È un semplicissimo pulsante con 4 prop (size, theme, label e disabled) e al click viene dispacciato un evento ‘btnLibClicked’. Anche lo stile è molto basico: contiene giusto le regole base e alcuni modificatori per cambiare la dimensione del pulsante stesso. Il codice completo del pulsante lo trovate nel repository che ho creato per questo articolo.

<template>
  <button
    class="Button"
    :class="classes"
    :disabled="disabled"
    @click="onClickBtn"
  >
    {{ label }}
  </button>
</template>

Button.spec.js

Il secondo file è il test. Se non abbiamo conoscenze di Unit testing e Vue è possibile approfondire le basi qui. Ci sarebbero moltissime cose da dire anche qui. Cerchiamo di analizzarlo a grandi linee. Prima di tutto importiamo il nostro componente Button e creiamo il nostro wrapper che contiene il componente Vue montato e renderizzato. Il nostro wrapper può essere creato con o senza options. In questo particolare caso, abbiamo utilizzato propsData per impostare una prop (label) quando il componente è montato. Il nostro expect infine prevede che sull’istanza del componente appena montato il valore della prop label sia proprio ‘Lorem ipsum’. Il nostro test è terminato. Ripeto, abbiamo scalfito solo la superficie.

import { mount } from '@vue/test-utils';
import Button from '@/components/button/Button.vue';

describe('Button.vue', () => {

  const wrapper = mount(Button, {
    propsData: {
      label: 'Lorem ipsum'
    }
  });

  it('renders props.label when passed', () => {
    expect(wrapper.props().label).toBe('Lorem ipsum');
  });

});

Cosa importante prima di continuare è modificare il file jest.config.js cambiando il path del testMatch così che vada a matchare tutti i file di test all’interno della cartella components.

module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  testMatch: ['**/components/**/*.spec.js?(x)'],
};

Button.stories.js

L’ultimo file da creare è proprio il file dello Storybook. Prima abbiamo visto come installarlo, adesso siamo pronti per creare la nostra prima storia, che come dicevo rappresenta un particolare stato del componente stesso. Possiamo creare una storia nel modo più semplice possibile e cioè replicando staticamente tutte le varianti e prop ma non è sicuramente un modo molto efficace per descrivere i nostri elementi di UI. Soprattutto per quelli molto complessi. Ecco un esempio di come potrebbe essere scritto:

import Button from './Button.vue';

export default {
  title: 'Vue UI Library/Button',
  component: Button,
};

export const Small = () => ({
  components: { Button },
  template: '<Button size="small" label="Button Label" />',
});

export const Medium = () => ({
  components: { Button },
  template: '<Button size="medium" label="Button Label" />',
});

Proviamo a fare un passo in avanti ultilizzando gli Storybook args, che ci consentono di comporre gli argomenti in modo dinamico per poterne avere poi più controllo tramite gli argTypes e modificarne i valori tramite i controlli. In particolare gli Storybook Controls offrono la possibilità di interagire dinamicamente con gli args di un componente senza bisogno di modificare il codice. Avremo a disposizione select, input, switch, radiobox, etc per poter modificare live il nostro componente. Possiamo specificare quali controlli utilizzare, dichiarando un argType personalizzato. Gli ArgType sono una funzionalità di Storybook per specificare il comportamento degli args. In questo caso per esempio ho deciso di usare una select per modificare il valore della prop size del componente.

export default {
    title: 'Vue UI Library/Button',
    component: Button,
    argTypes: {
        size: {
            control: {type: 'select'},
            options: ['small', 'medium', 'large'],
        },
    }
}

Ed ecco un esempio di come si possono usare gli args per definire un template e associarlo a tutte le storie di un componente. Ogni storia può definire alcuni valori di default per costruire uno stato particolare del componente e visualizzarlo cosi nella barra laterale come elemento della lista. Ad esempio qui stiamo definendo una storia che descrive un pulsante di tipo small, il cui testo è Button.

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { Button },
  template: '<Button v-bind="$props" />',
});

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

La cosa comoda è che questi args sono definiti a livello di storia ma possono facilmente essere utilizzati cross stories tramite lo spread operator. Quindi se dobbiamo replicare un pulsante di tipo small ma con una label differente o proprietà differenti, possiamo farlo nel modo seguente.

export const SmallLongName = Template.bind({});
SmallLongName.args = {
  ...Small.args,
  label: 'Primary with a really long name',
};

Infine possiamo avere anche args a livello di componente. Questi tipi di argomenti, definiti cosi, verranno ereditati da tutte le stories del componente stesso.

export default {
  title: 'Button',
  component: Button,
  args: {
    size: 'small',
  },
};

Ci sono tante altre feature e addons interessanti da poter utilizzare. Per questo possiamo fare riferimento alla documentazione ufficiale in cui troveremo tutte le informazioni necessarie per scrivere delle storie complete di tutto.

File package.json e processo di build

Anche questo file deve essere leggermente modificato affinché tutto funzioni correttamente. Di seguito ecco una lista di comandi importanti da tenere sempre sottomano:

  • test:unit -> Lancia la suite test in Jest (ogni componente ha il suo test)
  • lint -> Lancia un check del codice in base al tipo di preset scelto durante la creazione del progetto
  • serve:storybook -> Lancia il serve locale dello Storybook
  • build:storybook -> Crea una build dello Storybook
  • build:lib -> Crea una build della libreria esportandola in formato UMD e common.js

In particolare il comando di build deve essere leggermente modificato rispetto alla versione generata dalla CLI in quanto bisogna aggiungere il flag --target lib per poter avere un pacchetto di file idoneo da essere incluso in un secondo momento. Oltre al target possiamo definire il nome della libreria e l’entry point della stessa. Ecco il comando completo:

"build:lib": "vue-cli-service build --target lib --name vue-component-library src/main.js"

Altra nota importante sul flag –target lib. Le dependencies del package sono state svuotate. In particolare la dipendenza di Vue non c’è piú. La scelta è nostra, avremmo anche potuto lasciarla in quanto in modalità “lib”, Vue viene esternalizzato. Ciò significa che il bundle non includerà Vue anche se il tuo codice importa Vue. Per evitare questo comportamento, basta inserire il flag –inline-vue.

Infine nel campo “main”, che definisce l’entry point per accedere alla nostra libreria, andrà aggiunto proprio il file ‘dist/vue-component-library.umd.min.js’ prodotto dal processo di build. Più nel dettaglio il processo di build genera diversi file. Analizziamoli insieme:

  • dist/myLib.common.js: CommonJS bundle da usare tramite bundler webpack (attualmente webpack non supporta ancora il formato di output dei moduli ES per i bundle).
  • dist/myLib.umd.js: UMD bundle da usare direttamente nel browser o tramite AMD loader
  • dist/myLib.css

Come utilizzare in locale la libreria (senza doverla pubblicare su NPM)

Per prima cosa bisogna avviare il processo di build direttamente dalla libreria stessa. Se non si vuole esportare su NPM la libreria, soprattutto in una prima fase di test della libreria stessa in cui si fanno molte modifiche strutturali, si può usare ad esempio yarn nel repository che la ospiterà digitando yarn add ../vue-component-library (il percorso ovviamente dipende dalla posizione dei due repository). Questo comando aggiungerà la libreria (locale) tra le dipendenze del package.json.

Inclusione della libreria

Per utilizzare la libreria come plugin, e quindi disponibile globalmente in tutta l’app, basterà includerla nel main.js del repo ospitante insieme all’eventuale stile prodotto dal processo di build. Ricordiamoci che abbiamo anche a disposizione un sottomodulo dello store dedicato alla libreria che troverai sotto il nome di ‘vueComponentLibraryStore’.

import Vue from 'vue';
import VueComponentLibrary from 'vue-component-library';
import App from './App.vue';
import store from './store';
import 'vue-component-library/dist/vue-component-library.css';

Vue.config.productionTip = false;

Vue.use(VueComponentLibrary, { store });

new Vue({
  store,
  render: (h) => h(App),
}).$mount('#app');

Infine, per utilizzare la libreria basterà includere i componenti e usarli come qualsiasi altro componente scritto in Vue:

<template>
  <div class="LibraryTest">
    <InputText label="Prova label" @inputLibChange="onLibraryCatchEvent"/>
    <Button @btnLibClicked="onLibraryCatchEvent" />
  </div>
</template>

Conclusioni

Siamo arrivati alla fine di questa panoramica. Spero di aver dato le basi per continuare ad approfondire autonomamente tutte le tematiche trattate. Per forza di cose non si poteva trattare tutto in maniera molto dettagliata in un solo articolo ma sono sicuro che partendo da quello che abbiamo visto insieme si possono sperimentare tante altre cose. Per farlo ricorda che a questo link è presente un repository per cominciare a costruire la tua libreria di componenti, migliorando ulteriormente i test già presenti, aggiungendo altri componenti e descrivendoli tramite le stories. In futuro potrebbero arrivare altri articoli inerenti a questo argomento. Non abbiamo trattato il versionamento dei pacchetti su npm, la gestione degli stessi tramite Monorepo, l’utilizzo di altri module bundler come ad esempio Rollup. Di argomenti ce ne sono tanti.

È bene ricordare un’ultima cosa che ho imparato in questi anni di sviluppo e che si può riassumere nella frase don’t reinvent the wheel. In concreto cosa significa ciò? Che questo che abbiamo analizzato oggi è sicuramente un buon punto di partenza per creare una libreria di componenti di UI completamente custom ma online si trovano tantissime valide librerie per Vue che offrono set molto completi di componenti. Dobbiamo essere in grado di riconoscere quale di quelle fa più al caso nostro e al contesto in cui si lavora.

Ecco una lista di librerie di componenti e alcuni spunti utili presi direttamente dalla Vue Community per capire quale libreria fa al caso nostro:

“Which UI library is the best” stands among the most common questions from newcomers to Vue.js ecosystem. The answer isn’t easy though, since there is no such thing as “the best” components library for Vue, due to many factors that should be considered. What you can look for instead is a solution optimal for your specific use case.

Per concludere lascio un ultimo link raccolto durante il talk di Debbie O’Brien al JsDay 2022 di Verona. Si tratta di una libreria di componenti basata su Tailwind - “Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.”

Bibliografia

  1. https://itnext.io/create-a-vue-js-component-library-as-a-module-part-1-a1116e632751 (part 1)
  2. https://itnext.io/create-a-vue-js-component-library-part-2-c92a42af84e9 (part 2)
  3. https://www.xiegerts.com/post/creating-vue-component-library-introduction/ (serie di articoli)
  4. https://blog.logrocket.com/building-vue-3-component-library/ (vue3)
  5. https://blog.harveydelaney.com/creating-your-own-vue-component-library/
  6. https://javascript.plainenglish.io/how-to-create-test-bundle-vue-components-library-8c4828ab7b00

Articolo scritto da

Front-end dev | Milano

☝ Ti piace quello che facciamo? Unisciti a noi!