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
|
# 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)
|
|
||||||
|
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()
|
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...`)
|
||||||
|
|||||||
+18
-13
@@ -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
-1
@@ -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
-1
@@ -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{}
|
||||||
|
|||||||
+8
-22
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user