{.columns #title}
{.col style="flex: 0 0;"}
{.vspace} Vincent Balat {.vspace}
{.vspace}
{.vspace}
FUN-OCaml - Berlin
Sep 16-17 2024
{.vspace}
{.vspace}
{.comment .right} Use arrow keys to navigate
$ opam install ocsigen-start ocsipersist-sqlite-config
{.vspace}
{.vspace}
{.vspace}
{.vspace}
{.comment}
You can find these slides on ocsigen.org
and the source code of the sides with the solution of
exercices on Github.
{.comment} Refer to the main one page user manual to get more explanations.
{style="position: relative; font-weight: bold; font-size: larger; color: #0c4481;"}
{.center}
{style="position: absolute; top: 100px; left: 940px;"} Ocsigen Server
{#aaa style="position: absolute; top: 320px; left: 1000px;"} Eliom
{style="position: absolute; top: 520px; left: 940px;"} Js_of_ocaml
{style="position: absolute; top: 700px; left: 800px;"} Tyxml
{style="position: absolute; top: 700px; left: 370px;"} Lwt
{style="position: absolute; top: 150px; left: 20px;"} Ocsigen Start
{style="position: absolute; top: 350px; left: -30px;"} Ocsigen Toolkit
{style="position: absolute; top: 540px; left: 50px;"} Ocsigen i18n
{pause up-at-unpause=aaa}
{.vspace}
{.center style="font-size: smaller;"}
Jérôme Vouillon, Vincent Balat
Pierre Chambart, Grégoire Henry, Benedikt Becker,
Vasilis Papavasileiou, Gabriel Radanne, Hugo Heuzard,
Benjamin Canou, Boris Yakobowski, Jan Rochel, Idir Lankri,
Jérémie Dimino, Romain Calascibetta, Raphaël Proust, Anton Bachin, Baptiste Strazzulla,
Julien Sagot, Stéphane Glondu, Gabriel Kerneis, Denis Berthod, Thorsten Ohl,
Danny Willems, Kate Deplaix, Enguerrand Decorne, Grégoire Lionnet,
Jaap Boender, Gabriel Scherer, Gabriel Cardoso, Yuta Sato, Sora Morimoto,
Christophe Lecointe, Arnaud Parant, Jérôme Maloberti, Charly Chevalier,
[Jean-Henri Granarolo, Simon Castellan, Barbara Lepage, Séverine Maingaud,
Mauricio Fernandez, Michael Laporte, Nataliya Guts, Archibald Pontier,
Jérôme Velleine, Charles Oran, Pierre Clairambault,
Cécile Herbelin]{style="font-size: smaller;"}
…
The sports social network https://besport.com/news
{.parttitle}
{.partsubtitle}
$ ocsigenserver -c config
Where config
is something like:
<ocsigen>
<server>
<port>8080</port>
<commandpipe>local/var/run/mysite-cmd</commandpipe>
<logdir>local/var/log/mysite</logdir>
<datadir>local/var/data/mysite</datadir>
<charset>utf-8</charset>
<debugmode/>
<extension findlib-package="ocsigenserver.ext.staticmod"/>
<host hostfilter="mydomain.com">
<static dir="local/var/www/staticdir"/>
</host>
</server>
</ocsigen>
{pause #lib down-at-unpause=downserver}
{.vspace}
{.server}
let () =
Ocsigen_server.start
[ Ocsigen_server.host [Staticmod.run ~dir:"static" ()]]
Example of Dune file for this program:
(executable
(public_name myproject)
(name main)
(libraries
ocsigenserver
ocsigenserver.ext.staticmod))
Compile with:
dune build
Find more complex configuration examples in the manual
{#downserver}
Create a new project:
$ dune init project --kind=executable mysite
$ cd mysite
{.pause}
Define a service in bin/main.ml
:
{#servunit .server}
let myservice =
Eliom_service.create
~path:(Eliom_service.Path ["foo"])
~meth:(Eliom_service.Get Eliom_parameter.unit)
()
{.hidden #servparam .server}
let myservice = Eliom_service.create ~path:(Eliom_service.Path ["foo"]) ~meth:(Eliom_service.Get (Eliom_parameter.(string "myparam" ** int "i"))) (){#servparamexpl .encadré}
Use module
Eliom_parameter
to specify which GET or POST parameters you want your page to take.Eliom checks types automatically and converts them to OCaml values
Register a handler:
{#regunit .server}
let () =
Eliom_registration.Html.register ~service:myservice
(fun () () ->
Lwt.return
Eliom_content.Html.F.(html (head (title (txt "The title")) [])
(body [h1 [txt "Hello"]])))
{.hidden #regparam .server}
let () = Eliom_registration.Html.register ~service:myservice (fun (myparam, _i) () -> Lwt.return Eliom_content.Html.F.(html (head (title (txt "The title")) []) (body [h1 [txt myparam]]))){#regparamexpl .encadré}
The service handler now takes as first argument something of type
string * int
{.hidden #regparam2 .server}
let () = Eliom_registration.File.register ~service:myservice (fun (_myparam, _i) () -> Lwt.return "filename"){.encadré}
Services can return other types of content.
For example, we can return a file:
{.hidden #regparam3 .server}
let () = let a = ref 0 in Eliom_registration.Action.register ~service:myservice (fun (myparam, _i) () -> a := a + 1; Lwt.return ()){.encadré}
or an action.
Actions are services performing a side-effect. Handlers do not return any value. By default, the current page is regenerated.
{.hidden #regparam4 .server}
let () = Eliom_registration.Html.register ~service:myservice (fun (myparam, _i) () -> Lwt.return ([%html{|<html><head><title></title></head> <body><h1>Hello</h1></body> </html>|}){.encadré}
A PPX extension provided by Tyxml makes it possible to use regular HTML syntax if you prefer
{.hidden #regparam5 .server}
let () = Eliom_registration.Html.register ~service:myservice (fun (myparam, _i) () -> Lwt.return Eliom_content.Html.F.(html (head (title (txt "The title")) []) (body [p [p [txt myparam]]]))) ^^^^^^^^^^^^^^^ Error: This expression has type [> p ] elt = 'a elt but an expression was expected of type [< p_content ] elt = 'b elt Type 'a = [> `P ] is not compatible with type 'b = [< `A of phrasing_without_interactive | `Abbr | `Audio of phrasing_without_media ... | `Output | `PCDATA | `Progress | `Q ... | `Wbr ] The second variant type does not allow tag(s) `P{.encadré}
Tyxml type-checks your HTML! This makes sure at compile time your program will never generate incorrect pages!
{pause #tyxmlend down-at-unpause=sdkjf}
Start the server with static files and Eliom:
{.server}
let () =
Ocsigen_server.start
~command_pipe:"local/var/run/mysite-cmd"
~logdir:"local/var/log/mysite"
~datadir:"local/var/data/mysite"
~default_charset:(Some "utf-8")
[
Ocsigen_server.host
[ Staticmod.run ~dir:"local/var/www/mysite" ()
; Eliom.run () ]
]
{pause #sdkjf down-at-unpause=servicesdown}
Add packages ocsipersist-sqlite
, ocsigenserver.ext.staticmod
and eliom.server
to file bin/dune
, in the "libraries" section and create the directories used in the configuration just above.
Compile and run:
$ dune exec mysite
Go to http://localhost:8080/foo to test your program.
{pause focus-at-unpause=servunit exec-at-unpause}
document.querySelector("#servunit").classList.add("focused")
{pause unstatic-at-unpause="servunit" static-at-unpause="servparam" exec-at-unpause}
document.querySelector("#servunit").classList.remove("focused")
document.querySelector("#servparam").classList.add("focused")
{pause unstatic-at-unpause="servparamexpl" focus-at-unpause=regunit exec-at-unpause}
document.querySelector("#servparam").classList.remove("focused")
document.querySelector("#regunit").classList.add("focused")
{pause unstatic-at-unpause="regunit" static-at-unpause="regparam" focus-at-unpause=regparam exec-at-unpause}
document.querySelector("#regunit").classList.remove("focused")
document.querySelector("#regparam").classList.add("focused")
{pause unstatic-at-unpause="regparam" static-at-unpause="regparam2" focus-at-unpause=regparam2 exec-at-unpause}
document.querySelector("#regparam").classList.remove("focused")
document.querySelector("#regparam2").classList.add("focused")
{pause unstatic-at-unpause="regparam2" static-at-unpause="regparam3" focus-at-unpause=regparam3 exec-at-unpause}
document.querySelector("#regparam2").classList.remove("focused")
document.querySelector("#regparam3").classList.add("focused")
{pause unstatic-at-unpause="regparam3 regparamexpl" static-at-unpause="regparam" focus-at-unpause=regparam exec-at-unpause}
document.querySelector("#regparam3").classList.remove("focused")
document.querySelector("#regparam").classList.add("focused")
{pause unstatic-at-unpause="regparam" static-at-unpause="regparam4" focus-at-unpause=regparam4 exec-at-unpause}
document.querySelector("#regparam").classList.remove("focused")
document.querySelector("#regparam4").classList.add("focused")
{pause unstatic-at-unpause="regparam4" static-at-unpause="regparam" focus-at-unpause=regparam exec-at-unpause}
document.querySelector("#regparam4").classList.remove("focused")
document.querySelector("#regparam").classList.add("focused")
{pause unstatic-at-unpause="regparam" static-at-unpause="regparam5" focus-at-unpause=regparam5 exec-at-unpause}
document.querySelector("#regparam").classList.remove("focused")
document.querySelector("#regparam5").classList.add("focused")
{pause down-at-unpause="tyxmlend" unfocus-at-unpause="regparam5" exec-at-unpause}
document.querySelector("#regparam5").classList.remove("focused")
{#servicesdown}
Server-side session data is stored in Eliom references:
{#eref1 .server}
let r =
Eliom_reference.eref
~scope:Eliom_common.default_session_scope 0
let f () =
let%lwt v = Eliom_reference.get r in
Eliom_reference.set r (v + 1);
{#eref2 .hidden .server}
let r =
Eliom_reference.eref
~persistent:"myrefname"
~scope:Eliom_common.default_session_scope 0
let f () =
let%lwt v = Eliom_reference.get r in
Eliom_reference.set r (v + 1);
{pause unstatic-at-unpause="eref1" static-at-unpause="eref2"}
{pause}
{.encadré #scopes style="width:550px; left:650px; top:-140px; "}
Eliom references have a scope!
|Request|
request_scope
| |Tab|default_process_scope
| |Session|default_session_scope
| |Session group|default_group_scope
| |Site|site_scope
| |Global|global_scope
|
{.exo}
- On your main page, add a form with a single text input field for a user name.
Submiting this form sends the user name to a POST service, that will register your user name.
When the user name is already known, your main page displays this name, and a logout button.
- Implement a second page, and add HTML links from one page to the other
{pause}
Hints:
- See an example of form and POST service here
- Use an action (service with no output, that will just redisplay the page after perfoming a side-effect) to set a scoped reference with the user name
- To close the session use function
Eliom_state.discard_all
Test your app with several browsers to check that you can have several users simultaneously.
{pause down-at-unpause=endstep4}
{.comment} See the solution here.
{.exo} Advanced version: instead of using a reference with scope session, create a session group whose name is the user name. Check that you can share session data across multiple browsers.
{#endstep4}
Services have many other features:
- Services can be identified by a path
and/or by a name added automatically by Eliom as (GET or POST) parameter - Secure services (csrf-safe, secure sessions, https only …)
- Dynamic creation of services [Continuation based Web Programming]{.encadré style="position: relative; left: 40px; top: 20px;"}
- Scoped services (?scope)
- Temporary services (?max_use, ?timeout …)
{.vspace}
{pause}
Example:
- A user submit a form with some data
- You ask Eliom to create dynamically a new temporary service, identified by an auto-generated name,
The form data will be saved in the closure!
[Functional Web Programming!]{.encadré style="position: relative; left: 700px; top: -100px;"}
{.parttitle}
{.partsubtitle}
With Eliom, you can write a client-server Web and mobile app as a single
distributed app!
To do that, you need a dedicated buid system.
Use the default basic application template provided by Eliom to get it:
eliom-distillery --template app.exe -name myapp
Have a look at files myapp.eliom
and myapp_main.eliom
.
Insert the following line in myapp.eliom
:
{.client}
let%client () = print_endline "Hello"
See the result in your browser's console.
{pause down-at-unpause=clientdown1}
{.server .b}
let%server message = "Hello"
{.client .t}
let%client () = print_endline ~%message
The values are send together with the page (Eliom never calls the server if you don't ask to).
{#clientdown1}
{pause down-at-unpause=clientdown2}
{.server .b}
let%rpc f (i : int) : unit Lwt.t = Lwt.return (i + 10)
{.client .t}
let%client () = ... let%lwt v = f 22 in ...
Warning: type annotations are mandatory
{#clientdown2}
{pause up-at-unpause=clientdown2}
{.server #onclick}
button ~a:[a_onclick [%client (fun ev -> ... )]] [ ... ]
{.shared .b .bb #lwt_js_events}
let%shared mybutton s =
let b = button [txt "click"] in
let _ =
{.client .t .b .tt .bb}
[%client (Lwt_js_events.clicks (To_dom.of_element ~%b)
(fun ev -> Dom_html.window##alert(Js.string ~%s) : unit))
]
{.shared .t .tt}
in
d
Warning: type annotations are mandatory
{pause down-at-unpause=clientdown3}
The code is actually included in the client-side program as a function, which is called when the page is received.
On this example, you can see a few new concepts:
To_dom.of_element
(orof_div
, etc.) is used to get the DOM node corresponding to some TyxmlEliom_content.Html.D
nodeJs.string
is used to convert an OCaml string to a Javascript string##
is a syntax used to call Javascript methods in typesafe way. Similarly,##.
is used to access Javascript object fields.Lwt_js_events
defines a very simple and powerful and simple way to bind interface events (hereclicks
means: "for all clicks, call the function")
{#clientdown3}
Generating pages on the client or the server depending on cases
This is a typical example of service definition and registration for a client-server Web and mobile app:
{.server .b}
let%server myservice = Eliom_service.create
~path:(Path ["foo"])
~meth:(Get any)
()
{.client .b .t}
let%client myservice = ~%myservice
{.shared .t}
let%shared () =
Eliom_registration.Html.register
~service:myservice
(fun get_params () -> ...)
If the service is registered on both sides, the server side version will be used for the first call (or for indexing by search engines) the client side version will be used for subsequent calls (or on the mobile app)
{.exo}
Modify your program to use shared services
Check that when you click on the link, the second page is generated on the client
Hints:
- For that version, we will keep regular HTML forms with actions implemented on the server only (which means that the app will restart with a server-side generated page every time you connect or disconnect).
- To keep the username on client side, use a regular reference. The connection service handler must set both the server-side scoped reference and the client-side reference (using a client-value). This client-side reference must also be set every time the client process starts, that is, when a page is generated on the server (subsequent pages are generated on the client, without stopping the client-side process).
{.comment} See the solution here.
{.exo}
Add a text input field in the connected version of your page, with a submit button, that will send the message to the server.
The server will just display the message in its console
Hints:
-
This time, we won't submit the form to a new service, but use a RPC. Use regular Tyxml
input
fields instead of theForm
module. -
Take example either on the [onclick]{} example above or use [Lwt_js_events]{}.
Eliom has several ways to do server to client communication.
Here we will use module Eliom_notif
.
It gives you the possibility to define resources on which user can listen
or notify.
On server side, instanciate functor Eliom_notif.Make_Simple
.
Type key
is for resource ids. Type notification
is for the messages
you want to send.
{.server}
module%server Notif = Eliom_notif.Make_Simple (struct
type identity = ...
type key = ...
type notification = ...
let get_identity = ...
end)
Call Notif.listen
from server side
if you want to be notified when there is a new message send on a resource.
If you want to send a message to all users listening on a given resource, call function Notif.notify
from server side.
On client side, ~%(Notif.client_ev ())
is a React event of type
(key, notif) React.E.t
. Use it to receive the messages.
{pause down-at-unpause=notifdown}
Example:
{.client}
React.E.map (fun notification -> ...) ~%(Notif.client_ev ())
{#notifdown}
{.exo}
Make it possible to send a message to another user connected on the same server.
Modify your form to have two fields: one for the recipient name, and one for the message. Change your RPC accordingly.
Display the messages you receive in the page.
Make sure that you receive only the message sent to you.
Hints:
- Instantiate functor
Eliom_notif.Make_Simple
on server side, withget_identity
being the function to get current user from your scoped reference. Resources are chat channels. Here a channel is identified (key
) by the recipient name (one channel for each user, where everyone can write). - Your RPC to send a message will now notify the recipient (server-side)
- Every time the client process starts (i.e. every time a page is generated on the server), you need to start listening on your own channel (on server-side), and ask the client to react to event
client_ev
- Use module
Manip
to append the new element to the page
{.comment} See the solution here.
{.parttitle}
Client-server widgets
{style="height: 700px;"}
Library
user management, passwords, etc.
Application template
Code samples
Ocsigen Start is the easiest way
to learn Eliom!
{style="height: 200px;"}
eliom-distillery --template os.pgocaml -name myapp
Then read the README file
{style="width: 500px; position: absolute; right: 0;"}
Code run in a webview Cordova or Capacitor
Pages generated on client-side
Exact same code as the Web app
Use Ocsigen Start to test
{style="height: 1900px;"}
Please read the main documentation page first!
and look at each example in Ocsigen Start's template.
{style="text-align: right;"}
[Slides powered by Slipshow]{style="color: #aaa"}.