Transcript of Firebase screencast.
CoderX here.
Firebase is a hosting service that provides an authenticated realtime database. The database acts like a JSON tree that pushes changes out to clients. Clients can write to and subscribe to parts of the object. This subscription model fits nicely with reactive style pages.
Reacting to changes with Firebase, and how to interop
In this screencast we will build a website that allows users to login, subscribe to public data, and write updates. We will
Write to and read from Firebase
Listen to change notifications
Add user authentication
Build a UI to make use of the data
Deploy the project
Examine the impact of optimized compilation
Option 1: Deploy with whitespace optimization
Options 2: Use or provide externs
Option 3: Use CLJSJS as a blueprint
Review JavaScript interop
Write to and read from Firebase
Let’s start a new project. Firebase provides a JavaScript API (https://firebase.google.com/docs/web/setup).
Let’s load it in our index.html page under resources.
script src="https://www.gstatic.com/firebasejs/3.1.0/firebase.js"
Sign up for Firebase using your Google account. Click on console then create a project (https://console.firebase.google.com/). We need to use these API keys in our code. Let’s start a firebase namespace, and put some initialization code in it.
(defn init []
(js/firebase.initializeApp
#js {:apiKey "USE_YOUR_KEY"
:authDomain "YOUR_PROJECT.firebaseapp.com"
:databaseURL "https://YOUR_PROJECT.firebaseio.com"
:storageBucket "YOUR_PROJECT.appspot.com"}))
js/ is a special form to allow you to access root objects in JavaScript. The firebase object is defined by the api we included earlier, and contains an initializeApp function.
#js specifies a JavaScript object literal.
Add a database to the project. Modify the database access rules to allow anonymous read on everything, and write to test.
{
"rules": {
".read": true,
"test": {
".write": true
}
}
}
In our code, we will write a value to a key.
(defn db-ref [path]
(.ref (js/firebase.database) (string/join "/" path)))
(defn save [path value]
(.set (db-ref path) value))
(save ["test"] "hello world"))
A ref represents a specific location in your database and can be used for reading or writing data. You can reference the root, or a child location in your database by passing a path. Writing is done with the set method. We can check that the value was saved in the database from the Firebase console. We can now read the value using once.
(.once
(db-ref path)
"value"
(fn received-db [snapshot]
(prn "Got:" (.val snapshot))))
Great, we can read and write data.
Listen to change notifications
The neat thing about Firebase is that it will notify clients as the database changes. The Firebase real-time database is a cloud-hosted database. Data is synchronized to every connected client.
Listening can be done with the on method, which is just like once except that it continues to call back to us whenever the source data changes. In order to make use of the new data in our application we want to watch for changes. Ratoms are convenient ways for the UI to respond to change. We can reset a ratom when new data arrives. Here we meet a common interop issue.
Components get added and removed from the DOM. We don’t want to have multiple subscriptions pile up. So we need a way to disconnect a listener when the component is removed. This is a lifecycle concern; the component needs to clean up a resource. Reagent’s third form is the appropriate solution for lifecycle concerns. We create a class that has an unmount handler that detaches our listener.
(defn on [path f]
(let [ref (db-ref path)
a (reagent/atom nil)]
(.on ref "value" (fn [x]
(reset! a (.val x))))
(reagent/create-class
{:display-name "listener"
:component-will-unmount
(fn will-unmount-listener [this]
(.off ref))
:reagent-render
(fn render-listener [args]
(into [f a] args))})))
This is a component that takes a path and a component as an argument. The component passed should expect a ratom to observe as the first argument. Once we have defined on we can observe values very concisely.
[on ["test"] (fn [a] [:div @a])]
While such a component is mounted in the DOM it will respond to any changes to the specified path into the database by reacting to a ratom bound to the values received. When a user changes a value, the other user sees the new data. We didn’t need to write any explicit communication code. The changes flow down as they happen.
Pretty cool.
Add user authentication
In the Firebase console select authentication providers and enable Google. To check the auth status we hook up to onAuthStatusChanged.
(.onAuthStateChanged
(js/firebase.auth)
(fn auth-state-changed [user-obj]
(reset! user {:photoURL (.-photoURL user-obj)
:displayName (.-displayName user-obj)
:uid (.-uid user-obj)}))
(fn auth-error [error]
(js/console.log error)))
When the user is logged in, we’ll show their profile picture as a button to logout. Let’s make a login button that calls loginWithPopup.
(defn login-view []
[:div
{:style {:float "right"}}
(if-let [{:keys [photoURL displayName]} @firebase/user]
[:span
[:button.mdl-button.mdl-js-button.mdl-button--fab.mdl-button--colored
{:on-click
(fn logout-click [e]
(firebase/logout))
:title displayName
:style {:background-image (str "url(" photoURL ")")
:background-size "cover"
:background-repeat "no-repeat"}}]]
[:button.mdl-button.mdl-js-button.mdl-button--raised.mdl-button--colored
{:on-click
(fn login-click [e]
(firebase/sign-in-with-popup))}
"Login with Google"])])
Let’s try logging in… it works!
Now modify the database access rules to require user authentication.
{
"rules": {
".read": true,
"test": {
".write": true
},
"users": {
"$uid": {
".write": "$uid === auth.uid"
}
}
}
}
Writing to the users uid path now requires the writer to be authenticated.
We are limited to writing JSON values. But we can encode more complex data models. For this project we will be using Datascript as the model. Save the model using pr-str and load it with edn/read-string.
(pr-str @conn)
(edn/read-string {:readers d/data-readers} in)
Build a UI to make use of the data
Users will add short titles of topics that interest them. I’ll render the titles as nodes in a graph.
The nodes can be linked together. Users can put their faces on nodes to vote for them.
Deploy the project
The Firebase command line tool deploys our application. It is a NodeJS package that you can install by typing `npm install firebase`. Execute `firebase init` in your project directory to create configuration files. Database rules are specified in the database.rules.json file. The firebase.json file specifies a folder of files to copy for hosting. https://firebase.google.com/docs/hosting/deploying
Make a build task that creates a minified build to a `public` folder that contains our HTML and compiled JavaScript. Check that everything is working with `firebase serve`, which serves the public folder locally. To push the folder to the host, type `firebase deploy`. I put the release build and deployment step in a script and readme so I don’t forget it.
Our site is online, open to the public, and supports real-time collaboration.
Examine the impact of optimized compilation
While developing it is convenient to use no optimizations because compilation is far faster. The main disadvantage of no optimizations is that the output goes into many separate files. When deploying, it is more convenient to have a single JavaScript output file.
Advance mode compilation does aggressive renaming, dead code removal, and global inlining, resulting in highly compressed JavaScript. This power comes with some restrictions. Advanced compilation mode makes assumptions about the code that it optimizes, and breaking these assumptions will result in errors. This is a problem for interop with JavaScript libraries. JavaScript libraries must be accompanied by extern definitions, or the aggressive renaming will cause interop calls to be undefined. https://developers.google.com/closure/compiler/docs/api-tutorial3
Option 1: Deploy with whitespace optimization
I encourage you to avoid worrying about the effects of advanced optimization on interop by not using it at all. Configure your deployment build to use optimizations whitespace instead of advanced. This will produce a single output file suitable for deployment. The only downside is that your js file will be larger than it could otherwise be (for this project the output is 1.7Mb). For most purposes this is not noticeable.
:compiler {:output-to "resources/public/js/compiled/firefire.js"
:main firefire.core
:optimizations :whitespace}
Options 2: Use or provide externs
JavaScript functions and variables get aggressively renamed, causing interop to fail.
Extern definitions prevent renaming, so that interop will work correctly. For advanced compilation we need both the JavaScript source file and an externs file. An externs file is a JavaScript file that declares all the public interfaces.
firebase.initializeApp = function(options, name) {};
Externs files can also contain documentation annotation. Many libraries get distributed with externs. If you use npm to retrieve a library, check for an externs directory. If externs are not provided, you may be able to generate them using a tool like Michael’s (http://jmmk.github.io/javascript-externs-generator/).
Pro tip, you can also use the original JavaScript as it’s own externs file. You can turn off the warnings it will produce with configuration specified in David’s blog post here:
Declare foreign-libs is done by specifying an edn file like so:
{:foreign-libs
[{:file "react/react.js"
:file-min "react/react.min.js"
:provides ["com.facebook.React"]}]
:externs ["react/externs.js"]}
[{:file "react/react.js"
:file-min "react/react.min.js"
:provides ["com.facebook.React"]}]
:externs ["react/externs.js"]}
You can specify a preamble for your build instead, which will prepend a JavaScript file to your final compiled output. But as we just saw, specifying a foreign lib is not much more work. In either case the important part is the externs.
In practise I recommend avoiding this option entirely in favor of using CLJSJS.
Option 3: Use CLJSJS as a blueprint
CLJSJS is a standard way to package the foreign-libs definitions file with the externs and the JavaScript library so that you can use them as a dependency.
There is already a package defined for Firebase. To use it we just add it to our dependencies, and require it in our code. (Don’t forget to remove the include from your HTML file).
But what if we need a newer version? Let’s take a closer look at the definitions for the Firebase package. https://github.com/cljsjs/packages/blob/master/firebase/build.boot
The javascript and externs come from a NPM distribution, which is referenced by a version number. At this point it should be clear that all there is to a CLJSJS package is specifying the location of the JavaScript, the externs, and the namespace to be used when requiring.
To package a newer version we can checkout this code, bump the version, and install it locally with `boot package install target`.
If the library we want to use doesn’t have a CLJSJS package, we can copy the definitions from another package. For example if the library we want to use is distributed on NPM, we could copy the Firebase package as a starting point for how to wrap it. CLJSJS provides a well documented blueprint for how to wrap JavaScript libraries to be compatible with advanced compilation.
Review JavaScript interop
Interop with JavaScript from ClojureScript is similar to interop with Java from Clojure.
You call native methods using the dot or dot dot form.
(.method object arg1 arg2)
(. object method arg1 arg2)
(.. object method1 (method2 arg1 arg2))
JavaScript has a global root object. To access the global root in ClojureScript, use the special js prefix.
js/window
(.setEventListener js/window (fn [e] (prn “hi”)))
(js/window.setEventListener (fn ...)))
To access properties, ClojureScript differs from Clojure slightly in that you must use a dash to indicate property access. JavaScript does not distinguish properties from methods, so the dash is required to explicitly designate property access.
(.-property object)
(. object -property)
(.. object -property1 -property2)
(set! (.-property object) value)
Array access is done with aget and aset.
(aget array index)
(aget array index2 index2 index3)
(aset array index value)
You can chain access forms together.
(set! (.-foo (aget array index1)) value)
Object constructors are the same as Clojure. You can use constructors from the global JavaScript root.
(js/THREE.Vector3. 0 0 0)
(new js/THREE.Vector3 0 0 0)
Modules can be referenced and imported from the Closure Library.
(ns example
(:import [goog.event KeyCode]))
JavaScript objects can be created with js-obj or by using the special #js reader literal. These forms are not recursive. When nesting objects specify the children as literals.
(js-obj "key" "value")
#js {:key "value"}
I recommend using ClojureScript data literals as much as possible, and only using JavaScript objects for interop with third party libraries.
There are transformation functions to convert from ClojureScript data structures to JavaScript objects. These are both recursive.
(clj->js)
(js->clj)
You can optionally keywordize-keys but keep in mind that not all strings can be keywords.
There is a penultimate escape hatch form js* that takes raw JavaScript.
(js* "some(javascript);")
JavaScript allows anything to be thrown, so the ClojureScript catch form has a special term default which will match anything. Use this to catch unexpected values being thrown.
(try
(/ :1 2)
(catch :default ex))
You can annotate your functions with export to prevent them being name-mangled during advanced compilation. They will then be easily consumable from JavaScript.
(defn ^:export f []
(+ 1 2))
Conclusion
Firebase is a convenient hosting service you can leverage from ClojureScript. The free starter plan provides a database, hosting, and authentication. ClojureScript allows you to work with JavaScript APIs with interop code. For development you can include a JavaScript file from your HTML page. For deployment I recommend using whitespace optimization to avoid the build complications that advanced compilation introduces. When your project is mature and would benefit from shrinking with advanced mode compilation, you will need externs defined. CLJSJS is the best way to specify externs, and provides wrappers for many libraries already.
Until next time,
Keep coding.