Jan 3rd, 2016 - written by Kimserey with .
F# + WebSharper is an awesome combination to build web application. The only problem is that if you want to build a webapp larger than a to-do list, it’s hard to find examples to use as references. It is even harder to find tutorials that touches on overall design and answer questions like: How should I start? Where should I put the code? How should I separate the code, put it in different files, with different namespaces or modules? How many layers?
Asking these questions at the beginning of the development is important. All these questions if answered correctly will lead to a good structure for the application. Answered incorrectly, or not even asked, will most likely lead to unmaintainable/fragile/rigid (or whatever bad adjectives for coding) code.
Over the last few months, I have been working on a web app built in F# with WebSharper and came out with an architecture that caters to extensions and decoupling. Today, I will share this structure and hope that it will give you ideas and help you in your development.
So let’s get started!
The ideas behind this architecture was highly inspired by Addy Osmani blog post on Patterns for large application and more precisely on modular architecture.
The idea of a modular achitecture is that the application is composed by small pieces (modules) which are completely independent from each other. One lives without knowing the others and none of the modules have dependencies on other modules. The patterns explained in the blog post of Addy Osmani goes much deeper and defines many other patterns but to me the most crucial understanding is that we should strive to manage dependencies. Coupling is the worst enemy of large applications. It stops us from changing or removing pieces of the application and brings FUD in our daily development. I’ve been there.. and it’s most certainly not fun.
Here’s how the architecture looks like:
Two important points to note from the diagram:
From the diagram we can see five clear boundaries where we can place our codes:
Dependencies flow downward only. Elements don’t reference other elements from the same level.
Following this rule enables us flexibility. We will then be able to easily remove or add pages. We can also substitute a module for another in a webpart or substitute a webpart for another in a page without issues as they are independent from each other.
Now that we understand the architecture, let’s see how we can apply it in F# with WebSharper.
To see how we can apply this architecture, we will build a sample app which contains 3 pages:
If you aren’t familiar with WebSharper.UI.Next html notation, I have wrote a previous blog post where I gave some explanations about the UI.Next.Html notation and how to use the reactive model Var/View of UI.Next.
First, we start by creating empty containers for our future code:
Following the architecture diagram, we place the common code in its own library. The Site project contains the Shell / Page / Webpart / Module categories.
F# allows us to ensure the references are one way only. Only bottom files can reference top files, your functions must be defined first before you can use it. Therefore, if we keep the modules at the top level, it will indirectly make the modules the code with the least dependencies in the project.
The domain contains all the domain types. One of this type is the Page
record type which contains the information about how a page should be displayed and from where it can be accessed.
1
2
3
4
5
6
7
8
9
10
11
12
13
type Page = {
Title: string
Content: Doc
// defines how the page should be displayed
DisplayOption: DisplayOption
// defines how the page should be accessed
AccessOption: AccessOption
// defines the route url
Route: Route
}
and DisplayOption = PageWithMenu | FullPage
and AccessOption = Menu of string | Other
and Route = Route of string list
We then write the code to compose the shell and the navbar. The links in the navbar will be constructed based on what is defined in the pages. If the page is accessible through nav, it will create a button link in the nav. Then the display option is used to define whether the page will be full screen or embedded with nav at the top.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module Main =
/// Embeds the content in a page with nav if needed
let mkContent ctx curr =
match curr.DisplayOption with
| DisplayOption.FullPage ->
curr.Content
| DisplayOption.PageWithMenu ->
let routes =
All.pages |> List.choose (fun p ->
match p.AccessOption with
| AccessOption.Menu title -> Some (title, ctx.Link p.Title)
| AccessOption.Other -> None)
curr.Content
|> Menu.Static.embed (ctx.Link "Home") curr.Title routes
/// Builds a sitelet given a page
let mkPage page =
Sitelet.Content
<| page.Route.Value
<| page.Title
<| fun ctx -> Content.Page (Title = page.Title, Body = [ mkContent ctx page ])
/// Builds the site by summing all sitelets
let main() =
All.pages |> List.map mkPage |> Sitelet.Sum
Each page is defined as a sitelet using the route and title and then sum together to form the main sitelet.
The menu is defined as followed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module Menu =
[<JavaScript>]
module private Client =
open WebSharper.JavaScript
let brand homeLink =
divAttr [ attr.``class`` "navbar-brand"
attr.style "cursor: pointer;"
on.click (fun _ _ -> JS.Window.Location.Replace(homeLink)) ] [ text "Arche" ]
module Static =
open WebSharper.Sitelets
let private nav homeLink curr routes =
let navBar left right =
let navHeader =
divAttr [ attr.``class`` "navbar-header" ]
[ buttonAttr [ attr.``class`` "navbar-toggle collapsed"
Attr.Create "data-toggle" "collapse"
Attr.Create "data-target" "#menu"
Attr.Create "aria-expanded" "false" ]
[ spanAttr [ attr.``class`` "sr-only" ] []
spanAttr [ attr.``class`` "icon-bar" ] []
spanAttr [ attr.``class`` "icon-bar" ] []
spanAttr [ attr.``class`` "icon-bar" ] [] ]
client <@ Client.brand homeLink @>]
let navMenu =
divAttr [ attr.``class`` "collapse navbar-collapse"; attr.id "menu" ] [ left; right ]
navAttr [ attr.``class`` "navbar navbar-default" ] [ divAttr [ attr.``class`` "container-fluid" ] [ navHeader; navMenu ] ]
let navButtons =
let liList =
routes
|> List.map (fun (title, route) ->
liAttr [ if curr = title then yield attr.``class`` "active" ]
[ aAttr [ attr.href route ] [text title] ])
|> Seq.cast
|> Seq.toList
ulAttr [attr.``class`` "nav navbar-nav"] liList
navBar navButtons Doc.Empty
let embed homeLink currRoute routes doc =
[ nav homeLink currRoute routes :> Doc
divAttr [ attr.``class`` "container-fluid" ] [ doc ] :> Doc
] |> Doc.Concat
There are two modules within the Menu module.
Client contains the code which will be converted to JS. Static contains the code that is used by the Sitelet to compose the page. In other modules, there will be one more module called Server
which will contain the WebSharper RPC calls.
Now the shell is ready to welcome all the pages that we define and we won’t need to touch it anymore (sounds like the open close… you know, open for extension close to modification).
Pages are pretty straightforward:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let pages = [
{ Title = "Home"
Route = Route.Create [ "" ]
Content = client <@ Home.Client.page() @>
DisplayOption = DisplayOption.PageWithMenu
AccessOption = AccessOption.Other }
{ Title = "Map"
Route = Route.Create [ "map" ]
Content = client <@ Map.Client.webpart() @>
DisplayOption = DisplayOption.PageWithMenu
AccessOption = AccessOption.Menu "Map" }
{ Title = "Weather"
Route = Route.Create [ "weather" ]
Content =
divAttr [ attr.style "max-width: 600px; margin: auto;" ] [ client <@ Weather.Client.webpart() @> ]
DisplayOption = DisplayOption.PageWithMenu
AccessOption = AccessOption.Menu "Weather" }
]
As expected, they define the title, route, content of the page and how it should be displayed and accessed.
In our sample, the webparts are straightforward. But in other apps those might be more complex. The role of the webpart is to combine the modules together to form a part of functionality that is useful to the user.
Here we just need to combine the Map
module with the LocationPicker
module for the Map webpart
and same for the Weather webpart
.
Let’s see the Weather webpart
:
1
2
3
4
5
6
7
8
9
10
11
12
module Weather =
[<JavaScript>]
module Client =
open WebSharper.UI.Next.Html
let webpart() =
let (locationDoc, locationView) =
LocationPicker.Client.page()
panel "Weather" [ divAttr [ attr.style "text-align: center;" ]
[ Weather.Client.page locationView ]
div [ locationDoc ] ]
panel
is a helper defined in the Bootstrap module
in Common
. I won’t explain it but you can find it in the code.
The Weather webpart
uses the LocationPicker module
which returns its content plus a view on a location variable. The Weather module
takes a location view and uses it to display the weather for this particular location.
It is important to note that the LocationPicker module
is not directly used in the Weather module
. Weather module
accepts a view as an argument, it doesn’t matter where the view comes from. It is the role of the webpart to bind the LocationPicker module
location view result to the Weather module
and to ensure that LocationPicker
is not dependent on Weather
and vice versa. Modules should not reference each other.
The last part of our architecture is the modules. We will look at Weather module
, you can have a look at the full code here. For that we need to define a Forecast
record type:
1
2
3
4
5
6
7
type private Forecast = {
Title: string
Description: string
ImageUrl: string
Temperature: decimal
TemparatureMinMax: decimal * decimal
}
To get the weather, I used http://openweathermap.org/api. Using the JsonProvider
from FSharp.Data
simplifies the interaction with the open weather api. We then provide a RPC
call which returns the Forecast
depending on the city given. RPC
allows Server
calls to be called from Client
code. Serialization is handled automatically by WebSharper and we can use the same types that is returned from the Server
in the Client
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module private Server =
type WeatherApi =
JsonProvider<""" {"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"},{"id":311,"main":"Drizzle","description":"rain and drizzle","icon":"09d"}],"base":"cmc stations","main":{"temp":282.66,"pressure":999,"humidity":87,"temp_min":281.75,"temp_max":283.15},"wind":{"speed":5.7,"deg":140},"rain":{"1h":0.35},"clouds":{"all":75},"dt":1451724700,"sys":{"type":1,"id":5091,"message":0.0043,"country":"GB","sunrise":1451721965,"sunset":1451750585},"id":2643743,"name":"London","cod":200} """>
let apiKey =
Arche.Common.Config.value.OpenWeather.ApiKey
[<Rpc>]
let get city =
async {
let! weather = WeatherApi.AsyncLoad(sprintf "http://api.openweathermap.org/data/2.5/weather?q=%s&units=metric&appid=%s" city apiKey)
return weather.Weather
|> Array.tryHead
|> Option.map (fun head ->
{ Title = sprintf "%s, %s" weather.Name weather.Sys.Country
Description = head.Main
ImageUrl = sprintf "http://openweathermap.org/img/w/%s.png" head.Icon
Temperature = weather.Main.Temp
TemparatureMinMax = weather.Main.TempMin, weather.Main.TempMax })
}
We then construct a reactive doc based on the city given. Everytime the city changes, the RPC
is called and the doc is updated.
View.MapAsync: ('A -> Async<'B>) -> View<'A> -> View<'B>
takes as first argument an async
function which is ran every time the view is updated and returns a view of the result of the async
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[<JavaScript>]
module Client =
open WebSharper.UI.Next.Html
open WebSharper.UI.Next.Client
let page city =
city
|> View.MapAsync Server.get
|> View.Map (function
| Some forecast ->
let (min, max) =
forecast.TemparatureMinMax
let temperature txt =
span [ text txt; spanAttr [] [ text "°C" ] ]
div [ imgAttr [ attr.src forecast.ImageUrl ] []
divAttr [ attr.style "" ]
[ temperature (sprintf "%i" <| int forecast.Temperature)
divAttr [] [ text forecast.Title ] ]
pAttr [] [ temperature (sprintf "Min: %i" <| int min)
text " - "
temperature (sprintf "Max: %i" <| int max) ] ]
| None -> p [ text "No forecrast" ])
|> Doc.EmbedView
Modules are the last element of the architecture. They must be completely independent and can be added or removed with ease from webparts.
Today, we have seen one way of structuring a web app which allows us to reduce coupling between elements and allows rapid changes and adding new features easily. We have built a shell which doesn’t need to be touched anymore and automatically add links to its menu based on the pages that we register. Finally this structure also allows us and other developers to not be confused about where to place code and defined a clear way for components to interact with each other. I hope you enjoyed reading this post as much I enjoyed writing it. As usual, if you have any questions, you can hit me on twitter @Kimserey_Lam. Thanks for reading!