Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How we migrated a complex JavaScript application to ClojureScript step-by-step

How we migrated a complex JavaScript application to ClojureScript step-by-step

As software evolves along with the languages and frameworks it is built upon, it’s easy to create a monster. This is the tale of tackling complexity at the heart of Exoscale, and how a data-driven approach with ClojureScript enabled us to seamlessly replace a JavaScript app without user outage.

Alex King

April 06, 2019
Tweet

Other Decks in Programming

Transcript

  1. Alexander James King • github.com/alexanderjamesking • Clojure dev at Exoscale

    • Clojure(script) / Scala / Java / JS • Previous: JUXT / BBC / HMRC
  2. • European cloud provider • Data centres in Switzerland, Germany

    and Austria • CPU, GPU, Object Storage, Elastic IPs, DNS, Run Status • Data Security, GDPR Compliance, Data Privacy
  3. Problems • Velocity • Lack of tests • Tooling •

    Build tools • Dependencies • Speed of change
  4. –Robert Cecil Martin “The higher the quality, the faster you

    go. The only way to go fast is to go well.”
  5. Improving the Quality • Tests at all levels of the

    pyramid • Fewer dependencies • Small, focused libraries • Better tooling
  6. Greenfield vs Brownfield Build a Replacement Not very agile ∴

    High risk Moving target ∴ High risk Migrate the existing app Agile ∴ Low risk Complex ∴ Medium risk
  7. How can we migrate? URL: Server Page reloads (e.g nginx)

    URL: Browser HTML5 History API Component Lots of JS Interop 1 2 3
  8. Clojure Python :option-1 URL: Server JS CLJS + Clean codebase

    - Page reloads required - Multiple apps to manage
  9. Python :option-2 URL: Browser JS CLJS A2 Router A1 +Single

    app +No page reloads - Challenging Interop? History API
  10. <div class="ng-scope“ ng-controller=“NavController"> <div id=“alerts-wrapper”>…</div> <div class="col-nav primary”>…</div> <div class="internal-wrapper">

    <div class="col-nav secondary ng-scope”>…</div> <div class="col-content"> <div class="top-bar"></div> <div id="cljs-app"></div> <ng-view class=“ng-scope"></ng-view> </div> </div> </div>
  11. One URL at a time • Bootstrap both JS and

    CLJS apps • Routing: History API • Minimal JS <-> CLJS interop • Write UI tests against the JS app
  12. UI Tests • Selenium and Chrome Headless • Clojure mock

    of the API • Independent of implementation • Write tests against JS • Swap JS with CLJS
  13. Python API Initial Release Controller Router Data Instance IP IP

    Addresses View Model JS API CLJS API Poller
  14. Python API Additional Modules Controller Router Instances Data Instance Firewall

    IP View Model IP Addresses View Model JS API CLJS API Poller
  15. reagent-project / reagent • Define React components with data (Hiccup-like

    syntax) • reagent.core/atom for component state • reagent.core/render
  16. ;; basic component (defn title-bar [] [:div.title-bar [:h1.page-title.ellipsis "New Instance”]])

    ;; with local state (defn confirm-delete [callback] (let [confirming? (reagent/atom false) toggle #(swap! confirming? not)] (fn [delete-fn] (if @confirming? [:li [:button {:on-click callback} "Delete"] [:button {:on-click toggle} "Cancel"]] [:li [:button {:on-click toggle} "Delete"]]))))
  17. (ns portal.compute.ssh-keys.model) (rf/reg-event-db ::quick-filter (fn [db [_ value]] (assoc db

    ::quick-filter value))) (rf/reg-sub ::quick-filter (fn [db _] (not-empty (::quick-filter db)))) (ns portal.compute.ssh-keys.view) (defn quick-filter-names [] [:input {:on-change #(rf/dispatch [::model/quick-filter (.. % -target -value)]) :value @(rf/subscribe [::model/quick-filter]) :type "text"}])
  18. weavejester / integrant • Application structure (e.g. Component/Mount) • System

    as data • Why? • Dependency Injection • Multiple configurations
  19. (ns portal.system) {::router/init {:routes routes/app-routes :route-data route-data} ::notification/init {:callback js/window.jsAPI.displayNotification}

    ::http/init {:api-base-url api-base-url :csrf-token csrf-token} ::controller/init {:target-id target-id}} (ns portal.notification) (defmethod ig/init-key ::init [_ {:keys [callback]}] (rf/reg-fx ::info (partial display-notification callback “info")))
  20. (ns portal.routes) ;; CLJC (def app-routes ["" ["/u/:workspace" ["/compute" ["/ip"

    ["" :compute-ip] ["/:zone" :compute-ip-zone]] ["/instances" ["" :compute-instances] ["/add" :compute-instance-add] ["/:id" ["" :compute-instance] ["/move" :compute-instance-move] ["/scale" :compute-instance-scale]]]]]])
  21. ;; reitit route data (ns portal.system) ;; CLJS {:compute-instance {:view

    instance-view/view :mount ::instance-model/mount :unmount ::instance-model/unmount :query->ttl {::instance/list 10 ::volume/list 10 ::private-network/list 10 ::snapshot/list 10}}} ;; route-data at construction (reitit/router routes {:conflicts nil :expand (expand route-data)})
  22. – Alan J. Perlis “It is better to have 100

    functions operate on one data structure than 10 functions on 10 data structures.”
  23. ::namespaced/keywords • Namespace owns the data • Avoid deeply nested

    DB • Avoid circular dependencies • (::ip/list not :portal.data.ip/list) • Separate transient and persistent state
  24. ::namespaced/keywords in re-frame db cljs.user> (-> @re-frame.db/app-db keys sort) (:portal.compute.instance.model/editing-displayname?

    :portal.compute.instance.model/input-displayname :portal.data/last-fetch :portal.data/subscriptions :portal.data.account/account :portal.data.firewall/list :portal.data.instance/list :portal.data.private-network/list :portal.data.service-offering/list :portal.data.ssh-key/list :portal.data.volume/list :portal.data.zone/list :portal.router/route :portal.router/workspace)
  25. ::namespaced/keywords in re-frame event handler (ns portal.data.ip) (rf/reg-event-fx ::allocate-success (fn

    [_ _] {::notification/info "Successfully allocated IP” ::js-api/fetch [::list] ::data/fetch-n [[::list] [::instance/list]]}))
  26. ;; Java 101 public class Person { private String name;

    public String getName() { return name; } public void setName(String name) { this.name = name; } }
  27. (ns portal.router) (defn route-handler [db] ;; encapsulation (get-in db [::route

    :data :name])) (rf/reg-sub ::route-handler route-handler) (ns portal.compute.firewalling.model) (when (= :compute-firewalling (router/route-handler db)) ;; ...)
  28. Embrace JS Interop • Interop by URL alone is not

    enough • Outgoing: JS API • Incoming: CLJS API
  29. window.jsAPI = { fetchAccount: function() { reduxDispatch(fetch_account(false)); }, fetchInstances: function()

    { reduxDispatch(fetch_instances()); }, displayNotification: function(msg, level) { getModule('notify')({ message: msg, classes: `alert-${level}`, }); }, ... };
  30. (ns portal.cljs-api) (defn ^:export match-route [route] (rf/dispatch-sync [::router/match-route route])) (defn

    ^:export fetch-ips [] (rf/dispatch [::data/fetch [::ip/list]])) (defn ^:export fetch-instances [] (rf/dispatch [::data/fetch [::instance/list]])) ...
  31. ;; Calling JS from CLJS (js/window.jsAPI.fetchInstances) (js/window.jsAPI.displayNotification "info" "VM Created")

    ;; Calling CLJS from JS portal.cljs_api.fetch_instances(); portal.cljs_api.match_route(action.payload); ;; Converting data structures cljs.user> (clj->js {:hello "Dutch Clojure Days"}) #js {:hello "Dutch Clojure Days"} cljs.user> (js->clj #js {:hello "Dutch Clojure Days"} :keywordize-keys true)
  32. Model data as maps • Even lists! • Easier data

    access • Optimistic updates • get-in, assoc-in, update-in • keys, vals
  33. ;; vector cljs.user> (->> [{:id "a1" :ipaddress "1.2.3.4"} {:id "a2"

    :ipaddress "1.2.3.3"}] (filter #(= "a2" (:id %))) first :ipaddress) “1.2.3.3” ;; map cljs.user> (-> {"a1" {:id "a1" :ipaddress "1.2.3.4"} "a2" {:id "a2" :ipaddress "1.2.3.3"}} (get-in ["a2" :ipaddress])) “1.2.3.3"
  34. What went well? • UI Tests • One URL at

    a time • Data layer • JS Interop
  35. Things we can improve • More JS Interop • Smaller

    initial scope • Integration tests • Reusable components
  36. Python API Initial Release Controller Router Data Instance IP IP

    Addresses View Model JS API CLJS API Poller