4

【译】Roam Research 自定义组件 —— 跟 {{roam/render}} 来一次亲密接触!

 2 years ago
source link: https://blog.jimmylv.info/2021-03-10-a-closer-look-at-roamrender-zh-translation/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【译】Roam Research 自定义组件 —— 跟 {{roam/render}} 来一次亲密接触!

2021年03月10日

Roam Research 采用的是 Clojure 技术栈的 Datomic/datascript Datalog 数据库,能够将内容同步到不同的设备,并管理非常复杂的撤销操作,还能够支持各种程度的自定义组件和插件功能定制,方便开发者利用 Reagent 渲染组件,并支持与 JavaScript 互操作。本文就将硬核解析 Roam 背后原理,发掘 Roam 基于 Block 的深层技术优势,帮助你迎接 Roam API 时代的到来!

原文地址:A closer look at {roam/render} —— Zsolt Viczián


Swiss Army Knife

Roam 就好像一把优秀的瑞士军刀,竟然包含一个完整的 ClojureScript 开发环境。在过去两周里,我逐渐熟悉了 ` {{roam/render}} `。这篇文章可以算作我自己的笔记,总结整理一下我的所见所学。

这篇文章是针对开发者和 Roam 黑客的。如果你还不属于这些阵营,你很可能会对这些内容感到吃力。

我并不是一个 Web 开发者,React、Clojure、Reagent 和 Datalog 对我来说完全是崭新的事物。我是一个典型的无知者无畏的终端用户,我知道如何构建一个基于宏的庞然大物,但却无法保持其健壮/可测试/可维护。在玩roam/render的过程中,有一次我几乎把我的整个 roam 数据库都毁掉了 —— 这与roam/render无关,一切都是因为我太笨了。请谨慎尝试我的结论和例子,欢迎在评论中分享更好的解决方案,或者联系我进行勘误。

什么是 roam/render?

` {{roam/render}} `是Roam中用于构建自定义组件的原生特性。这些组件可以是简单的表格、计算器、滑块,也可以是复杂的交互式工具,比如数据透视表、图表、流程图等。

roam/render 也是一个使用 Reagent 在 ClojureScript 中实现的 React 组件。你可以使用 Datalog 在 Roam 的 Datomic Graph 中访问你的数据。

其实就在几周前,以上两句话中的每一个单词对我来说都是陌生的。让我们先来快速看看每个词的意思:

  • React是一个用于构建用户界面的 JavaScript 库。*
  • Clojure(/ˈkloʊʒər/, 很像 Closure 闭包的拼写)是一种支持动态类型和函数式编程的 Lisp 方言。和其他 Lisp 方言一样,Clojure 将代码视为数据,并有一个 Lisp 宏系统。*
  • ClojureScript是一个针对 JavaScript 的Clojure编译器。*
  • Reagent是一个 React 的 ClojureScript 封装。它可以帮助你轻松创建 React 组件。Reagent 有三个主要的功能,使其易于使用:使用函数创建React 组件,使用Hiccup 生成 HTML,以及在Reagent Atoms中存储状态。Reagent 可以让你写出简洁可读的 React 代码。*
  • Hiccup是一个用于呈现 HTML 的 Clojure 库。它使用 vectors 来表示元素,使用 maps 来表示元素的属性。* 试着玩一玩HTML2Hiccup,可以帮助你更好地了解 hiccup 的工作原理。另外,你也可以直接在 Roam 中输入 hiccup,只要在一个空块中输入:hiccup [:code "Hellow World!"],看看会发生什么。
  • Atoms是 Clojure 中的一种数据类型,它提供了一种管理共享、同步、独立状态的方法。一个 Atom 就像其他编程语言中的任何引用类型一样。Atom 的主要用途是保存 Clojure 的不可变数据结构。 *
  • Datomic是一种新型数据库。Datomic 不是将数据收集到表格和字段中,而是由Datoms [entity-id attribute value transaction-id] 建立。这种架构为改变和扩展数据库 Schema 提供了高度的灵活性,而不会影响现有的代码。*
  • Datalog是一种基于 Prolog 的声明式、基于形式逻辑的查询语言。*我分享了一些简单的 Datalog 实例,在这里查看如何生成 Roam Graph 有关的基本统计数据。

开启自定义组件

自定义组件功能默认是禁用的。在你搞事情之前,你必须在用户设置中启用它。

