Статьи

Объединение ClojureScript и JS Frameworks — Выпуск Knockout

Некоторое время назад я начал играть с ClojureScript и пытался заставить его работать с популярными фреймворками. Я играл с некоторыми из них, совсем недавно с Knockout.js. Этот пост подводит итог этих усилий и моего не очень оптимистичного взгляда на ClojureScript.

  • Я попробовал «голый» jQuery . Это было довольно гладко.
  • Я пробовал Backbone.js . Я заставил его работать на простом примере, хотя один читатель в Twitter справедливо заметил, что ClojureScript был отвратительным. Да, этот пример Backbone отвратителен. Позже я попытался сделать что-то менее тривиальное. В конце концов я в ужасе убежал, из-за несоответствия импеданса между сильно OO Backbone и не-OO ClojureScript под влиянием моего невежества в CLJS (и Backbone).
  • Я также дал Angular.js шанс. Это началось действительно гладко, потому что Angular гордо заявляет, что он не столько полагается на объектно-ориентированное программирование. Это было здорово. В тот самый момент, когда я начал спорить с компилятором, переименовавшим мои переменные, вскоре последовало открытие, что Angular и Closure не нужны

Итак, пришло время для другого эксперимента — на этот раз Knockout.js. Я следовал официальному учебнику и вот что я в итоге придумал.

Страница

Полная страница в Hiccup выглядит следующим образом. Ничего особенно захватывающего здесь.

(defn render-body []
  (hp/html5
     [:head]
     [:body
 
      [:p "First name: " [:strong {:data-bind "text: firstName"} "todo"]]
      [:p "Last name: " [:strong {:data-bind "text: lastName"} "todo"]]
      [:p "Full name: " [:strong {:data-bind "text: fullName"} "todo"]]
 
      [:p "First name: " [:input {:data-bind "value: firstName"}]]
      [:p "Last name: " [:input {:data-bind "value: lastName"}]]
 
      [:button {:data-bind "click: capitalizeLastName"} "Go caps"]
 
      (hp/include-js
        "//ajax.aspnetcdn.com/ajax/knockout/knockout-2.1.0.js"
        "js/cljs.js")
      (hp/include-css "css/todo.css")
      ]))

ClojureScript

Самая интересная часть — это код ClojureScript. Вот один из способов сделать это:

(ns hello-clojurescript)
 
(defn app-view-model []
  (this-as this
           (set! (.-firstName this) (.observable js/ko "Bert"))
           (set! (.-lastName this) (.observable js/ko "Bertington"))
           (set!
             (.-fullName this)
             (.computed js/ko
               (fn []
                 (str (.firstName this) " " (.lastName this))) this))
           (set!
             (.-capitalizeLastName this)
             (fn []
               (.lastName this (-> this .lastName .toUpperCase)))))
  nil
  )
 
(.applyBindings js/ko (app-view-model.))

Да, мне нужно явно вернуть «ноль» там. В противном случае он возвращается this.fullName = ..., и это нарушает нокаут.

Это работает, но трудно защитить его по сравнению с эквивалентом JS:

function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");
    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
    this.capitalizeLastName = function() {
        var currentVal = this.lastName();
        this.lastName(currentVal.toUpperCase());
    };
 
}
 
ko.applyBindings(new AppViewModel());

Полный код можно найти в моем репозитории GitHub .

Лучший путь — Макро

Этот код можно сделать намного лучше с помощью пользовательского макроса, который представлен в StackOverflow :

(defvar name_model
    first_name (observable "My")
    last_name (observable "Name")
    name (computed (fn [] (str (. this first_name) " " (. this last_name)))))
 
(. js/ko (applyBindings name_model));

Теперь это будет что-то!

… За исключением того, что определение макросов в ClojureScript сложнее, чем в простом Clojure, до такой степени, что я еще не заставил его работать.

Выводы по ClojureScript

Я потратил довольно много часов на изучение ClojureScript, и у меня смешанные чувства.

  1. Довольно сложно заставить ClojureScript работать с существующими JS-фреймворками, в основном потому, что использование объектов в CLJS требует много церемоний.
  2. В отличие от простого Clojure, «веселье» и «продуктивность» — это не те слова, которые приходят мне на ум, когда я думаю о своих приключениях в CLJS. «Разочарование» и «запугивание» гораздо более уместны. Я постоянно спорю с компилятором и пытаюсь победить его, чтобы делать правильные вещи, не получая удовольствия от решения проблем.
  3. Некоторые вещи могут быть покрыты макросами, но в целом это выглядит очень … жестким и сдержанным. Я чувствую, что время от времени я вынужден попадать в другой трудный угол, тратить на него слишком много времени, писать другой макрос и так далее. Все это только для того, чтобы устранить пробелы и сделать ClojureScript похожим на… JavaScript. Фактически это похоже на написание собственного слоя макросов для компиляции JS-like-DSL в ClojureScript .
  4. Возможно, есть лучший способ, и я просто делаю что-то не так. Может быть, вы вообще не должны использовать эти фреймворки, но катите свои или используйте те немногие, которые написаны на ClojureScript?
  5. Возможно, все это имеет мало смысла в таком маленьком масштабе, и вам нужно что-то действительно большое, чтобы оценить ClojureScript. Вам просто нужно потратить много часов на прохождение кривой обучения и макроса своего выхода. Может быть, тогда она станет более продуктивной, модульной и еще много чего. Я не знаю, на самом деле у меня слишком мало опыта в самом JavaScript, чтобы отвечать на такие вопросы. Я не очень оптимистично по этому поводу, хотя.