Web page howto
A few tips to compile and integrate your programs in web pages.
Quick!
Okay! Make a minimal web page.
cat - > min.html <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script type="text/javascript" defer="defer" src="min.js"></script>
<title>Brr minimal example</title>
</head>
<body>
<noscript>Sorry, you need to enable JavaScript to see this page.</noscript>
</body>
</html>
EOF
Make a minimal program.
cat - > min.ml <<EOF
open Brr
let () =
El.set_children (Document.body G.document) El.[txt' "Hello World!"]
EOF
Compile your program to bytecode and then to JavaScript
ocamlfind ocamlc -g -linkpkg -no-check-prims -package brr min.ml
js_of_ocaml a.out -o min.js
Load the web page in your browser:
xdg-open min.html # Linux and XDG compliant systems
open min.html # macOS
start min.html # Windows
And the fancy OCaml console?
Poke your program by side effect by linking against the brr.poked
library. Make sure everything gets in by using -linkall
.
ocamlfind ocamlc -g -linkall -linkpkg -no-check-prims \
-package brr,brr.poked min.ml
Compile with js_of_ocaml
. This time it needs to see the cmis and you need to add its toplevel and dynlink JavaScript support:
js_of_ocaml $(ocamlfind query -r -i-format brr.poked) -I . \
--toplevel +toplevel.js +dynlink.js a.out -o min.js
Make sure you have the OCaml console extension installed in your browser, open min.html
and click on the OCaml tab of the developer tools.
Quick with Dune!
Assuming you created the two minimal sources min.ml
, min.html
from the previous section. Create this dune
file:
cat - > dune <<EOF
(executables
(names min)
(libraries brr)
(link_flags (:standard -no-check-prims))
(modes js))
(rule
(targets min.js)
(deps min.bc.js)
(action (run cp %{deps} %{targets})))
(alias
(name app)
(deps min.js min.html))
EOF
Now build and open the web page in your browser:
dune build @app
xdg-open _build/default/min.html # Linux and XDG compliant systems
open _build/default/min.html # macOS
start _build/default/min.html # Windows
And the fancy OCaml console?
Poke your program by side effect by linking against the brr.poked
library. Make sure to -linkall
the modules into the bytecode and tweak a bit the js_of_ocaml
invocation.
But first since you want to access your main program's modules disable the dune
renaming business:
echo '(wrapped_executables false)' >> dune-project
This dune
file should now do:
cat - > dune <<EOF
(executables
(names min)
(libraries brr brr.poked)
(link_flags (:standard -linkall -no-check-prims))
(modes byte))
(rule
(targets min.js)
(action
(run %{bin:js_of_ocaml}
-I %{lib:brr:.} -I .min.eobjs/byte ; to see the program's cmis
--toplevel +toplevel.js +dynlink.js %{dep:min.bc} -o %{targets})))
(alias
(name app)
(deps min.js min.html))
EOF
Now build and open the web page in your browser:
dune build @app
xdg-open _build/default/min.html # Linux and XDG compliant systems
open _build/default/min.html # macOS
start _build/default/min.html # Windows
Make sure you have the OCaml console extension installed in your browser, and click on the OCaml tab of the developer tools.
How do I run my program?
Most programs need to have the page HTML parsed to operate meaningfully. There are various ways of detecting this state but the simplest is to keep the execution of your program to a classical toplevel main
invocation and integrate your script in the web page with the defer
attribute:
<script type="text/javascript" defer src="myscript.js"></script>
This ensures it is fetched in parallel but only executed when the document has been parsed (visual explanation). Your main program can simply be:
let main () = Console.(log [str "DOM content loaded."])
let () = main ()
However at this point ressources like stylesheets, images, fonts, etc. may not have loaded. This means that the page layout is not accurate and you cannot compute magnitudes that depend on them. Also if you draw on a canvas with custom fonts these may not be loaded yet and be substituted by generic ones.
In these cases wait for the Brr.Ev.load
event on the window to make sure all ressources are loaded. Here is a snippet that does this:
open Fut.Syntax
let main () =
Console.(log [str "DOM content loaded."]);
let* _ev = Ev.next Ev.load (Window.as_target G.window) in
Console.(log [str "Resources loaded."]);
Fut.return ()
let () = ignore (main ())
Note that your main
is always non-blocking (or the browser kills you) and returns before the page loads. The callbacks you setup and the futures you trigger do however outlive that invocation.
Also, if your program, maybe indirectly, uses Stdlib.at_exit
– which is not made for the browser – these functions will execute at the beginning of the life of your web page as they are automatically executed after all toplevel OCaml executions are done.
Web page size
Watch your program size. Keep the tubes unclogged. Trim convenience dependencies and try not to integrate libraries providing functionality that already exists in the browser – like JSON parsing.
js_of_ocaml
performs excellent dead code elimination. All these modules of Brr
you are not using won't impact your page size. However it's only as good as it can be; global mutable state may be impossible to dead code away.
Avoid mentions of Format
Unless you really want to use it, avoid any mention of Format
. js_of_ocaml
can't help you on that one – likely due to the presence of global mutable state in Format
. With js_of_ocaml
3.6.0 and OCaml 4.09.0 this program:
open Brr
let () = Console.(log [str "Hey!"])
compiles, without special options, to 14ko. Adding this dead code:
open Brr
let pp_float = Format.pp_print_float
let () = Console.(log [str "Hey!"])
makes it 35ko, which is 2.5 larger.