enable custom components in user settings

Hello World!

你可以通过在 Block 中添加以下代码来嵌入 roam/render: ` {{roam/render: ((block-ref))}} ` 其中 block-ref 指的是你的脚本代码的块引用。脚本代码必须至少有一个函数。如果这个 Block 有多个函数,那么在组件创建时只会调用最后一个函数。

被引用的代码块必须包含一个设置为 Clojure 类型的代码块。代码块的创建是通过使用三个反引号 ` ``` ` 来完成的。如果你不知道键盘上的反引号键在哪里,你也可以使用/菜单,然后选择 Clojure Code Block。

Hello World Roam Render Demo

一步一步来

  1. 在一个新的 Block 中输入: {{roam/render:((...))}}
  2. 当你开始在双括号之间打字时,Roam 会弹出 “search for blocks”。选择 “Create as block below”。这将在你当前 Block 下面创建一个 Block,并自动在括号之间放置一个块引用。
  3. 导航到新的 Block,并添加两个额外的反引号``来创建一个代码块。
  4. 将代码的语言设置为 Clojure。
  5. 最好给每个组件都有自己的命名空间(ns myns.1),虽然 Hello World 没有它也能工作。这将为你在以后的探索省去很多头疼的问题,因为当同名的函数相互竞争时,调试会让人非常头大。我遵循一个简单的模式,按顺序给我的测试命名空间编号。当我准备好了一个组件,我才会给它自己合适的命名空间。
  6. 代码块应该至少有一个函数:(defn hello-world [])
  7. [:div"..."] hiccup 这种写法等价于 <div>Hello World</div>
(ns blogpost.1)
(defn hello-world []
  [:div "Hello World"])

恭喜你!你已经使用 ClojureScript 成功创建了你的第一个 Roam 组件!现在你应该看到 Hello World 出现在代码上方的 Block 中。

接收 input 参数

当你开始为实际用例构建组件的时候,你很快就会意识到,如何知道当前组件正在运行的 Block 的 ID 呢?你需要这个 ID 来创建能够感知上下文的组件,即它们需要知道自己选择在哪个 Page 上,上级或上级有哪些 Block 等等。

我花了好几天的时间才发现如何做到这一点。我想让你省点力气! 当一个组件被调用时,Roam 会把 block-id 作为第 0 个参数传递给主函数(注意:你代码块中的最后一个函数)。当然,Roam 还会传递当你调用组件时,所输入的其他任何参数。

在新的 Block 输入下面的脚本,将产生以下输出。(当然,请注意,当你在自己的 Graph 中尝试这个脚本时,Block 的 ID ((Vy8uEQJiL))会有所不同,所以对应的 block-uid “eR7tRno7B “也会不同。)

{ { roam/render: ((Vy8uEQJiL)) 10 "input 1" ["input" "vector" "with" 5 "elements"] {:key1 "this is a map" "key2" "value 2" :key3 15} (1 2 3) #{"a" "b" "c"} } }
datatypes / arguments example

这里的内容很多。让我们借此机会学习一下 Clojure 数据结构。我正在向自定义组件传递六个参数。除了这六个参数之外,Roam 默认将 block-uid 作为第一个参数传递。如果你想深入了解 Clojure 数据结构,我推荐这篇文章

  • 第 0 个参数是block-uid。它是作为一个单一元素的 map 传递的。
  • 接下来,我添加到组件的第一个参数是一个 integer 整数。我可以简单地把它作为一个数字传递给组件。
  • 然后是一个 string 字符串。Clojure 只接受双 “引号” 标记的字符串。单个 ‘引号’(单引号)有不同的含义。使用'会产生一个不被计算的形态,下文还会提到如何在 datalog 查询中使用单引号。
  • 第三个输入参数是一个 vector。请注意,在 Clojure 中,你可以用空格隔开一个 vector 的多个元素。这个 vector 总共有四个元素,三个字符串,一个整数。
  • 接下来是一个有三个 keys-values 的 map。你可以使用 “string” 作为 key,就像在 JavaScript 对象中一样,然而,Clojure 还提供了使用:keywords 作为 key 的方式。关键词以 : 冒号开头。你可以使用 , 逗号来分隔 key-value 键值对,但这不是必须的。注意在输入中我没有使用逗号。
  • 第五个参数(Arg 4)是一个 list 列表。列表的主要用途是表示未被计算的代码,在你进行元编程时,用来编写生成或操作其他代码的代码。
  • 最后一个参数是一个 set 集合。set 和 vector 很像,关键的区别是 set 中的每个值都是唯一的。同样根据设计,set 中每一项的顺序是任意的。

代码本身是自解释的。你应该注意到我是如何使用[:b],相当于<b>...</b> 用来表示粗体文本。clojure.core/map-indexed将把输入向量 args 中的每个元素传给匿名函数fn[i n],其中i是索引号,n是正在处理的 vector 的当前元素。

(ns blogpost.2)
(defn main [ & args]
  [:div
   [:b "Number of arguments received: "] (count args)
   (map-indexed (fn[i n] [:div [:b "Arg " i ": "] (str n) ]) args)])

现在我们知道第 0 个参数总是带有 block-uid 的 map,我们可以稍微修改一下我们的代码,将 block-uid 接收到一个专用变量中。这是我在所有组件中使用的 “standard” 主函数声明。 (defn main [{:keys [block-uid]} & args]。将前面的例子用标准 main 函数稍微改写一下,就会变成这样。

(ns blogpost.2)
(defn main [{:keys [block-uid]} & args]
  [:div
   [:b "The block-uid is: " ] (str block-uid) [:br]
   [:b "Number of arguments received: "] (count args)
   (map-indexed (fn[i n] [:div [:b "Arg " i ": "] (str n) ]) args)])

一个简单的 Reagent 表单

Reagent 是表单交互的关键。它们允许你根据用户输入的数据,动态地改变组件的 HTML 渲染,无论是组件本身还是在 Graph 中的 Page 页面。

为了理解下一个例子,你首先需要了解 Clojure 变量。Clojure 中的所有变量都是不可变的,这意味着它们是作为常量创建的。变量可以有不同的作用域,也就是说,你可以在命名空间的层次上定义变量,或者只是在一个函数中定义变量。但是一旦设置,它们的值就不能改变。

有几个方法可以绕过这种不可变性。其中之一是通过使用 atoms。 atom 的工作原理我就不深究了(顺便说一句,就算我想解释也解释不清楚),Atom 就是对一个值的引用。使用 swap! reset!可以把这个引用改成一个新的值。在clojure.core/swap!中,你可以在给 Atom 分配新的值之前访问它的前一个值,在clojure.core/reset!中,你只需给 Atom 分配一个新的值,而不用去管它的前一个值。

roam/render中,我使用reagent.core/atom而不是clojure.core/atom。Reagent atoms 与普通 atoms 完全一样,只是它们会跟踪derefs(即访问它们的值)。Reagent 组件如果 derefs(解引用)发现其中的值发生变化,就会自动重新渲染。在实践中,这就意味着你可以根据数据的变化动态地改变组件。

我们将构建一个简单的表单,只包含一个输入字段。当你在 Graph 中输入一个页面的名称时,这个表单就会返回该页面的 UID。例如,你可以用这个来创建一个直接超链接到你的页面,URL 格式为 https://roamresearch.com/#/app/YOUR-GRAPH/page/9char-UID

(ns blogpost.3
  (:require
   [reagent.core :as r]
   [roam.datascript :as rd]))

(defn query-list [x]
  (rd/q '[:find ?uid .
          :in $ ?title
          :where [?e :node/title ?title]
                 [?e :block/uid  ?uid]]
        x ))

(defn main []
  (let [x (r/atom "TODO")]
    (fn []
     [:div
     [:input {:value @x
      :on-change (fn [e] (reset! x (.. e -target -value)))}]
     [:br]
     (query-list @x)])))

有几件事值得解释一下:

  • 请注意命名空间声明下的:require块。这些是我们代码中引用的命名空间。我稍后会分享一个简单的组件,你可以用它来探索 Roam 中所有可用的命名空间。使用:as你可以指定一个别名来引用你代码中的命名空间。
  • query-list函数中,我们使用 roam.datascript(rd) 命名空间中的q函数执行一个简单的数据记录 query。请注意查询'[:find ?uid .开头的单引号 '。另外,请注意?uid后面的点 .。这个点 . 将查询的结果转换为一个标量。我在这里使用它,是因为我想要找一个单一的 uid 值作为函数返回值。
  • 依然是 query-list 函数,请注意我们没有在函数的结尾有一个类似于 JavaScript 的 return的语句。在 Clojure 中,函数中最后一次调用的结果总是被返回。由于query-list只包含一次调用,所以在执行 query 时,将直接返回rd/q的结果。
  • 请注意,在main函数中,x 被定义为一个初始值为 “TODO” 的 Reagent atom。将声明放在(fn[]后面的匿名函数之外,可以确保在创建组件时,只会设置一次 atom,而不是在每一次往 INPUT 框输入任何文本时,都将其重置为默认值。

以上是由Conor的一个例子改编的。你可以在 Roam help 数据库 这里 找到 Conor 的版本。我的解决方案和 Conor 的解决方案之间的一个关键区别是,他使用的是roam.datascript.reactive,而不是仅仅使用roam.datascript。在这个具体的例子中,从我的理解来看它们是没有区别。如果我的理解是正确的,Datascript reactive 提供了一种创建查询的方法,当其结果集发生变化时,它可以自动识别。它们可以用于创建交互式组件,比如 {{table}}

如何存储组件的属性值

当你重新打开页面或关闭并重新打开 Roam 时,组件会被重新初始化。在大多数情况下,你会希望以某种方式存储组件的属性,这样当你下次打开页面时,你会发现组件处于你离开时的状态。

你可以选择将信息写入 Block 中,并决定写入哪一个 Block。你可以写到嵌套在组件下的 Block ,或者写到一个工具页面上的 Block 里面,例如[[my component/data]] 或者更新执行组件的那个 Block。最后一种选择涉及更新 {{roam/render: ((block-UID)) }} 与 input 参数,这与我们在前面的例子中打印 input 参数的方式有些类似。我将在一个非常简单的例子中演示如何做到这一点。

顺便说一下,我用datascript.core做了一个实验,将自定义实体写入到 Roam 数据库。我也能够执行查询,但是没有找到一种方法让 Roam 保存更改。手动编辑自定义实体到 EDN 文件中是可行的(演示),所以使用datascript添加自定义实体应该也是可行的。

(ns blogpost.4
  (:require
   [roam.datascript :as rd]
   [roam.block :as block]
   [clojure.string :as str]))

(defn save [block-uid & args]
  (let [code-ref (->> (rd/q '[:find ?string .
                              :in $ ?uid
                              :where [?b :block/uid ?uid]
                         	     [?b :block/string ?string]]
                            block-uid)
                      (str)
                      (re-find #"\({2}.{9}\){2}"))
        args-str (str/join " " args)
        render-string (str/join [""])]
    (block/update
      {:block {:uid block-uid
               :string render-string}})))

(defn main [{:keys [block-uid]} & args]
  (let [some-value [1 2 "string" {:map "value"}]]
    [:div [:strong "args: "] (str/join " " args) [:br]
     [:button
      {:draggable true
       :on-click (fn [e] (save block-uid some-value))}
      "Save"]]))

这个组件将保存 some-value 的值。在这个例子中,为了简单起见,它是硬编码的,但当然,你可以构建任何你想要的数据结构来代替 some-value。请注意以下几点:

  • 我的 save 函数是在 main 函数中按钮的:on-click事件中调用的。在我的实验中,每次组件改变其值时自动调用 save 的效果并不好,因为每次覆盖 {{roam/render: ((block-UID))}} 的时候,组件都会重新初始化,使得无法填写表单,或者通过交互的方式使用组件。
  • 我在 save 函数中定义了三个变量。code-refarg-strrender-string
  • code-ref将保存 block-string 的当前值,因为我在执行 datalog 查询的时候,会读取:block/string属性的当前值,并通过block-uid过滤。
  • ->>是一个函数,它通过一组 (str)(re-find) 的形式来执行表达式。它的唯一目的是让代码更易读。
  • re-find中的正则表达式会找到 {{roam/render: 后面的((block-UID))
  • 一旦render-string准备好了,我就调用roamAlphaAPIblock/update 将 Block 更新为我的 string 文本。

下面是这段代码的运行情况:

Saving state

似乎还有一种方法,可以使用 Reagent 在 Form-3 Components 中所提供的:component-will-unmount事件处理器,来触发保存组件。虽然我还没有尝试过这种方法,但根据文档,这应该提供了一种方法来存储组件的属性,当你导航到不同的页面时,它就会从当前视图中消失。如果你对此感兴趣,你可以阅读这里

如何与 Javascript 互操作

ClojureScript 提供了一种调用 JavaScript 函数的简单方法。当你想访问 DOM document 属性或函数时,这就非常实用了。比如调用 roam42 函数或创建一个 JavaScript 钩子来处理roam/render组件的数据(可以节省你学习 Clojure 的时间)。

第一个例子是返回页面的 location 位置。

(ns blogpost.5)
(defn main []
  [:div (. (. js/document -location) -href)])

请注意 JavaScript DOM document 是如何使用js/document访问的。

属性是用-property符号来访问的。用于访问对象属性的 JavaScript . 点符号,需要转化为嵌套的小括号,每个括号都调用下一个属性或函数。

使用->可以使代码更易读,特别是在属性链特别长的时候。

(ns blogpost.6)
(defn main []
  [:div (-> js/document
          (.-location)
          (.-href))])

现在,我们将再做一遍上面的 Reagent 表单简单示例 (blogpost.3),但是将(defn query-list [x]替换为 JavaScript 来执行我们的 datalog 查询。

javascript interoperability

下面是 ClojureScript 的代码。

(ns blogpost.7
  (:require
   [reagent.core :as r]))

(defn main []
  (let [x (r/atom "TODO")]
    (fn []
     [:div
     [:input {:value @x
       :on-change (fn [e] (reset! x (.. e -target -value)))}]
      [:br]
      (.myDemoFunction js/window @x)])))

以及 ` {{roam/js}} ` 的代码。

window['myDemoFunction'] = function (x) {
  return roamAlphaAPI.q(`[:find ?uid .
                          :in $ ?title
                          :where [?e :node/title ?title]
                                 [?e :block/uid ?uid]]`, x);
}

如果你想把多个变量传给 JavaScript,你可以在函数调用(.myFunction js/window variable-1, @an-atom, block-uid)之后简单地列出这些变量。

Roam 已有的命名空间

roam/render 中有以下命名空间。

  • Clojure: cljs.pprint, clojure.core, clojure.edn, clojure.pprint, clojure.repl, clojure.set, clojure.string, clojure.template, clojure.walk。
  • Roam: roam.block, roam.datascript, roam.datascript.reactive, roam.page, roam.right-sidebar, roam.user, roam.util。
  • 其他: datascript.core、reagent.core。

这段代码将列出每个命名空间中所有可用的函数。

(ns blogpost.8)

(defn main []
  [:div (map (fn [x] [:div
              [:strong (pr-str x)]
              [:div (map
                     (fn [n] [:div (pr-str n)])
                     (->> (ns-publics x)
                          (seq)
                          (sort)))]])
          (into [] (->> (all-ns)(map ns-name)(sort))))])

Tips 和技巧

一些有用的链接

下面是我自己写的一些组件,因为我在继续深入研究roam/render。请在你的测试/开发 Graph 中尝试它,一定要格外小心。虽然我相信这些例子,应该可以在不损坏你的数据库的前提下工作,但如果出了问题,我概不负责哦……

你也可以随意浏览我的 Graph,里面有很多有关 Clojure 和 Reagent 成功和不成功的尝试。我会尽量把成功/不成功的解决方案都标出来。

` {{roam/render: ((block-UID))}} 引用组件的形式不是很友好。它需要点击几次才能得到 block-uid` 并将其插入到渲染的 Block 中。我发现了两种选择。

你可以在 roam/templates 中创建一个简单的本地漫游模板,然后在那里用一个友好的名字添加你的组件。然后,当你需要它时,你可以使用;;template-name将组件插入到你的文档中。

你也可以 Hack 一下 block 的 UIDs。我在这里提供了一个解决方案。这将允许你用更友好的名字来创建组件,比如我的 query 查询组件。 ` {{roam/render: ((str_query))}} `。

这个让我头疼了几个小时。 Clojure 采用的是单通道(single-pass)编译。这意味着,如果一个函数在声明之前被调用,编译器会抛出一个错误。在某些情况下,不可能按照函数的使用顺序来声明函数,比如在涉及两个函数的迭代中。这时clojure.core/declare就很用了,你可以做一个正向声明,编译器就会知道,在这之后会有这个函数的定义。

输出 Roam 的原生链接

如果你正在构建组件,当你想输出 Roam 原生链接时将会遇到一个问题。你可以直接点击链接,就会打开页面。你也可以shift-click的链接,就会在侧边栏中打开页面。我发现有三种方法可以实现输出这种可工作的链接。

最简单的方法是使用roamAlphaAPI创建一个 Block,并在这个 Block 字符串中放置一个page linkblock-ref。这将转换为一个正确的 Roam 链接。

另一种方法是创建一个包含 Roam 查询的 Block,并在查询参数中包含你想显示的链接: {{query: {or: page linkblock-ref}}

由于 Roam 在几天前发布了roam.right-sidebar命名空间,现在可以完全模仿 Roam 原生链接了。我还没有时间去试验这第三个选项,但它看起来是可行的。

如果你对此感兴趣,不妨到我的Graph中看看,也许从写这篇文章的时候起,我已经实现了解决方案。 我将创建一个响应点击事件的 Reagent 组件。在点击时,我将把浏览器导航到一个类似于下面 JavaScript 例子的链接。在shift-click时,我将使用roam.right-sidebar来打开链接。

function getRoamLink(uid) {
  if (window.location.href.includes('/page/'))
    return window.location.href.substring(window.location.origin.length + 1, window.location.href.length - 9) + uid
  else
    return (
      window.location.href.substring(window.location.origin.length + 1, window.location.href.length) + '/page/' + uid
    )
}

Debug 调试

我使用的是以下代码进行调试。通过将 slient 开关转为 true,我可以禁用console.log调试信息。可能无需多言,input 参数只是为了这个例子。

Debug demo
console.log
(ns blogpost.9)

(def silent false)
(defn debug [x]
  (if-not silent (apply (.-log js/console) x)))

(defn main [{:keys [block-uid]} & args]
  (debug ["(main)  block-uid: " block-uid "  args: " args])
  [:div "debug demo"])

基于 Roam 是一个笔记应用这个前提,拥有 ClojureScript 环境当然很牛逼。但是,就像瑞士军刀的微型锯子一样,在某些情况下,它是很方便的。但如果你想砍一棵树,请去买一把电锯。

roam/render 有两个巨大的限制。它没有文档,更重要的是,它缺乏适当的调试工具。它确实会将一些错误信息输出到控制台日志中,如果有一致的调试信息,就有可能追踪到错误,但它严重缺乏基本的调试功能,如 breakpoints、watches 等等。当你排除 ClojureScript 代码故障时,就会比本来要花费的时间要长很多。

比较好的一点是,你可以将代码分别放到兄弟层级的 Block,这样就可以更好地添加注释了,利用中间的 Block 加入流程图、解释等,有点类似于 Python 中的 Jupiter Notebook。但是,你无法将可能频繁使用的函数,放到同一个共享的命名空间,以便在组件之间重用。我尝试将引用共享名称空间的 Block 作为组件的同级 Block,但不起作用。一个变通的办法是把所有组件的源代码放在同一个页面上,并放到同一个命名空间中。这样做是可行的,但如果你有很多组件,恐怕会变得很乱。这也会让你很难跟其他人共享组件。

Datascript 也有一些限制。你不能创建自己的转换函数。你不能在 Roam 数据库中创建自定义实体。还有一些 Clojure 命名空间和函数缺失。例如,clojure.string/lower-case无法使用。我想使用小写字母来支持对大小写不敏感的搜索,幸运的是,实际情况下配合 clojure.core/re-findclojure.core/re-pattern(?i) flag 有一个可以变通的方法。

在笔记应用中拥有一个 Clojure 执行环境绝对是很酷的。但是,如果没有相应的调试工具和文档,开发组件的效率其实非常低。

比较好的一点是 roam/render 和 ClojureScript 可以用来输出渲染自定义组件,比如表单、数据透视表和其他交互工具。

基于我现有的知识,其实与 JavaScript 互操作似乎是最好的方式。你可以使用 roam/render,顾名思义,渲染输出的部分,然后你可以使用 JavaScript 来构建应用程序的逻辑部分。如果这样做的话,你就可以同时得到两个世界里最棒的部分了。你可以在 Reagent 中轻松地渲染响应式组件,这样就是在一个真正的开发环境中,使用满足需求的调试工具完成大部分的开发工作。此外,你可以使用你已经熟悉的语言进行开发(假设你已经具备了 JS 技能),你还能够在 JavaScript 中创建可重用的代码,这样还可以跟其他人共享复用代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK