From 396729adf715796c199f28ad62eceb31dabf2c2a Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Fri, 3 Mar 2023 20:17:44 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20aggiunta=20documentazione=20sull'ar?= =?UTF-8?q?chitettura=20e=20varie=20migliorie=20qua=20e=20l=C3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 23 +++++++++++ README.md | 10 +++++ cmd/devserver/main.go | 6 ++- server.js | 39 +++++++++++++------ services/server/dev/dev.go | 31 ++++++++------- .../listautenti.go} | 2 +- .../listautenti_test.go} | 2 +- services/server/server.go | 4 +- sl/sl.go | 8 +++- vite.config.js | 30 ++++---------- 10 files changed, 103 insertions(+), 52 deletions(-) create mode 100644 ARCHITECTURE.md rename services/server/{listaUtenti/lista-utenti.go => listautenti/listautenti.go} (97%) rename services/server/{listaUtenti/lista-utenti_test.go => listautenti/listautenti_test.go} (98%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..0ff309c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,23 @@ +# Architettura + +Questo progetto utilizza [Go](https://go.dev/) per la backend e l'ecosistema di NodeJS per la frontend. Più precisamente utilizziamo [ViteJS](https://vitejs.dev/). + +La cosa più interessante è l'integrazione tra ViteJS e la backend in Go anche per siti con più pagine (di default ViteJS non rende ciò molto semplice). + +Ci sono vari _entry-point_ per la nostra applicazione (per comodità sono sempre moduli go che eventualmente lanciano altri processi se necessario): + +- Quando saremo in produzione l'unico server sarà quello di Go lanciato attraverso l'_entry-point_ [./cmd/server/main.go](./cmd/server/main.go) + + In particolare prima di poter lanciamo questo server bisogna aver eseguito [./cmd/build/main.go](./cmd/build/main.go) che esegue solo il codice relativo al router della nostra applicazione e genera un file `out/routes.json`. Poi bisogna eseguire `npm run build` che chiama ViteJS e genera tutte le route dentro la cartella `out/frontend/` + +- Quando siamo in development usiamo solo [./cmd/devserver/main.go](./cmd/devserver/main.go) che lancia in background il server di ViteJS (chiama `npm run dev` che a sua volta è un alias per `node server.js`) quindi possiamo vedere tutto in tempo reale da `localhost:3000`. + + Più precisamente il server di ViteJs all'avvio richiede al server in Go tutte le route da montare utilizzando la route speciale `/api/development/routes` (in particolare Fiber ed ExpressJS hanno la stessa sintassi per definire le route quindi questa cosa è facile da fare). + + Poi quando uno sviluppatore prova ad andare su una pagina ci sono due casi, se la route era statica allora leggiamo il file html, lo facciamo processare a ViteJS e poi lo rimandiamo all'utente. Se invece la route era di tipo dinamico allora leggiamo sempre il file e lo processiamo con ViteJS però ora utilizziamo l'altra route speciale che esiste solo in fase di sviluppo `/api/development/render` che renderizza la pagina applicando il templating del server e poi una volta finito inviamo la pagina al client. + + Quando saremo in produzione tutte le pagina saranno già state renderizzate da ViteJS quindi saremo nel caso standard di _http server_ con views da renderizzare con il _template engine_ del caso prima di mandare la pagina al client. + +- L'ultimo entry-point è [./cmd/build/main.go](./cmd/build/main.go) che lancia la nostra applicazione in una modalità "finta" in cui il server http non viene avviato ma vengono registrate tutte le route utilizzando sempre il modulo `dev`. Questo ci permette di costruire l'albero delle route (statiche e dinamiche) che poi servirà a ViteJS quando facciamo `npm run build`. + + Ciò serve perché così ci basta definire tutte le route una volta sola nel Go e poi funzioneranno in automatico anche nel server di ViteJS senza dover ripetere due volte il codice. (questa è la parte più di _meta-programming_ di tutto il progetto) \ No newline at end of file diff --git a/README.md b/README.md index bb2f460..d4dff3d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # Website 2 +Repo per il nuovo sito del PHC + +## Docs + +- [./ARCHITECTURE.md](./ARCHITECTURE.md) + + Alcune note sull'architettura di questo progetto. + +## Usage + ## Development ```bash shell diff --git a/cmd/devserver/main.go b/cmd/devserver/main.go index 96ba918..d3a96cd 100644 --- a/cmd/devserver/main.go +++ b/cmd/devserver/main.go @@ -13,6 +13,10 @@ import ( "git.phc.dm.unipi.it/phc/website/sl" ) +func init() { + log.SetFlags(0) +} + func main() { l := sl.New() @@ -55,7 +59,7 @@ func main() { go func() { scanner := bufio.NewScanner(r) for scanner.Scan() { - log.Printf(`[ViteJS] %s`, scanner.Text()) + log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text()) } }() diff --git a/server.js b/server.js index 7cb2b93..7337e10 100644 --- a/server.js +++ b/server.js @@ -14,10 +14,18 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url)) async function main() { const routes = await getDevRoutesMetadata('http://127.0.0.1:4000/api/development/routes') - console.dir(routes) - const app = express() + console.log('Found static routes:') + for (const [route, file] of Object.entries(routes.static)) { + console.log(`- ${route} -> "${file}"`) + } + + console.log('Found dynamic routes:') + for (const [route, file] of Object.entries(routes.dynamic)) { + console.log(`- ${route} -> "${file}"`) + } + const app = express() app.use(morgan(':method :url :status :response-time ms - :res[content-length]')) const vite = await createViteServer({ @@ -27,28 +35,36 @@ async function main() { app.use(vite.middlewares) - Object.entries(routes.static).forEach(([route, file]) => { - app.get(route, async (_req, res) => { + for (const [route, file] of Object.entries(routes.static)) { + app.get(route, async (req, res) => { + console.log(`Requested static route "${route}":`) + let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8') + + // Replace "./" with the absolute path of the html page htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/') + console.log(`- applying vite transformations for "${file}"`) const html = await vite.transformIndexHtml(file, htmlPage) - console.dir(file) + console.log(`- sending resulting page for "${route}"`) res.writeHead(200, { 'Content-Type': 'text/html' }).end(html) }) - }) + } - Object.entries(routes.dynamic).forEach(([route, file]) => { + for (const [route, file] of Object.entries(routes.dynamic)) { app.get(route, async (req, res) => { + console.log(`Requested dynamic route "${route}":`) + let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8') + + // Replace "./" with the absolute path of the html page htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/') + console.log(`- applying vite transformations for "${file}"`) const html = await vite.transformIndexHtml(file, htmlPage) - console.log('req.url = ', req.url) - console.log('req.originalUrl = ', req.originalUrl) - + console.log(`- applying server transformations for "${file}"`) const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -64,9 +80,10 @@ async function main() { const renderedHtml = await templateHtmlReq.json() + console.log(`- sending resulting page for "${route}"`) res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml) }) - }) + } app.listen(3000, () => { console.log(`Listening on port 3000...`) diff --git a/services/server/dev/dev.go b/services/server/dev/dev.go index d478960..22af4b2 100644 --- a/services/server/dev/dev.go +++ b/services/server/dev/dev.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "os" "path" "git.phc.dm.unipi.it/phc/website/services/config" @@ -15,6 +16,9 @@ import ( "github.com/gofiber/fiber/v2" ) +// Logger is the debug logger, in the future this will be disabled and discard by default. +var Logger *log.Logger = log.New(os.Stderr, "[services/server/dev] ", log.Lmsgprefix) + // slot represents a private "write only" service var slot = sl.NewSlot[*devService]() @@ -26,7 +30,7 @@ func InjectInto(l *sl.ServiceLocator) { func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any { dev, err := sl.Use(l, slot) if err != nil { - log.Fatal(err) + Logger.Fatal(err) } return map[string]any{ @@ -100,9 +104,9 @@ func Configure(l *sl.ServiceLocator) (*devService, error) { r.Post("/api/development/render", func(c *fiber.Ctx) error { var data struct { - Route string `json:"route"` - Page string `json:"page"` - Request struct { + Route string `json:"route"` + HtmlPage string `json:"page"` + Request struct { ParamsMap map[string]string `json:"params"` QueryMap map[string]string `json:"query"` } `json:"request"` @@ -112,7 +116,9 @@ func Configure(l *sl.ServiceLocator) (*devService, error) { return err } - repr.Print(data) + Logger.Printf(`server rendering route "%s"`, data.Route) + Logger.Printf(`- params: %s`, repr.String(data.Request.ParamsMap)) + Logger.Printf(`- query: %s`, repr.String(data.Request.QueryMap)) handler, ok := d.dynamicRoutesHandlers[data.Route] if !ok { @@ -121,7 +127,7 @@ func Configure(l *sl.ServiceLocator) (*devService, error) { var buf bytes.Buffer if err := handler(&buf, devServerRequest{ - []byte(data.Page), + []byte(data.HtmlPage), data.Request.ParamsMap, data.Request.QueryMap, }); err != nil { @@ -136,27 +142,26 @@ func Configure(l *sl.ServiceLocator) (*devService, error) { // RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) { - log.Printf(`registering vite route %q for %q`, frontendFile, mountPoint) - dev, err := sl.Use(l, slot) if err != nil { - log.Fatal(err) + Logger.Fatal(err) } dev.staticRoutes[mountPoint] = frontendFile - log.Print(dev) + + Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint) } func RegisterDynamicRoute(l *sl.ServiceLocator, mountPoint, frontendFile string, handler Handler) { - log.Printf(`registering vite route %q for %q`, frontendFile, mountPoint) - dev, err := sl.Use(l, slot) if err != nil { - log.Fatal(err) + Logger.Fatal(err) } dev.dynamicRoutes[mountPoint] = frontendFile dev.dynamicRoutesHandlers[mountPoint] = handler + + Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint) } func GetArtifactPath(frontendFile string) string { diff --git a/services/server/listaUtenti/lista-utenti.go b/services/server/listautenti/listautenti.go similarity index 97% rename from services/server/listaUtenti/lista-utenti.go rename to services/server/listautenti/listautenti.go index 964441c..e6bbd64 100644 --- a/services/server/listaUtenti/lista-utenti.go +++ b/services/server/listautenti/listautenti.go @@ -1,4 +1,4 @@ -package listaUtenti +package listautenti import ( "git.phc.dm.unipi.it/phc/website/services/database" diff --git a/services/server/listaUtenti/lista-utenti_test.go b/services/server/listautenti/listautenti_test.go similarity index 98% rename from services/server/listaUtenti/lista-utenti_test.go rename to services/server/listautenti/listautenti_test.go index 87f48a0..dd5c09d 100644 --- a/services/server/listaUtenti/lista-utenti_test.go +++ b/services/server/listautenti/listautenti_test.go @@ -1,4 +1,4 @@ -package listaUtenti_test +package listautenti_test import ( "context" diff --git a/services/server/server.go b/services/server/server.go index bf72474..a9dd910 100644 --- a/services/server/server.go +++ b/services/server/server.go @@ -3,7 +3,7 @@ package server import ( "git.phc.dm.unipi.it/phc/website/services/server/articles" "git.phc.dm.unipi.it/phc/website/services/server/dev" - "git.phc.dm.unipi.it/phc/website/services/server/listaUtenti" + "git.phc.dm.unipi.it/phc/website/services/server/listautenti" "git.phc.dm.unipi.it/phc/website/services/server/routes" "git.phc.dm.unipi.it/phc/website/sl" @@ -19,7 +19,7 @@ func Configure(l *sl.ServiceLocator) (*Server, error) { sl.InjectValue(l, routes.Root, fiber.Router(r)) dev.InjectInto(l) - if err := listaUtenti.Configure(l); err != nil { + if err := listautenti.Configure(l); err != nil { return nil, err } if err := articles.Configure(l); err != nil { diff --git a/sl/sl.go b/sl/sl.go index 38b0838..9e95265 100644 --- a/sl/sl.go +++ b/sl/sl.go @@ -1,6 +1,12 @@ // The [sl] package has two main concepts, the [ServiceLocator] itself is the main object that one should pass around through the application. A [ServiceLocator] has a list of slots that can be filled with [InjectLazy] and [InjectValue] and retrieved with [Use]. As slots should be unique they can only be created with the [NewSlot] function. // // The usual way to use this module is to make slots for go interfaces and then pass implementations using the [InjectValue] and [InjectLazy] functions. +// +// Services can be of various types: +// - a service with no dependencies can be directly injected inside a ServiceLocator using [InjectValue]. +// - a service with dependencies on other service should use [InjectLazy]. This lets the service to initialize itself when required and makes the developer not think the topological sort to put onto the DAG of service dependencies. +// - a service can also be private, in this case the slot for a service should be a private field in the service package. This kind of services should also provide a way to inject them into a ServiceLocator. +// - a package also just provide a slot. This is useful for using the ServiceLocator to easily pass around values, effectively threating slots just as dynamically scoped variables. package sl import ( @@ -12,7 +18,7 @@ import ( // Logger is the debug logger, in the future this will be disabled and discard by default. // // As this is the service locator module it was meaning less to pass this through the ServiceLocator itself (without making the whole module more complex) -var Logger *log.Logger = log.New(os.Stderr, "[sl]", log.LstdFlags) +var Logger *log.Logger = log.New(os.Stderr, "[sl] ", log.Lmsgprefix) // slot is just a "typed" unique "symbol". type slot[T any] *struct{} diff --git a/vite.config.js b/vite.config.js index 58e66d8..c2fb97c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,46 +1,32 @@ import { defineConfig } from 'vite' -import { dirname, join, resolve } from 'path' +import { join } from 'path' import preactPlugin from '@preact/preset-vite' import { getBuildRoutesMetadata } from './meta/routes.js' -import crypto from 'crypto' - /** @type {import('vite').UserConfig} */ const sharedConfig = { + // all files processed by ViteJS are inside this folder root: './frontend', + // for now the only plugin is Preact plugins: [preactPlugin()], } -function routesToRollupInput([route, file]) { - const chunkName = - file - .replaceAll('.html', '') - .replaceAll('index', '') - .replace(/^\/|\/$/, '') - .replaceAll('/', '-') + - '-' + - crypto.createHash('md5').update(route).update(file).digest('hex').slice(0, 8) - - return [chunkName, join(__dirname, 'frontend', file)] -} - export default defineConfig(async config => { if (config.command === 'build') { const routes = await getBuildRoutesMetadata('out/routes.json') - const input = Object.fromEntries( - [...Object.entries(routes.static), ...Object.entries(routes.dynamic).map(([route, { htmlFile }]) => [route, htmlFile])].map( - routesToRollupInput - ) + + const allHtmlEntrypoints = [...Object.values(routes.static), ...Object.values(routes.dynamic)].map(file => + join(__dirname, 'frontend', file) ) - console.dir(input) + console.dir(allHtmlEntrypoints) return { ...sharedConfig, build: { outDir: '../out/frontend', rollupOptions: { - input, + input: allHtmlEntrypoints, }, }, }