forked from phc/website
refactor: aggiunta documentazione sull'architettura e varie migliorie qua e là
This commit is contained in:
@@ -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
|
||||
|
||||
Repo per il nuovo sito del PHC
|
||||
|
||||
## Docs
|
||||
|
||||
- [./ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
|
||||
Alcune note sull'architettura di questo progetto.
|
||||
|
||||
## Usage
|
||||
|
||||
## Development
|
||||
|
||||
```bash shell
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
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...`)
|
||||
|
||||
+18
-13
@@ -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 {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package listaUtenti
|
||||
package listautenti
|
||||
|
||||
import (
|
||||
"git.phc.dm.unipi.it/phc/website/services/database"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package listaUtenti_test
|
||||
package listautenti_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
+8
-22
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user