Fullstack web development con Go & ViteJS

Chi sono?

Antonio De Lucreziis, studente di Matematica e macchinista del PHC

Il PHC è un gruppo di studenti di Matematica con interessi per, open source, Linux, self-hosting e soprattutto smanettare sia con hardware e software (veniteci pure a trovare!)

Cosa stiamo per creare?

Applicazione Contatore "Full Stack"

https://example.xyz/

Contatore: 73

Incrementa

Decrementa

Applicazione "Contatore"

  • Si va sul sito e poi si può premere uno di due pulsanti
  • Un tasto incrementa il valore mentre l'altro lo decrementa
  • Vedremo come anche un'applicazione semplice di questo tipo in realtà ci permette di trattare di molti concetti tra cui frontend, backend ed API (e se ce la facciamo anche Database)

Cos'è più precisamente una richiesta?

HTML

CSS

JS

Go

Frontend

Backend

Client

Server

Go e ViteJS

Go

ViteJS

Go, linguaggio di programmazione compilato, statically typed e con una libreria standard con tutte le cose essenziali.

Un tool per NodeJS per creare velocemente web app in HTML, JS, CSS utilizzando tutto l'ecosistema di NodeJS e NPM senza configurare quasi nulla

Perché Go e ViteJS?

Front-End

Back-End

  • Javascript

  • Typescript

  • WASM*
  • Webpack
  • ...
  • Python

  • NodeJS

  • Golang

  • Rust
  • ...

HTTP 101

Cosa succede quando proviamo ad andare su un sito?

Utente

Server

Connessione Client-Server

Utente

Server

http://example.xyz

Connessione Client-Server

\underbrace{\vphantom{htp/}\texttt{http}}_{\mathclap{\text{Protocollo}}} \texttt{://} \underbrace{\vphantom{htp/}\texttt{example.org}}_{\text{Dominio}} \, \texttt{:} \underbrace{\vphantom{htp/}\texttt{1234}}_{\mathclap{\text{Port}}} \, \underbrace{\vphantom{htp/}\texttt{/foo/bar}}_{\text{Path}} \, \underbrace{\vphantom{htp/}\texttt{?id=123}}_{\text{Query}}

URL?

Utente

Server

http://example.xyz

Connessione Client-Server

Utente

Server

http://192.0.2.42:80

Il DNS si occupa di risolvere il dominio e convertirlo in un IPv4 (o IPv6)

Connessione Client-Server

Ehm, e l'HTTPS?

Client

Server

Connessione TLS

Scambio di chiavi

Scambio di dati in modo sicuro (HTTP)

?

Server (Macchina)

:80

example.xyz (192.0.2.42)

Server (Macchina)

:80

Server (Programma)

example.xyz (192.0.2.42)

Server (Macchina)

:80

Server (Programma)

Altro programma (Client)

?

Server (Macchina)

:80

Server (Programma)

localhost:80
(127.0.0.1:80)

Altro programma (Client)

:80

:80

FIREWALL

Creazione del progetto

$ mkdir gdg-counter-website
$ cd gdg-counter-website

Creazione progetto

# inizializziamo il file "go.mod"
$ go mod init gdg-counter-website

# o se avessimo un repo git anche così...
$ go mod init github.com/aziis98/gdg-counter-website

Inizializzazione progetto in Go

# Crea il file "package.json" 
$ npm init

# Installa ViteJS come dipendenza di development
$ npm install -D vite

Inizializzazione progetto di NodeJS

O anche con un altro package manager alternativo a npm (ad esempio io userò pnpm)

ViteJS (1)

{
    "name": "frontend",
    "version": "1.0.0",
    "scripts": {
    	// avvia il server di ViteJS in modalità di development
        "dev": "vite",
        // crea la cartella "dist/" con tutti gli asset e bundle
        "build": "vite build"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    }
}

Configurazione di base

package.json
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GDG Talk Counter</title>
  </head>
  <body>
    <h1>GDG Counter Website</h1>
    <div class="app">
      <div id="counter-value">???</div>
      <button id="btn-decrement">Decrementa</button>
      <button id="btn-increment">Incrementa</button>
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Pagina HTML

index.html
const counterElement = document.querySelector('#counter-value')
const incrementButton = document.querySelector('#btn-increment')
const decrementButton = document.querySelector('#btn-decrement')

function updateCounter(value) {
    counterElement.textContent = `Counter: ${value}`
}

/* ... */

Un po' di JS

src/main.js

Client

Server

PATCH example.xyz/a/b/c

Client

Server

POST example.xyz/a/b/c

Client

Server

GET example.xyz/a/b/c

Client

Server

DELETE example.xyz/a/b/c

Client

Server

PUT example.xyz/a/b/c

Client

Server

PATCH example.xyz/a/b/c

Client

Server

POST example.xyz/a/b/c

Client

Server

GET example.xyz/a/b/c

Client

Server

DELETE example.xyz/a/b/c

Client

Server

PUT example.xyz/a/b/c

Client

Server

GET /api/value

valore aggiornato del contatore

Client

Server

POST /api/increment

valore aggiornato del contatore

Client

Server

POST /api/decrement

valore aggiornato del contatore

/* ... */

incrementButton.addEventListener('click', () => {
    fetch('/api/increment', { method: 'POST' })
        .then(res => res.json())
        .then(data => updateCounter(data))
})

decrementButton.addEventListener('click', () => {
    fetch('/api/decrement', { method: 'POST' })
        .then(res => res.json())
        .then(data => updateCounter(data))
})

fetch('/api/value')
    .then(res => res.json())
    .then(data => updateCounter(data))

Colleghiamo i bottoni con il server

src/main.js
/* ... */

incrementButton.addEventListener('click', async () => {
    const res = await fetch('/api/increment', { method: 'POST' })
    const data = await res.json()
    updateCounter(data)
})

decrementButton.addEventListener('click', async () => {
    const res = await fetch('/api/decrement', { method: 'POST' })
    const data = await res.json()
    updateCounter(data)
})

async function main() {
    const res = await fetch('/api/value')
    const data = await res.json()
    updateCounter(data)
}

main()

Stessa cosa ma con async-await

src/main.js
import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        port: 3000,
    },
})

