refactor: aggiunta documentazione sull'architettura e varie migliorie qua e là

next
Antonio De Lucreziis 2 years ago
parent 7100009ab1
commit 396729adf7

@ -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)

@ -1,5 +1,15 @@
# Website 2 # Website 2
Repo per il nuovo sito del PHC
## Docs
- [./ARCHITECTURE.md](./ARCHITECTURE.md)
Alcune note sull'architettura di questo progetto.
## Usage
## Development ## Development
```bash shell ```bash shell

@ -13,6 +13,10 @@ import (
"git.phc.dm.unipi.it/phc/website/sl" "git.phc.dm.unipi.it/phc/website/sl"
) )
func init() {
log.SetFlags(0)
}
func main() { func main() {
l := sl.New() l := sl.New()
@ -55,7 +59,7 @@ func main() {
go func() { go func() {
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
for scanner.Scan() { for scanner.Scan() {
log.Printf(`[ViteJS] %s`, scanner.Text()) log.Printf(`[cmd/devserver] [vitejs] %s`, scanner.Text())
} }
}() }()

@ -14,10 +14,18 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url))
async function main() { async function main() {
const routes = await getDevRoutesMetadata('http://127.0.0.1:4000/api/development/routes') 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]')) app.use(morgan(':method :url :status :response-time ms - :res[content-length]'))
const vite = await createViteServer({ const vite = await createViteServer({
@ -27,28 +35,36 @@ async function main() {
app.use(vite.middlewares) app.use(vite.middlewares)
Object.entries(routes.static).forEach(([route, file]) => { for (const [route, file] of Object.entries(routes.static)) {
app.get(route, async (_req, res) => { app.get(route, async (req, res) => {
console.log(`Requested static route "${route}":`)
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8') let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
// Replace "./" with the absolute path of the html page
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/') htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
console.log(`- applying vite transformations for "${file}"`)
const html = await vite.transformIndexHtml(file, htmlPage) 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) 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) => { app.get(route, async (req, res) => {
console.log(`Requested dynamic route "${route}":`)
let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8') let htmlPage = await readFile(resolve(__dirname, './frontend/', file), 'utf8')
// Replace "./" with the absolute path of the html page
htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/') htmlPage = htmlPage.replace(/\.\//g, '/' + dirname(file) + '/')
console.log(`- applying vite transformations for "${file}"`)
const html = await vite.transformIndexHtml(file, htmlPage) const html = await vite.transformIndexHtml(file, htmlPage)
console.log('req.url = ', req.url) console.log(`- applying server transformations for "${file}"`)
console.log('req.originalUrl = ', req.originalUrl)
const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', { const templateHtmlReq = await fetch('http://127.0.0.1:4000/api/development/render', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -64,9 +80,10 @@ async function main() {
const renderedHtml = await templateHtmlReq.json() const renderedHtml = await templateHtmlReq.json()
console.log(`- sending resulting page for "${route}"`)
res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml) res.writeHead(200, { 'Content-Type': 'text/html' }).end(renderedHtml)
}) })
}) }
app.listen(3000, () => { app.listen(3000, () => {
console.log(`Listening on port 3000...`) console.log(`Listening on port 3000...`)

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"path" "path"
"git.phc.dm.unipi.it/phc/website/services/config" "git.phc.dm.unipi.it/phc/website/services/config"
@ -15,6 +16,9 @@ import (
"github.com/gofiber/fiber/v2" "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 // slot represents a private "write only" service
var slot = sl.NewSlot[*devService]() var slot = sl.NewSlot[*devService]()
@ -26,7 +30,7 @@ func InjectInto(l *sl.ServiceLocator) {
func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any { func UseRoutesMetadata(l *sl.ServiceLocator) map[string]any {
dev, err := sl.Use(l, slot) dev, err := sl.Use(l, slot)
if err != nil { if err != nil {
log.Fatal(err) Logger.Fatal(err)
} }
return map[string]any{ 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 { r.Post("/api/development/render", func(c *fiber.Ctx) error {
var data struct { var data struct {
Route string `json:"route"` Route string `json:"route"`
Page string `json:"page"` HtmlPage string `json:"page"`
Request struct { Request struct {
ParamsMap map[string]string `json:"params"` ParamsMap map[string]string `json:"params"`
QueryMap map[string]string `json:"query"` QueryMap map[string]string `json:"query"`
} `json:"request"` } `json:"request"`
@ -112,7 +116,9 @@ func Configure(l *sl.ServiceLocator) (*devService, error) {
return err 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] handler, ok := d.dynamicRoutesHandlers[data.Route]
if !ok { if !ok {
@ -121,7 +127,7 @@ func Configure(l *sl.ServiceLocator) (*devService, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := handler(&buf, devServerRequest{ if err := handler(&buf, devServerRequest{
[]byte(data.Page), []byte(data.HtmlPage),
data.Request.ParamsMap, data.Request.ParamsMap,
data.Request.QueryMap, data.Request.QueryMap,
}); err != nil { }); err != nil {
@ -136,27 +142,26 @@ func Configure(l *sl.ServiceLocator) (*devService, error) {
// RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page // RegisterRoute will register the provided "mountPoint" to the "frontendHtml" page
func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) { func RegisterRoute(l *sl.ServiceLocator, mountPoint, frontendFile string) {
log.Printf(`registering vite route %q for %q`, frontendFile, mountPoint)
dev, err := sl.Use(l, slot) dev, err := sl.Use(l, slot)
if err != nil { if err != nil {
log.Fatal(err) Logger.Fatal(err)
} }
dev.staticRoutes[mountPoint] = frontendFile 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) { 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) dev, err := sl.Use(l, slot)
if err != nil { if err != nil {
log.Fatal(err) Logger.Fatal(err)
} }
dev.dynamicRoutes[mountPoint] = frontendFile dev.dynamicRoutes[mountPoint] = frontendFile
dev.dynamicRoutesHandlers[mountPoint] = handler dev.dynamicRoutesHandlers[mountPoint] = handler
Logger.Printf(`registered vite route "%v" for "%v"`, frontendFile, mountPoint)
} }
func GetArtifactPath(frontendFile string) string { func GetArtifactPath(frontendFile string) string {

@ -1,4 +1,4 @@
package listaUtenti package listautenti
import ( import (
"git.phc.dm.unipi.it/phc/website/services/database" "git.phc.dm.unipi.it/phc/website/services/database"

@ -1,4 +1,4 @@
package listaUtenti_test package listautenti_test
import ( import (
"context" "context"

@ -3,7 +3,7 @@ package server
import ( import (
"git.phc.dm.unipi.it/phc/website/services/server/articles" "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/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/services/server/routes"
"git.phc.dm.unipi.it/phc/website/sl" "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)) sl.InjectValue(l, routes.Root, fiber.Router(r))
dev.InjectInto(l) dev.InjectInto(l)
if err := listaUtenti.Configure(l); err != nil { if err := listautenti.Configure(l); err != nil {
return nil, err return nil, err
} }
if err := articles.Configure(l); err != nil { if err := articles.Configure(l); err != nil {

@ -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 [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. // 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 package sl
import ( import (
@ -12,7 +18,7 @@ import (
// Logger is the debug logger, in the future this will be disabled and discard by default. // 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) // 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". // slot is just a "typed" unique "symbol".
type slot[T any] *struct{} type slot[T any] *struct{}

@ -1,46 +1,32 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { dirname, join, resolve } from 'path' import { join } from 'path'
import preactPlugin from '@preact/preset-vite' import preactPlugin from '@preact/preset-vite'
import { getBuildRoutesMetadata } from './meta/routes.js' import { getBuildRoutesMetadata } from './meta/routes.js'
import crypto from 'crypto'
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const sharedConfig = { const sharedConfig = {
// all files processed by ViteJS are inside this folder
root: './frontend', root: './frontend',
// for now the only plugin is Preact
plugins: [preactPlugin()], 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 => { export default defineConfig(async config => {
if (config.command === 'build') { if (config.command === 'build') {
const routes = await getBuildRoutesMetadata('out/routes.json') 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( const allHtmlEntrypoints = [...Object.values(routes.static), ...Object.values(routes.dynamic)].map(file =>
routesToRollupInput join(__dirname, 'frontend', file)
)
) )
console.dir(input) console.dir(allHtmlEntrypoints)
return { return {
...sharedConfig, ...sharedConfig,
build: { build: {
outDir: '../out/frontend', outDir: '../out/frontend',
rollupOptions: { rollupOptions: {
input, input: allHtmlEntrypoints,
}, },
}, },
} }

Loading…
Cancel
Save