Ocsigen

Ocsigen
Quickstart
guide

Vincent Balat

FUN-OCaml - Berlin
Sep 16-17 2024

Use arrow keys to navigate

Step 1: Install

$ opam install ocsigen-start ocsipersist-sqlite-config

You can find these slides on ocsigen.org
and the source code of the sides with the solution of exercices on Github.

Refer to the main one page user manual to get more explanations.

Ocsigen
Ocsigen

Ocsigen Server

Eliom

Js_of_ocaml

Tyxml

Lwt

Ocsigen Start

Ocsigen Toolkit

Ocsigen i18n

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

Example app: Be Sport

The sports social network https://besport.com/news

Ocsigen Ocsigen

Ocsigen

First part: Server-side programming

Write your website in OCaml

Step 2: Ocsigen Server

As an executable

$ 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>

As an OCaml library

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

Step 3: Generating pages with Eliom

Create a new project:

$ dune init project --kind=executable mysite
$ cd mysite

Define a service in bin/main.ml:

let myservice =
  Eliom_service.create
    ~path:(Eliom_service.Path ["foo"])
    ~meth:(Eliom_service.Get Eliom_parameter.unit)
    ()

Register a handler:

let () =
  Eliom_registration.Html.register ~service:myservice
    (fun () () ->
      Lwt.return
         Eliom_content.Html.F.(html (head (title (txt "The title")) [])
                                    (body [h1 [txt "Hello"]])))

Start the server with static files and Eliom:

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 () ]
    ]

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.

Sessions: scoped references

Server-side session data is stored in Eliom references:

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);

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

Step 4: Implement a basic login mechanism

  1. 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.

  1. Implement a second page, and add HTML links from one page to the other

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.

See the solution here.

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.

Ocsigen

Service, reinvented

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
    • Scoped services (?scope)
    • Temporary services (?max_use, ?timeout …)

Example:

  1. A user submit a form with some data
  2. 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!

Ocsigen

Second part: Client-server programming

Write a Web and mobile app in OCaml

Running code on the client

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:

let%client () = print_endline "Hello"

See the result in your browser's console.

let%server () = ...
let%client () = ...
let%shared () = ...
Client-server code

Injections: using server-side values in client side code

let%server message = "Hello"
let%client () = print_endline ~%message

The values are send together with the page (Eliom never calls the server if you don't ask to).

RPC: calling a server-side function

let%rpc f (i : int) : unit Lwt.t = Lwt.return (i + 10)
let%client () = ... let%lwt v = f 22 in ...

Warning: type annotations are mandatory

Client-values: inserting client-side code in your pages

button ~a:[a_onclick [%client (fun ev -> ... )]] [ ... ]
let%shared mybutton s =
  let b = button [txt "click"] in
  let _ =
    [%client (Lwt_js_events.clicks (To_dom.of_element ~%b)
               (fun ev -> Dom_html.window##alert(Js.string ~%s) : unit))
    ]
  in
  d

Warning: type annotations are mandatory

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 (or of_div, etc.) is used to get the DOM node corresponding to some Tyxml Eliom_content.Html.D node
  • Js.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 (here clicks means: "for all clicks, call the function")
Client-server code

Client-side services

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:

let%server myservice = Eliom_service.create
                         ~path:(Path ["foo"])
                         ~meth:(Get any)
                         ()
let%client myservice = ~%myservice
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)

Ocsigen

Step 5: Client-server version of your program

Modify your program to use shared services

Check that when you click on the link, the second page is generated on the client

Hints:

  1. 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).
  2. 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).

See the solution here.

Step 6: Send a message to the server

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 the Form module.

  • Take example either on the onclick example above or use Lwt_js_events.

Sending a message to the client

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.

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.

Example:

React.E.map (fun notification -> ...) ~%(Notif.client_ev ()) 

Step 7: Send a message to another user

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:

  1. Instantiate functor Eliom_notif.Make_Simple on server side, with get_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).
  2. Your RPC to send a message will now notify the recipient (server-side)
  3. 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
  4. Use module Manip to append the new element to the page

See the solution here.

Ocsigen

Going further

Ocsigen Toolkit

Client-server widgets

Ocsigen Ocsigen Ocsigen Ocsigen

Ocsigen Start

Library
user management, passwords, etc.

Application template

Code samples
Ocsigen Start is the easiest way
to learn Eliom!

eliom-distillery --template os.pgocaml -name myapp

Then read the README file

Ocsigen Ocsigen Ocsigen Ocsigen Ocsigen
Ocsigen

Mobile apps

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

How to learn more?

Please read the main documentation page first!

and look at each example in Ocsigen Start's template.

Ocsigen
Slides powered by Slipshow.