Applications et Frameworks Web
Romuald THION
Semestre printemps 2023-2024 UCBL
Ici, le serveur d’écho TCP :
import net from "node:net";
function handleConnection(socket) {
socket.on("error", (error) => console.error(`error...`));
socket.on("end", () => console.debug(`closed...`));
socket.on("data", (chunk) => socket.write(`server ${chunk}`));
socket.write("Bonjour !\n");
}
const server = net.createServer();
server
.on("connection", handleConnection)
.on("listening", () => console.debug(`listening...`))
.listen(1337);
📄 node-tcp-echo.js du CM6.
Il faut implémenter (tout ou partie de) la spécification HTTP https://httpwg.org/specs/ :
💡 Des frameworks d’applications existent dans tous les écosystèmes Flask ou FastAPI en Python, Jakarta/Tomcat en Java, Dream en OCaml, Rust, etc. 💡
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
Host:
pour multiplexerIf-Modified-Since:
pour HTTP
304📑 Opérations encapsulées dans un objet Request
(celui
de Node.js ou propre). 📑
🍔 Un framework Web traite les requêtes HTTP pour produire les réponses HTTP associées. 🍔
// on étend (déclarativement) le serveur avec une route
server.route({
// verbe et chemin extraits de la requête
method: "GET",
path: "/",
// fonction de traitement qui prend la requête et calcule
// une proto réponse qui sera complétée puis envoyée
handler: (request, h) => {
const name = request.query.name ?? "World";
// dans ce framework on return simplement,
// dans d'autre on configure un objet réponse
return { message: `Hello ${name}!` };
},
});
Exemple avec https://hapi.dev/ (source).
Le framework construit l’objet Response
, plusieurs
opérations avant d’émettre sur le réseau :
Content-Length
et Etag
(empreinte)☣️ Erreur classique : essayer de modifier la réponse après envoi. ☣️
👉 L’application calcule le code de retour, une partie des en-têtes et le contenu (body), le framework se charge de transformer en réponse HTTP.
HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!DOCTYPE html>… (here come the 29769 bytes of the requested web page)
🖐️ Avec la gestion des requêtes et réponses HTTP, les frameworks proposent des services complémentaire pour assurer certaines fonctionnalités :
👉 Les frameworks minimalistes et unopiniated proposent peu de services, les frameworks batteries included en proposent un plus grand nombre déjà intégrés.
👉 Les frameworks s’appuient sur des principes de génie logiciel ou patterns assez caractéristiques.
👉 JavaScript, fonctionnel et objet, intègre des design patterns dans un style idiomatique. Voir Do you need Design Patterns in Functional Programming?.
🏁 Le programme principal (dit aussi point d’entrée,
main
en C/C++) laisse le contrôle du flot
d’exécution au framework.
💡 C’est le framework qui appelle nos handlers…
…pas le contraire (approche bibliothèque) ! 💡
// création de l'application par le framework
const server = Hapi.server({ port });
/* configuration en utilisant server.route() */
// on laisse la main au flot d'exécution principal
await server.start();
Point d’entrée d’une application Hapi (source).
Idem par exemple pour ASP.NET, voir Routing in ASP.NET Core.
📖 Don’t call us, we’ll call you back. 📖
Voir Inversion Of Control et Inversion of Control Containers and the Dependency Injection pattern de Martin FOWLER.
One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user’s application code.
The framework often plays the role of the main program in coordinating and sequencing application activity. This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application. Ralph Johnson and Brian Foote.
🧑⚖️ Le contrôle est inversé : c’est le framework qui contrôle l’appel des fonctions qu’on lui fournit.
❓ Comment définir et passer les traitements de l’application qu’on développe ?
Promise
💡 Quels que soient les langages et les architectures logicielles, il faut pouvoir organiser les traitements de l’application, notamment les séquencer :
💡 Mais aussi factoriser les traitements, e.g.,
authentification et autorisation sur admin/**
.
👉 Deux grandes stratégies de séquencement explicite et réutilisation :
💡 Principe du design pattern chain of responsabilities pour regrouper les traitements en unités indépendantes et réutilisables.
🧠 Problème : l’ordre des déclarations impacte trop fortement le séquencement, elles sont difficiles à ordonner. 🧠
Soit une séquence \mathsf{functs} = [f_1, \ldots, f_n] :
Request
, Response
,
Toolkit
ou Context
selon le
frameworkÉtant donnés un premier x: a et un dernier callback \mathsf{last} on veut chaîner les fonctions en calculant :
f_1(a, x_1 \mapsto f_2 (x_1, x_2 \mapsto \ldots x_{n-1} \mapsto f_n({n-1}, \mathsf{last})))
// addition en CPS avec "trace" des valeurs
const add = (k) => (x, next) => (console.log(x, next), next(k + x));
const composeCPS = (functs) => (x, last) =>
functs.reduceRight((next, f) => (x) => f(x, next), last)(x);
composeCPS([add(3), add(2), add(1)])(12, console.log);
12 [Function: next]
15 [Function: next]
17 [Function: log]
18
📄 Voir chain-of-functions.js. 📄
next()
à la Expressapp.get()
ajoute à la
séquence de handlers, next()
est le
callback CPS de chaque handler. L’application Express
va les chaîner (Writing
middlewares).👉 On déclare un filtre (basé sur la classe
jakarta.servlet.GenericFilter
) et où l’appliquer (Source).
👉 La méthode FilterChain.doFilter()
est l’équivalent
objet du callback next()
d’Express (source).
public class VisitorCounterFilter implements Filter {
private static Set<String> users = new HashSet<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) {
HttpSession session = ((HttpServletRequest) request).getSession(false);
Optional.ofNullable(session.getAttribute("username"))
.map(Object::toString)
.ifPresent(users::add);
request.setAttribute("counter", users.size());
chain.doFilter(request, response);
}
}
👉 Exemple avec Dream,
Tidy Web framework for OCaml and ReasonML en OCaml.
Séquencement explicite par composition de middleware
avec
(@@)
.
(* handler : request -> response promise *)
(* middleware : handler -> handler, un décorateur de handler *)
let count = ref 0
let count_requests inner_handler request =
count := !count + 1;
inner_handler request
let () =
Dream.run
@@ Dream.logger
@@ count_requests
@@ Dream.router [
Dream.get "/" (fun _ -> Dream.html ( (* ... *)));
]
💡 C’est une instance du design pattern
Observer
, comme les EventEmitter
, avec un
ordre de déclenchement fixé. 💡
User Handler
qui est le principal
handler utilisateur.Calcul de temps de traitement à l’échelle du serveur.
server.ext("onRequest", (request, h) => {
request.app.startTime = process.hrtime.bigint();
return h.continue;
});
server.ext("onPreResponse", (request, h) => {
request.app.endTime = process.hrtime.bigint();
request.response.header(
"X-Response-Time",
`${(request.app.endTime - request.app.startTime) / 1000n}`,
);
return h.continue;
});
❓ Quand générer la vue, le document HTML : statiquement côté serveur ou dynamiquement côté client en JavaScript ?
🏛️ Applications Web dites classiques
👍 Avantage pour le référencement (SEO – Search Engine Optimization, voir Google’s SEO starter guid), pour le chargement de la première page.
users/leaderboard
Route https://lifweb.univ-lyon1.fr/users/leaderboard
server.route({
method: "GET",
path: "/leaderboard",
handler: async (request, h) => {
const { all: allUsers } = request.query;
server.log("debug", `allUsers=${allUsers}`);
const results = await server.app.db.leaderBoard();
return h.view("leaderboard", { results });
},
options: {
description: "Classement utilisateurs",
validate: {
query: Joi.object({
all: Joi.boolean().optional().description("..."),
}),
},
},
});
💻 Applications dites AJAX, ou Web 2.0 ou SPA (Single Page Applications)
<body id="app"></body>
Voir, par exemple Client-side Vs. Server-side Rendering: What to choose when?
👍 Applications utilisables offline, limite les transferts HTTP, permet le développement de clients riches.
👍 Peuvent être servies par un autre serveur que celui qui fourni les données (voir autorisation CORS).
👎 La logique métier est fractionnée, voire dupliquée, entre client et serveur.
👉 Des frameworks MVC JS souvent utilisés pour gérer la complexité.
users
Route https://lifweb.univ-lyon1.fr/users
server.route({
method: "GET",
path: "/",
handler: async (request, h) => {
const { idUsr } = request.auth.credentials;
return {
...request.auth.credentials,
successes: await server.app.db.userSuccesses(idUsr),
attempts: await server.app.db.userAttempts(idUsr),
};
},
options: {
auth: "default",
response: {
schema: userSchema,
},
},
});
Site | GitHub | Stars | Down/week | Année |
---|---|---|---|---|
Express | GitHub | 62.5k | 31.4M | 2009 |
Koa | GitHub | 34.5k | 1.6M | 2013 |
Fastify | GitHub | 29.4k | 1.8M | 2016 |
Hapi | GitHub | 14.4k | 0.8M | 2011 |
Voir par exemple Choosing the right Node.js Framework: Express, Koa, or Hapi?
💯 On compare sur un serveur minimaliste /?name
qui
décore avec le temps de traitement.
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Bench : 2 ms (97.5%) et 0.83 ms (\bar{x}), 7,850 R/S (\bar{x}).
Voir server-express.js
Koa is developed by the same team behind Express.js. Referred to as a lighter version of Express
Bench : 0 ms (97.5%) et 0.01 ms (\bar{x}), 29,769 R/S (\bar{x}).
Voir server-koa.js
Fastify is a web framework highly focused on providing the best developer experience […]. It is inspired by Hapi and Express and […] is one of the fastest web frameworks in town.
Bench : 0 ms (97.5%) et 0.01 ms (\bar{x}), 32,181 R/S (\bar{x}).
Voir server-fastify.js
Build powerful, scalable applications, with minimal overhead and full out-of-the-box functionality - your code, your way Originally developed to handle Walmart’s Black Friday scale,[…].
Bench : 0 ms (97.5%) et 0.03 ms (\bar{x}), 22,091 R/S (\bar{x}).
Voir server-hapi.js
Le serveur https://lifweb.univ-lyon1.fr/ est avant tout une API Web
✅ CSR
Mais le serveur sert aussi du HTML !
✅ SSR
Le serveur sert aussi sa documentation
✅ CSR
TP5 mise en place d’un serveur.
🕥 Vérifié automatiquement le lundi 1er avril à 23:59, comme pour le CTF. 🕥
🏁 Dernier TP6 (trois séances) : réaliser un serveur d’API et un client (SSR/SPA) qui l’utilise pour application Web complète de réduction d’URL.