Développement sans fron-Tiers
@JoGrenat
Architecture 3-tiers
Présentation
Applicatif
Données
Présentation
Applicatif
Données
Présentation
Applicatif
Données
Présentation
Applicatif
Données
JSON
Texte
Binaire
Statut HTTP
REST
GraphQL
Websocket
Requête SQL
Requête No-SQL
JSON
Data[]
Frontières
Entre-Deux
Contexte / Dynamisme
Who is
this?
- Sessions
- Requêtes multiples
- HATEOAS
{
"_links": {
"changePassword": {
"href": "/api/user/1/change-password",
"method": "POST",
"payload": {
"token": "80b3db96-06bb-4685-86a1-c115410210fd"
}
}
}
}
const token = getToken(user);
return {
payload: {token},
changePassword: (password: string) => doSomething(user, password)
};
Paradigme
C'est quoi
l'héritage ?
Et un
arbre ?
Performances et temps
- Sérialiser les données côté client
- Créer une requête HTTP côté client
- Créer un endpoint HTTP côté serveur
- Désérialiser les données côté serveur
- Créer une requête SQL
- Parser la requête SQL
- Désérialiser la réponse de la base
- Sérialiser la réponse pour le front
- Vérifier le status HTTP
- Désérialiser la réponse
🤯
Garanties
type RemoteString
= Loading
| Loaded String
| OnError Int String
anError : RemoteString
anError = OnError 400 "Bad Request"
aSuccess : RemoteString
aSuccess = Loading "Hello World!"
Type algébrique
type NonEmpty<a> = [a, ...a[]];
const invalid : NonEmpty<number> = [];
Liste non vide
- Validité des requêtes
- Contrainte d'unicité
- Références externes
- Longueur de texte
Sécurité
L'analyse du flux d'information est difficile
- Sécurité
- Garanties
- Contexte / Dynamisme
- Temps / Performance
- Paradigmes
Programmation multi-tiers
Présentation
Applicatif
Données
Programmation multi-tiers
- Hop.js
- Links
- Ur/Web
- Eliom/Ocsigen
- Gavial
- Opa
- Swift
- SIF
Rien n'est nouveau
- MeteorJs
- GWT
- ...
Le code back/front est co-localisé
export async function loader({ request }) {
return getProjects();
}
export async function action({ request }) {
const form = await request.formData();
return createProject({ title: form.get("title") });
}
export default function Projects() {
const projects = useLoaderData();
const { state } = useTransition();
const busy = state === "submitting";
return (
<div>
<Form method="post">
<input name="title" />
<button type="submit" disabled={busy}>
{busy ? "Creating..." : "Create New Project"}
</button>
</Form>
</div>
);
Serveur
Client
router.get('/projects', async (req, res) => {
const projects = await projectsRepository.getAll();
res.json({ projects });
});
router.post('/projects', async (req, res) => {
const title = request.body.title;
const project = await createProject({ title });
res.json({ project });
});
export default function Projects() {
const [projects, setProjects] = useState([]);
const [state, setState] = useState('idle');
const [title, setTitle] = useState('');
useEffect(() => {
setState('busy');
fetch('/projects').then(({projects}) => {
setState('loaded');
setProjects(projects)
}).catch(() => setState('error'));
});
const createProject = () => {
fetch('/project', { method: 'POST', body: { title } });
}
return (
<div>
<!-- ... -->
</div>
);
Code lié à la communication
Code lié à la communication
- Split d'une fonctionnalité
- Moins de bruit dans le code
- Moins d'erreurs possibles
- Charge cognitive réduite
- Maintenance facilitée
export async function loader({ request }) {
return getProjects();
}
export async function action({ request }) {
const form = await request.formData();
return createProject({ title: form.get("title") });
}
export default function Projects() {
const projects = useLoaderData();
const { state } = useTransition();
const busy = state === "submitting";
return (
<div>
<Form method="post">
<input name="title" />
<button type="submit" disabled={busy}>
{busy ? "Creating..." : "Create New Project"}
</button>
</Form>
</div>
);
Serveur
Client
Conventions
Sérialisation
Photo by Efe Kurnaz
Désolé...
fun completions(search) server {
// Search words
}
fun suggest(search) client {
domReplaceChildren(
format(completions(search)),
getNodeById("suggestions")
)
}
fun main() {
var myhandler = spawnClient {
fun receiver() {
receive { case Suggest(search) -> suggest(search); receiver() }
}
receiver()
};
page
<body>
<h1>Dictionary suggest</h1>
<form l:onkeyup="{myhandler!Suggest(pre)}">
<input type="text" l:name="search"
autocomplete="off"/>
</form>
<div id="suggestions"/>
</body>
}
main()
module Echo_app = Eliom_registration.App (struct let application_name = "echo" let global_data_path = None end)
let%server main_service = create ~path:(Path []) ~meth:(Get Eliom_parameter.unit) ()
let%server make_input up =
let inp = Html.D.Raw.input () in
let btn = Html.D.button ~a:[Html.D.a_class ["button"]] [Html.D.pcdata "Echo!"] in
ignore [%client
(Lwt.async (fun () ->
Lwt_js_events.clicks (Html.To_dom.of_element ~%btn) (fun _ _ ->
~%up (Js.to_string (Html.To_dom.of_input ~%inp)##.value);
Lwt.return_unit)) : unit) ];
Html.D.div [inp; btn]
let%server () = Echo_app.register
~service:main_service
(fun () () ->
let item_up = Up.create (Eliom_parameter.ocaml "item" [%derive.json :string]) in
let item_down = Down.of_react (Up.to_react item_up) in
let list, handle = ReactiveData.RList.create [] in
let list = ReactiveData.RList.map [%shared fun i -> Html.D.li [Html.D.pcdata i] ] list in
let input = make_input item_up in
ignore [%client
(Eliom_client.onload
(fun _ -> ignore (React.E.map (fun i -> ReactiveData.RList.cons i ~%handle) ~%item_down)) : unit) ];
Lwt.return (Eliom_tools.D.html ~title:"echo" (Html.D.body [input; Html.R.ul list])))
Code tiré de A Survey of Multitier Programming
PASCAL WEISENBURGER, JOHANNES WIRTH, and GUIDO SALVANESCHI,
Technische Universität Darmstadt, Germany
Séparation client / serveur
Communication
implicite vs explicite
implicite vs explicite
Granularité du split
expression, fonction, fichier
var db = database "todo";
var items = table "items" with (name : String) from db;
mutual {
fun showList() server {
page
<html>
<body>
<form l:action="{add(item)}" method="POST">
<input l:name="item"/>
<button type="submit">Add item</button>
</form>
<table>
{for (item <- query {for (item <-- items) [item]})
# HTML
}
</table>
</body>
</html>
}
fun add(name) server {
insert items values [(name=name)];
showList()
}
fun remove(name) server {
delete (r <-- items) where (r.name == name);
showList()
}
}
showList()
Séparation client / serveur
Communication
implicite vs explicite
implicite vs explicite
Couverture
client, serveur, base de données
Granularité du split
expression, fonction, fichier
Alors pourquoi ?
Gain de temps
Charge cognitive réduite
Meilleure maintenabilité
Nos types voyagent
- Les requêtes à la DB
- Les "appels serveur"
- Le HTML
- Nos types métiers
- Problème d'impédance
Partage de code
- Server-side rendering
- Gestion intégrée des états
- Progressive enhancement
- Communication bi-directionnelle
- Contexte stocké côté client
- Concurrence côté front
Programmation multi-tiers
JoinedChatroom room ->
(model, sendToBackend (UserJoined { name = "Bob", room = room }))
UserJoined user ->
(model, sendToFrontend clientId (UserGreeting ("Hello " ++ user.name)))
- Couverture 3-tier + infrastructure
- CI / CD automatiquement gérée
- Elimination du code mort
- Excellent tooling
- Evergreen migrations
- Gros vendor-locking
- Grosses charges (DB > 1Go ou + 1000 req/s)
- https://dashboard.lamdera.app/shouldnt-use
Java + Information Flow
Langage security-typed
int {Alice→Bob} x;
- Assure la confidentialité de la DB au front en passant par le file system
- Les données utilisateurs doivent être vérifiées
- Formulaire générées de façon unique
Théorique
🪐
🪐
Pratique
🚀
🚀
🚀
🚀
Remote Mob Programming
Pascal Weisenburger, Johannes Wirth, et Guido Salvaneschi, Technische Universität Darmstadt, Germany
Slides : bit.ly/multitiers-fr
@JoGrenat
Tous les papiers des langages
Merci !
Slides : bit.ly/multitiers-fr
@JoGrenat
Développement sans Front-Tiers
By ereold
Développement sans Front-Tiers
- 1,092