Lee Richardson on 2020-09-19 #fsharp #fable #blog #javascript #static site
Trying to do things the simple way... ish.
Update: This is old and uses Fable 2. Fable 3 is easier to use and doesn't need fable-splitter or fable-loader. Maybe I'll update this post one day with the details.
If you just want the "how" rather than the "why" then skip down to the Fable heading.
This blog you're reading right now is a statically generated website. That means it builds HTML and CSS and serves them up raw. Nothing dynamic, no DOM manpiulation, nothing. It uses Fornax and Tailwind CSS. Part of the reason for doing this is that I like things to be as simple as they need to be, part is that I want it to be blazing fast, and part is just for fun. Up until this post there was no JavaScript at all. I wanted to put a tag filter on the index page and found myself wondering where my compromise lies between minimalism and functionality.
I went through a few iterations but settled on using F# compiled with Fable. Read on for why and how.
Raw JavaScript
After the first time I used TypeScript I swore I would never go back to JS. TS feels safer and cleaner, but it's also a lot of overhead. Modern web development almost always uses large frameworks and tools with literally thousands of dependencies. At this point I had no JS, so no Webpack, Gulp, Grunt, Angular, React, TypeScript. Nothing except NPM to build Tailwind. I tried not to use it but Tailwind really wants you to use it through NPM so I ran into problems without it. Again trying to do it a more simple way turned out not to be easy. There may be another blog post in that.
Anyway, for the functionality I needed, raw JS should absolutely have been the way to go... Buuuut I despise JS and this is a personal project so I wanted it to be fun.
TypeScript
So I installed TypeScript and poked around. The code generated was pretty large, but it's backwards compatible so fair enough. Do I want backwards compatibility? Nah. For a personal project website I just want to use clean modern features and not worry about supporting IE or anything else ancient. Perhaps in the future. TS was absolutely a fine solution. I use it in my day job, it's well supported... but...
Fable
Why not try F# on the front end as well? I love F#. Fornax allows me to use F# for HTML generation. Why not for JS generation too? So step 1, add a Fornax generator and try to run the Fable compiler through the CLI because I don't need the whole Fable webapp here.
CLI
Fishing around for the right package I found fable-splitter
. This lets you call the Fable compiler and simply hand it some F# to get some JS back. Fantastic. Let's give it a whirl.
npm install -D fable-splitter fable-compiler @bable/core
test.fsx
#r "../_lib/Fable.Core.dll"
#r "../_lib/Browser.Dom.dll"
#r "../_lib/Browser.Event.dll"
open Browser
let el = document.querySelector "#site-description"
el.textContent <- "Testeroo"
First we reference the DLLs which I obtained from the Nuget packages. On my todo list is to look into if the new
#r "nuget:Fable.Core"
syntax works here, but for now I shall refernce the files as is the standard for a Fornax project.We open the
Browser
module to get access to the document.We just make a basic observable change to see if it works.
Then we run:
./node_modules/.bin/fable-splitter test.fsx
And here's what it spits out:
test.js
export const el = document.querySelector("#site-description");
el.textContent = "Testeroo";
I reference the generated JS file from a script
tag (remembering to set type
to module
or else export
doesn't work) and boom, it works fine. Nice and clean and readable too. Let's do a bit more.
test.fsx
//...
console.log "Hello, World!"
Now we get this:
test.js
import { some } from "./fable-library.2.13.0/Option";
export const el = document.querySelector("#site-description");
el.textContent = "Testeroo";
console.log(some("Hello, World!"));
Because console.log
's first parameter is optional, it's modelled with a proper option type and some interop for JS to use it. log: ?message: obj * [<ParamArray>] optionalParams: obj[] -> unit
. Goodo. But what's this now?
Loading failed for the module with source “http://127.0.0.1:8080/js/fable-library.2.13.0/Option”.
The Fable compiler includes the bits of its library that your code needs, but it can't load it here because the generated import statement is missing ".js". Why?
No idea, but it's dealt with in the Fable compiler with string manipulation. Not very nice if you ask me.
This isn't the recommended way to use Fable anyway so I thought I'd see what it's like going the Webpack route. We're already pretty deep into this and the library code needed was quite large so I could do with a minifier.
Webpack
npm install -D webpack webpack-cli
./node_modules/.bin/webpack-cli init
webpack.config.js
var path = require("path");
module.exports = {
mode: "production",
entry: "./js/main.fsx",
devServer: {
contentBase: "./public",
port: 8080,
},
module: {
rules: [{
test: /\.fs(x|proj)?$/,
use: "fable-loader"
}]
}
}
./node_modules/.bin/webpack-cli -o bundle.js
And we get about 3.8kB of minified JS. Reference it in the script tag and it works perfectly. No longer human readable but I can fix that and even add source maps by modifying the Webpack config thusly:
var path = require("path");
module.exports = {
mode: "development",
entry: "./js/main.fsx",
devServer: {
contentBase: "./public",
port: 8080,
},
module: {
rules: [{
test: /\.fs(x|proj)?$/,
use: "fable-loader"
}]
},
devtool: "inline-source-map"
}
Summary
- 15 seconds to run
npm install
- 9 seconds to build
webpack-cli
node_modules
is- 116MB
- 530 dependencies according to
npm ls --parseable
I feel a bit dirty by throwing the minimalism out the window but it's reasonably small and fast and I get to use F# on the front end.