on text-jsstring help sign in
jsaddle/README.md
202 lines 9.9 kB history raw
1JSaddle
2=======
4JSaddle is an interface for JavaScript that works with GHCJS or GHC. It is used by [ghcjs-dom](https://github.com/ghcjs/ghcjs-dom)
5when compiled with GHC and is compatible with [ghcjs-dom](https://github.com/ghcjs/ghcjs-dom) when compiled with GHCJS.
7You can use JSaddle directly as follows:
9``` Haskell
10module Main ( main ) where
12import Control.Monad.IO.Class (MonadIO(..))
13import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)
14import Control.Lens ((^.))
15import Language.Javascript.JSaddle
16 (run, jsg, js, js1, jss, fun, valToNumber, syncPoint)
18main = run 3709 $ do
19 doc <- jsg "document"
20 doc ^. js "body" ^. jss "innerHTML" "<h1>Kia ora (Hi)</h1>"
22 -- Create a haskell function call back for the onclick event
23 doc ^. jss "onclick" (fun $ \ _ _ [e] -> do
24 x <- e ^. js "clientX" >>= valToNumber
25 y <- e ^. js "clientY" >>= valToNumber
26 newParagraph <- doc ^. js1 "createElement" "p"
27 newParagraph ^. js1 "appendChild" (
28 doc ^. js1 "createTextNode" ("Click " ++ show (x, y)))
29 doc ^. js "body" ^. js1 "appendChild" newParagraph
30 return ())
32 -- Make an exit button
33 exitMVar <- liftIO newEmptyMVar
34 exit <- doc ^. js1 "createElement" "span"
35 exit ^. js1 "appendChild" (
36 doc ^. js1 "createTextNode" "Click here to exit")
37 doc ^. js "body" ^. js1 "appendChild" exit
38 exit ^. jss "onclick" (fun $ \ _ _ _ -> liftIO $ putMVar exitMVar ())
40 -- Force all all the lazy evaluation to be executed
41 syncPoint
43 -- In GHC compiled version the WebSocket connection will end when this
44 -- thread ends. So we will wait until the user clicks exit.
45 liftIO $ takeMVar exitMVar
46 doc ^. js "body" ^. jss "innerHTML" "<h1>Ka kite ano (See you later)</h1>"
47 return ()
48```
50When compiled with GHC this code will run a Warp web server you can connect to at `http:\\localhost:3709\`.
52Here is the same program using ghcjs-dom to call JSaddle. As well as better type safety this version uses
53JS FFI directly (rather than via JSaddle) when compiled with GHCJS:
55``` Haskell
56module Main (
57 main
58) where
60import Control.Monad.IO.Class (MonadIO(..))
61import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)
63import GHCJS.DOM (run, syncPoint, currentDocument)
64import GHCJS.DOM.Document (getBody, createElement, createTextNode)
65import GHCJS.DOM.Element (setInnerHTML)
66import GHCJS.DOM.Node (appendChild)
67import GHCJS.DOM.EventM (on, mouseClientXY)
68import qualified GHCJS.DOM.Document as D (click)
69import qualified GHCJS.DOM.Element as E (click)
71main = run 3708 $ do
72 Just doc <- currentDocument
73 Just body <- getBody doc
74 setInnerHTML body (Just "<h1>Kia ora (Hi)</h1>")
75 on doc D.click $ do
76 (x, y) <- mouseClientXY
77 Just newParagraph <- createElement doc (Just "p")
78 text <- createTextNode doc $ "Click " ++ show (x, y)
79 appendChild newParagraph text
80 appendChild body (Just newParagraph)
81 return ()
83 -- Make an exit button
84 exitMVar <- liftIO newEmptyMVar
85 Just exit <- createElement doc (Just "span")
86 text <- createTextNode doc "Click here to exit"
87 appendChild exit text
88 appendChild body (Just exit)
89 on exit E.click $ liftIO $ putMVar exitMVar ()
91 -- Force all all the lazy evaluation to be executed
92 syncPoint
94 -- In GHC compiled version the WebSocket connection will end when this
95 -- thread ends. So we will wait until the user clicks exit.
96 liftIO $ takeMVar exitMVar
97 setInnerHTML body (Just "<h1>Ka kite ano (See you later)</h1>")
98 return ()
99```
101When compiled with GHC this code will run a Warp web server you can connect to at `http:\\localhost:3708\`.
103## How does it work on GHC
105As of version 0.5 it runs a small warp server that provides [index.html](https://github.com/ghcjs/jsaddle/blob/master/data/index.html) and
106[jsaddle.js](https://github.com/ghcjs/jsaddle/blob/master/data/jsaddle.js). The [jsaddle.js](https://github.com/ghcjs/jsaddle/blob/master/data/jsaddle.js) code connects to the server using WebSockets and processes JSON commands from the server and sends the result back over the same WebSocket.
108### Why use WebSockets?
110Older versions relied on WebKitGTK and JavaScriptCore, but in WekKit2 the interface JSaddle used is only available to WebKit Extensions
111so most of the benefits of this interface are lost (extensions are not in the same process as Gtk and RPC would have been needed).
112The general advice for people migrating from WebKit1 to WebKit2 seems to be to use JavaScript. There is an interface in WebKit2
113to send JavaScript code from the host application, but it does not seem to provide any advantages over the WebSockets approach.
115### JSVal
117When compiling with GHC a `JSVal` is represented by a integer key for a JavaScript `Map` in `jsaddle.js` code.
118The first five keys `[0..4]` are
119`null`, `undefined`, `true`, `false` and `window`. This means the Haskell code can quickly create a JSVal with one of
120these values. Both the Haskell code and `jsaddle.js` can allocate new `JSVal` keys. The Haskell code allocates negative
121keys and `jsaddle.js` allocates positive keys to avoid clashing.
123The haskell code will not know the value of the to go with the key, but it will include it in commands to the server
124that are sort of "hey call this function and and put the result in the map with this key"). This allows for the
125lazy execution of the JSaddle code.
127A finalizer is added on the Haskell code and an asynchronous `FreeJSVal` command is automatically sent to `jsaddle.js`
128when the JSVal is no longer reachable. The `jsaddle.js` code then deletes the key from the Map.
130### JSStrings
132These are haskell `Text` type and so reside on the native side of the WebSocket. If you want to make avoid transferring large string values over the WebSocket use `toJSVal` to conevt it to a `JSVal`.
134### Lazy Execution
136To improve performance commands are sent to `jsaddle.js` in batches that contain any number of asynchronous commands followed by one synchronous one. To get the best performance you should avoid synchronous commands, these are:
138| Synchronous Command | What will trigger it |
139| ------------------- | ------------------------------------------------------------ |
140| **ValueToString** | converting a `JSVal` to a `JSString` (`Text` when using GHC) |
141| **ValueToBool** | converting a `JSVal` to a `Bool` |
142| **ValueToNumber** | converting a `JSVal` to a `Double` |
143| **ValueToJSON** | converting a `JSVal` to a `JSON` |
144| **DeRefVal** | converting a `JSVal` to a `Value` |
145| **IsNull** | testing to see if a `JSVal` is `null` |
146| **IsUndefined** | testing to see if a `JSVal` is `undefined` |
147| **InstanceOf** | testing to see if a `JSVal` is an `instanceOf` a given type |
148| **StrictEqual** | testing too `JSVal` values for equality (`===`) |
149| **PropertyNames** | getting the list of properties in an JS object |
151So basically if you don't look inside a `JSVal` to find out something the state of the JavaScript context you will not produce any
152synchronous commands and your code will run fast. As soon as you look at what is in a `JSVal` we have to wait for `jsaddle.js` to
153send the results back and your code will block.
155In some cases you may wish to make sure all the JSaddle commands are executed. You can use the `syncPoint` and `syncAfter` functions to
156force all the pending asynchronous commands to be executed.
159## How does it work on GHCJS
161It uses a handful of JS FFI calls to execute JavaScript functions indirectly. This indirection will be small compared to the overhead of the WebSockets approach (used when JSaddle is compiled with GHC), but it will be significant compared to hand crafted JS FFI calls.
163For the best performance you may want to write both JS FFI and JSaddle wrappers
164For your JavaScript code. This is the approach taken by recent versions of `ghcjs-dom`.
166For instance here is `getElementById` from the `ghcjs-dom-jsffi` (used by `ghcjs-dom` when compiled with GHCJS)
168``` Haskell
169foreign import javascript unsafe "$1[\"getElementById\"]($2)"
170 js_getElementById :: Document -> JSString -> IO (Nullable Element)
172getElementById ::
173 (MonadIO m, IsDocument self, ToJSString elementId) =>
174 self -> elementId -> m (Maybe Element)
175getElementById self elementId
176 = liftIO
177 (nullableToMaybe <$>
178 (js_getElementById (toDocument self) (toJSString elementId)))
179```
181Here is `getElementById` from `jsaddle-dom` (used by `ghcjs-dom` when compiled with GHC)
183``` Haskell
184getElementById ::
185 (MonadDOM m, IsDocument self, ToJSString elementId) =>
186 self -> elementId -> m (Maybe Element)
187getElementById self elementId
188 = liftDOM
189 (((toDocument self) ^. jsf "getElementById" [toJSVal elementId])
190 >>= fromJSVal)
191```
193## Exceptions
195JSaddle does not support exceptions well. When compiled with GHCJS an exception will result in termination of the thread
196at the point the exception is thrown.
198When using GHC the Haskell executions will probably continue for a while before the exception is received at all by the Haskell code
199(because the lazy execution of the JS code). The exception will be thrown when the the next synchronous command is executed.
200When it reaches the synchronous command a haskell type JSException will be thrown (this may not be the thread that initiated the
201command that caused the exception). You can use `syncPoint` and `syncAfter` to force the exception to be thrown.
202There are `JSM` versions of `catch` and `bracket` that also include a `syncPoint` call.