Configurazione base per ViteJS

Demo di quanto fatto fino ad ora

Go (1)

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    setupRoutes(mux) // visto meglio in seguito
    
    server := http.Server{
        Addr:    ":4000",
        Handler: mux,
    }

    log.Printf("Starting server on port 4000...")
    log.Fatal(server.ListenAndServe())
}

Iniziamo a scrivere il server in Go

main.go
/* ... */

func setupRoutes(mux *http.ServeMux) {
    counter := 0

    mux.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode("ok")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    })

    mux.Handle("/", http.FileServer((http.Dir("./dist/"))))
}

Serviamo gli asset principali

main.go

ViteJS (2)

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        port: 3000,
        proxy: {
            '/api': 'http://localhost:4000/',
        },
    },
})

Configurazione per ViteJS

vite.config.js

Proxy di ViteJS per il server in Go

Development

ViteJS Dev Server

Go Server

:3000 /api/...

:3000 /...

:4000 /api/...

{...}

{...}

:3000

Proxy di ViteJS per il server in Go

Production

Go Server

:4000 /...

:4000 /api/...

dist/...

{...}

:4000

Demo (2)

Go (2)

/* ... */

func setupRoutes(mux *http.ServeMux) {
    counter := 0

    /* ... */

    mux.HandleFunc("/api/value", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode(counter)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    })
    
    /* ... */
}

Route di API

main.go
counter := 0

/* ... */

mux.HandleFunc("/api/increment", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    counter++

    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(counter)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
})

Route di API (Incrementa)

main.go
counter := 0

/* ... */

mux.HandleFunc("/api/decrement", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    counter--

    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(counter)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
})

Route di API (Decrementa)

main.go

Demo (3)

Conclusione

Come Pubblicarlo?

Ci sono alcuni siti che forniscono "container" con una quota gratuita di utilizzo al mese che sono molto comodi per fare prove all'inizio

Come Pubblicarlo?

  • Affittare un server (ai nostri tempi siamo scesi a circa ~5€/mese) dai provider classici come AWS (Amazon), Google Cloud, Azure...
  • Tenere un server in casa propria
  • Aggiungere un proxy per tenere più di un server:

Risorse

Possibili Sviluppi

  1. Aggiungere del CSS per migliorare l'aspetto della pagina
  2. Database: SQLite (Per non resettare i dati ogni volta che il server viene spento)
  3. Login e Utenti (Ogni utente ha un proprio counter su /u/USERNAME che gli altri possono cambiare)
  4. Framework JS per la Frontend: ad esempio Preact (Per ora abbiamo fatto tutto in js "vanilla")
  5. Dockerfile (per automatizzare il deployment)
  6. WebSocket/SSE per vedere il counter aggiornarsi in diretta
  7. SSR / Template (Inviare al client HTML già con i valori giusti, al momento inizialmente il counter è "???" e senza JS non si può neanche vedere il valore corrente del counter)

live code fino alla fine del tempo*

*in realtà ho già preparato quasi tutte le varianti

Go & ViteJS

By aziis98

Go & ViteJS

  • 264