TravisCI è un famoso servizio di continuous integration usato per testare e buildare le applicazioni e integrato con GitHub.
Netlify è un servizio di cloud hosting per i siti statici, integra il processo di continuous deployment, fornisce un certificato SSL, funzioni serverless, e molto altro... Useremo Netlify con GitHub per deployare il nostro sito ogni volta che pushiamo del nuovo codice.
Creiamo la app
Partiamo dalla creazione di una semplice app Vue per visualizzare tutti i nostri repository su Github. Potete visualizzare il codice qui, e questo sarà il risultato finale:

Gli utenti potranno cliccare sulla tile del repository per navigarci dentro o caricare altri repository da visualizzare.
Creiamo quindi la app con Vue CLI selezionando le opzioni di Unit Testing, Typescript e Linter/Formatter, e utilizziamo il framework Jest per il testing
vue create github-netlify
Andiamo adesso a creare il componente Repository
che renderizza la box del repository con url, nome e descrizione
<template> <a :href="repository.html_url"> <h2>{{repository.name}}</h2> <p>{{repository.description}}</p> </a> </template> <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; @Component export default class Repository extends Vue { @Prop() private repository!: any; } </script>
Nel componente principale App (App.vue
) chiamiamo la url Github https://api.github.com/users/USERNAME/repos
per fetchare tutti i repository appartenenti ad un dato user e renderizziamoli con il componente Repository
. Per rendere l'app facilmente configurabile, memorizziamo lo username in una variabile di ambiente e dichiariamola all'interno del file .env
come VUE_APP_GITHUB_USER=astagi
. Sia Netlify che Vue supportano il file .env
perciò possiamo usarlo tranquillamente per memorizzare tutte le variabili di cui necessitiamo durante lo sviluppo! (Ricordiamoci di aggiungere .env
a .gitignore
)
Il risultato di questa chiamata è una lista paginata di repository, per supportare le pagine aggiungiamo un pulsante "Load more" che utilizzerà il parametro page
.
<template> <div id="app"> <h1>My Github repositories</h1> <Repository v-for="repository of repositories" :key="repository.id" :repository="repository"/> <button v-on:click="fetchRepositories()" :disabled="!loadMoreEnabled">Load more</button> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import axios from 'axios'; import Repository from './components/Repository.vue'; @Component({ components: { Repository, }, }) export default class App extends Vue { private repositories: any[] = []; private page = 1; private loadMoreEnabled = true; private mounted() { this.fetchRepositories(); } public fetchRepositories() { this.loadMoreEnabled = false; axios.get(`https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=${this.page}`) .then((resp) => { this.repositories = this.repositories.concat(resp.data); this.page += 1; }) .finally(() => { this.loadMoreEnabled = true; }); } } </script>
Andiamo a testare adesso il metodo fetchRepositories
, mockando le richieste fatte con la libreria axios con qualche fixture (sono veramente lunghe, qua le fixture complete)!
import { shallowMount } from '@vue/test-utils'; import Vue from 'vue' import App from '@/App.vue'; import reposResponses from '../__fixtures__/reposResponses'; import axios from 'axios' jest.mock("axios"); (axios.get as jest.Mock).mockImplementation((url) => { switch (url) { case `https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=1`: return Promise.resolve({data : reposResponses.page1}); case `https://api.github.com/users/${process.env.VUE_APP_GITHUB_USER}/repos?page=2`: return Promise.resolve({data : reposResponses.page2}); } }); describe('App.vue component', () => { let wrapper: any; beforeEach(() => { wrapper = shallowMount(App); }); it('renders repositories on mount', async () => { await Vue.nextTick(); expect(wrapper.findAll('repository-stub').length).toEqual(30); }); it('fetches other repositories on load more', async () => { await Vue.nextTick(); wrapper.vm.fetchRepositories(); await Vue.nextTick(); expect(wrapper.findAll('repository-stub').length).toEqual(60); }); });
Lanciamo i test
yarn test:unit
In aggiunta ai test è necessario aggiungere il Code Coverage
, ovvero la misura di quante linee, branch, istruzioni del nostro codice vengono eseguite durante il test. Attiviamo il code coverage
in jest.config.js
module.exports = { preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', collectCoverage: true, collectCoverageFrom: ["src/**/*.vue", "!**/node_modules/**"] }
E lanciamo di nuovo i test per vedere il Code Coverage riportato
➜ github-netlify (master) ✗ yarn test:unit yarn run v1.19.2 $ vue-cli-service test:unit PASS tests/unit/app.spec.ts PASS tests/unit/lambda.spec.ts -----------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -----------------|----------|----------|----------|----------|-------------------| All files | 100 | 100 | 100 | 100 | | src | 100 | 100 | 100 | 100 | | App.vue | 100 | 100 | 100 | 100 | | src/components | 100 | 100 | 100 | 100 | | Repository.vue | 100 | 100 | 100 | 100 | | -----------------|----------|----------|----------|----------|-------------------| Test Suites: 2 passed, 2 total Tests: 5 passed, 5 total Snapshots: 0 total Time: 6.878s
Aggiungere la Continuous Integration con TravisCI
Adesso è arrivato il momento di setuppare TravisCI per la Continuous Integration! Attiviamo la TravisCI Github integration and Codecov sul repository e aggiungiamo il file .travis.yml
file per configurare come TravisCI si comporterà
language: node_js node_js: - 10 before_script: - yarn add codecov script: - yarn test:unit after_script: codecov
Ogni volta che pushiamo del codice sul nostro repository, TravisCI installerà il pacchetto codecov
per comunicare con il servizio Codecov (before_script
) e lancerà i test in automatico (script
), inviando i dati di coverage a Codecov (after_script
).
Aggiungere funzionalità backend
Chiamare Github API directly dal componente non è il miglior modo per fetchare tutti i repository. Come si evince dalla documentazione delle Github API c'è un endpoint molto più bello da utilizzare per prendere i repository personali con un rate limit più alto, https://api.github.com/user/repos
, ma necessita di un token di autenticazione per funzionare. Generare un nuovo token da Github è facilissimo, ma necessita di rimanere privato e non può essere esposto nel codice frontend, per questo necessitiamo di una parte backend per comunicare con Github. Fortunatamente grazie alle Netlify Function
puoi lanciare delle AWS's serverless Lambda function per far girare codice server-side senza avere un server dedicato o un account AWS, tutto gestito in modo trasparente da Netlify. Qua potete trovare la documentazione.
Usare le lambda function con Netlify è veramente facile: basta aggiungere una cartella lambda
nella root del progetto e un file getmyrepos.js
dove risiedono le funzioni, nel nostro caso:
const axios = require('axios'); exports.handler = function(event, context, callback) { let responseHeaders = { 'Content-Type': 'application/json' }; if (process.env.NETLIFY_DEV === 'true') { responseHeaders['Access-Control-Allow-Origin'] = '*'; } axios.get(`https://api.github.com/user/repos?visibility=public&page=${event.queryStringParameters.page}`, { headers : { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } }) .then((response) => { callback(null, { statusCode: 200, body: JSON.stringify(response.data), headers: responseHeaders }); }) .catch((error) => { callback(null, { statusCode: error.response.status, body: JSON.stringify({'message' : error.response.data.message}), headers: responseHeaders }); }); }
Esportiamo poi un metodo handler
method wper comunicare con l'endpoint delle Github API usando axios, aggiungendo il nostro token Github (presente nella variabile d'ambiente GITHUB_TOKEN
) negli header e quindi mandiamo la risposta attraverso la funzione callback
! Necessitiamo inoltre anche del parametro event.queryStringParameters
per prendere la page
. Per ulteriori informazioni rimando alla documentazione.
Per lanciare le funzioni lambda localmente, installa la Netlify CLI
sudo npm install netlify-cli -g
And add netlify.toml
file in the root of the project
[dev] command = "yarn serve" functions = "lambda"
Questo file contiene la configurazione dell'ambiente dev
: le funzioni lambda sono collocate nella cartellalambda
e il comando per eseguire l'app frontend è yarn serve
. Per eseguire l'intera app in modalità dev, aggiungi GITHUB_TOKEN
al file.env
e avvialo
netlify dev
La nostra app Vue ora gira su http://localhost: 8080
e la funzione lambda su http://localhost: 34567/getmyrepos
. È tempo di modificare il codice dell'app e i test per integrare la funzione lambda! Prima di tutto aggiungiamo l'intestazione Access-Control-Allow-Origin = *
alla risposta della funzione quando l'app è in esecuzione in modalità dev (la variabile di ambiente NETLIFY_DEV
è 'vera') perché l'app Vue e il servizio lambda sono esposti su porte diverse
// ... let responseHeaders = { 'Content-Type': 'application/json' }; if (process.env.NETLIFY_DEV === 'true') { responseHeaders['Access-Control-Allow-Origin'] = '*'; } // ...
Imposta una nuova variabile d'ambiente VUE_APP_BACKEND_ENDPOINT = http://localhost:34567
per definire il nostro endpoint back-end e modificare l'URL per recuperare i repository nel componenteApp.vue
e test
// ... axios.get(`${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=${this.page}`) .then((resp) => { this.repositories = this.repositories.concat(resp.data); this.page += 1; }) // ...
// ... (axios.get as jest.Mock).mockImplementation((url) => { switch (url) { case `${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=1`: return Promise.resolve({data : reposResponses.page1}); case `${process.env.VUE_APP_BACKEND_ENDPOINT}/getmyrepos?page=2`: return Promise.resolve({data : reposResponses.page2}); } }); // ...
Anche le funzioni Lambda sono facili da testare! Testiamo la nostra funzione aggiungendo la definizione lambda/getmyrepos.d.ts
per supportare TypeScript.
export declare function handler(event: any, context: any, callback: any): any;
import reposResponses from '../__fixtures__/reposResponses'; import axios from 'axios'; import { handler } from '@/../lambda/getmyrepos'; jest.mock('axios'); (axios.get as jest.Mock).mockImplementation((url) => { switch (url) { case `https://api.github.com/user/repos?visibility=public&page=1`: return Promise.resolve({data : reposResponses.page1}); case `https://api.github.com/user/repos?visibility=public&page=2`: let err: any = {} err.response = { status: 401, data: { message: 'Bad Credentials' } } return Promise.reject(err) } }); describe('Lambda function getmyrepos', () => { it('renders repositories on call page 1', (done) => { const event = { queryStringParameters : { page : 1, }, }; handler(event, {}, (e: any, obj: any) => { expect(obj.statusCode).toEqual(200); expect(obj.body).toEqual(JSON.stringify(reposResponses.page1)); done(); }); }); it('shows message error if any', (done) => { const event = { queryStringParameters : { page : 2, }, }; handler(event, {}, (e: any, obj: any) => { expect(obj.statusCode).toEqual(401); expect(obj.body).toEqual(JSON.stringify({message: 'Bad Credentials'})); done(); }); }); it('renders repositories with Access-Control-Allow-Origin * in dev mode', (done) => { process.env.NETLIFY_DEV = 'true'; const event = { queryStringParameters : { page : 1, }, }; handler(event, {}, (e: any, obj: any) => { expect(obj.headers['Access-Control-Allow-Origin']).toEqual('*'); done(); }); }); });
Ricoriamoci di aggiungere "lambda/*.js"
a collectCoverageFrom
nel file jest.config.js
.
Aggiungere il Continuous Deployment con Netlify
È tempo di deployare il nostro sito su Netlify! Dopo il login fare click su New site from Git
e aggiungere il repository.

È possibile configurare il ramo di produzione, creare comandi, funzioni direttamente su Netlify o aggiungerli a netlify.toml
. Il modo più semplice per configurare un progetto per CD è usare un ramo protetto chiamato "produzione" e configurare Netlify per iniziare a costruire e pubblicare solo quando viene inviato un nuovo commit su questo ramo. In Impostazioni di build avanzate
inImpostazioni -> Build & Deploy
devi impostare le variabili di ambiente, ad esempio le modifiche VUE_APP_BACKEND_ENDPOINT
nella produzione:/ .netlify / funzioni
.

Puoi usare inoltre il file netlify.toml
file per configurare le opzioni di build
[build] base = "/" publish = "dist" command = "yarn build" functions = "lambda" [dev] command = "yarn serve"
Per ogni pull request fatta, Netlify deploya una preview del sito, accessibile dai dettagli della PR.

Adoro usare questo flusso, posso controllare l'intero sito usando l'anteprima prima di mandare tutto in produzione, ma ci sono moltissimi modi alternativi per configurare il processo di Continuous Deployment! Non mancate per la seconda parte nella quale illustreremo tali metodi