Skip to content

Latest commit

 

History

History
493 lines (384 loc) · 18.5 KB

README-zh.md

File metadata and controls

493 lines (384 loc) · 18.5 KB

Binding.scala

Join the chat at https://gitter.im/ThoughtWorksInc/Binding.scala Build Status Maven Central (core funtionality) Maven Central (DOM integration) Maven Central (remote data-binding for scala.concurrent.Future) Maven Central (remote data-binding for ECMAScript 2015 Promise)

Binding.scala 是一个用 Scala 语言编写的数据绑定框架,可以在 JVM 和 Scala.js 上运行。

Binding.scala 可以用作 reactive web freamework。 它允许你使用原生 XHTML 语法去创建 reactive DOM 节点,这种 DOM 节点可以在数据源发生变化时自动地改变。

使用 Binding.scala 时可以参考 Binding.scala • TodoMVC 或者其他 DEMOs,他们中包含了许多常见功能的实现。

与其他 reactive web framework 对比

与其他 reactive web framework(例如 ReactJS)对比,Binding.scala 有更多的特性以及更少的概念。

Binding.scala ReactJS
HTML 语法支持 支持 部分支持。Regular HTML 不会编译,除非开发者人为地将 class 属性和 for 属性改为 classNamehtmlFor,并且人为地将 行内样式 的语法从 CSS 语法改为 JSON 语法。
DOM 更新算法 精准的数据绑定,比虚拟 DOM 更快 虚拟 DOM 之间存在差异,对于复杂的 DOM 你需要手动地管理 key 属性。
数据绑定表达式的生命周期管理 完全自动
静态类型检查 支持,甚至是 HTML 标签和属性 不支持
学习曲线 一直很简单 容易上手,但在理解边界情况时需要投入大量精力。

更多详细信息,请查看设计

开始使用

我们将在接下来的步骤里编写一个 Binding.scala 网页。

第 0 步:配置一个 Sbt Scala.js 项目

参考 http://www.scala-js.org/tutorial/basic/ 获取详细信息。

第 1 步:在你的 build.sbt 中添加 Binding.scala 依赖项

libraryDependencies += "com.thoughtworks.binding" %%% "dom" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

第 2 步:创建一个 data 域,其中包含一些 VarVars 作为你数据绑定表达式的数据源

case class Contact(name: Var[String], email: Var[String])

val data = Vars.empty[Contact]

一个 Var 代表一个可绑定变量, 同时它也实现了 Binding 特性, 这就意味着一个 Var 也可以被视为一个是数据绑定表达式。 如果其他数据绑定表达式依赖与一个 Var ,那么该表达式的值将在这个 Var 的值改变时作出相应的改变。

一个 Vars 代表一个可绑定变量序列, 同时它也实现了 BindingSeq 特性, 这就意味着一个 Vars 也可以被视为一个序列的数据绑定表达式。 如果另一个数据绑定表达式依赖与一个 Vars , 那么该表达式的值将在这个 Vars 的值改变时作出相应的改变。

第 3 步:创建一个含有数据绑定表达式的 @dom 方法

@dom
def table: Binding[Table] = {
  <table border="1" cellPadding="5">
    <thead>
      <tr>
        <th>Name</th>
        <th>E-mail</th>
      </tr>
    </thead>
    <tbody>
      {
        for (contact <- data) yield {
          <tr>
            <td>
              {contact.name.bind}
            </td>
            <td>
              {contact.email.bind}
            </td>
          </tr>
        }
      }
    </tbody>
  </table>
}

一个 @dom 方法代表一个数据绑定表达式。

其返回值类型永远被包装成 com.thoughtworks.binding.Binding 特性。 例如, @dom def x: Binding[Int] = 1@dom def message: Binding[String] = "content"

@dom 方法支持 HTML 语法。 并不像通常 Scala 方法中的 XML 语法, HTML 语法的类型是 org.scalajs.dom.raw.Node 或者 com.thoughtworks.binding.BindingSeq[org.scalajs.dom.raw.Node] 的子类型, 而不是 scala.xml.Node 或者 scala.xml.NodeSeq。 因此我们写出这样的代码,@dom def node: Binding[org.scalajs.dom.raw.HTMLBRElement] = <br/> 以及 @dom def node: Binding[BindingSeq[org.scalajs.dom.raw.HTMLBRElement]] = <br/><br/>

由其他数据绑定表达式组成的 @dom 方法有两种编写方式:

  1. 你可以在 @dom 方法中使用 bind 方法来获取其他 Binding 的值。
  2. 你可以在 @dom 方法中使用 for 或者 yield 表达式将 BindingSeq 映射到其他的表达式上。

你可以通过使用 { ... } 插入语法来在其他 HTML 元素中嵌入 Node 或者 BindingSeq[Node]

第 4 步:在 main 方法中将数据绑定表达式渲染至 DOM

@JSExport
def main(): Unit = {
  dom.render(document.body, table)
}

第 5 步: 在 HTML 页面中调用 main 方法

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="js-fastopt.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      SampleMain().main()
    </script>
  </body>
</html>

至此,你会看见一个只含有表头的空表格,这是因为 data 现在是空的。

第 6 步:添加一些 <button> 用于填充表格的 data

@dom
def table: Binding[BindingSeq[Node]] = {
  <div>
    <button
      onclick={ event: Event =>
        data.get += Contact(Var("Yang Bo"), Var("[email protected]"))
      }
    >
      Add a contact
    </button>
  </div>
  <table border="1" cellPadding="5">
    <thead>
      <tr>
        <th>Name</th>
        <th>E-mail</th>
        <th>Operation</th>
      </tr>
    </thead>
    <tbody>
      {
        for (contact <- data) yield {
          <tr>
            <td>
              {contact.name.bind}
            </td>
            <td>
              {contact.email.bind}
            </td>
            <td>
              <button
                onclick={ event: Event =>
                  contact.name := "Modified Name"
                }
              >
                Modify the name
              </button>
            </td>
          </tr>
        }
      }
    </tbody>
  </table>
}

当你点击 "Add a contact" 按钮时, 它会在 data 中添加一个新的联系人, 于此同时,因为 Binding.scala 知道 DOM 与 data 之间的关系, 那么它就会添加一个新的、与新增的联系人信息对应的 <tr>

当你点击 "Modify the name" 按钮时,对应联系人的 name 将会发生改变, 这是因为 Binding.scala 将对应的 trname 的值改变了。

完整的例子请参考 https://github.com/ThoughtWorksInc/Binding.scala-sample

设计

精准的数据绑定

ReactJS 需要用户为每一个组件提供 render 函数。 render 函数需要将 propsstate 映射至 ReactJS 的虚拟 DOM, 然后,ReactJS 框架会根据虚拟 DOM 的结构,创建一套结构相同的真实的 DOM。

state 改变时,ReactJS 框架会调用 render 函数去获取一个新的虚拟 DOM。 然而很不幸的是,ReactJS 并不能够确切地知道 state 改变了什么。 因此,ReactJS 不得不将新的虚拟 DOM 和原来的虚拟 DOM 进行比较, 并由此来猜测两个虚拟 DOM 之间的改变,最终应用到实际的 DOM 上。

举例来说,在你在 <table> 中的 <tbody> 的开头添加了一行 <tr>, ReactJS 可能会认为你改变了 <tbody> 中所有的 <tr>, 并且在末尾添加了一行 <tr>

原因是 ReactJS 中的 render 函数并不能描述 state 和 DOM 之间的关系。 相反,它表达的是创建虚拟 DOM 的过程。 也就是说,尽管作为数据绑定框架毫无疑问需要关于 state 改变的信息,但是 ReactJS 中的 render 函数却不能提供相关信息。

与 ReactJS 不同,Binding.scala 中的 @dom 方法并不是一个普通的函数。 它是一个描述数据源和 DOM 之间关系的模板。 当数据源发生部分改变时,Binding.scala 可以知道 DOM 中具体的哪些部分对应这些改变的数据。 因此,Binding.scala 只需要重新计算部分的 @dom 方法来获取部分的 DOM。

有着 Binding.scala 提供的精准的数据绑定能力的帮助,你可以摆脱不必要的、用于应对 ReactJS 的猜测算法的概念, 比如 key 属性,shouldComponentUpdate 方法,componentDidUpdate 方法以及 componentWillUpdate 方法。

模块性

在 ReactJS 中最小的组成单元是组件。

毫无疑问 React 组件要比 AngularJS 控制器更加轻量级,然而 Binding.scala 又比它更加优秀。

在 Binding.scala 中最小的组成单元仅仅是 @dom 方法。 每一个 @dom 方法有能力通过 .bind 组合其他的 @dom 方法。

case class Contact(name: Var[String], email: Var[String])

@dom
def bindingButton(contact: Contact): Binding[Button] = {
  <button
    onclick={ event: Event =>
      contact.name := "Modified Name"
    }
  >
   Modify the name
  </button>
}

@dom
def bindingTr(contact: Contact): Binding[TableRow] = {
  <tr>
    <td>{ contact.name.bind }</td>
    <td>{ contact.email.bind }</td>
    <td>{ bindingButton(contact).bind }</td>
  </tr>
}

@dom
def bindingTable(contacts: BindingSeq[Contact]): Binding[Table] = {
  <table>
    <tbody>
      {
        for (contact <- contacts) yield {
          bindingTr(contact).bind
        }
      }
    </tbody>
  </table>
}

@JSExport
def main(): Unit = {
  val data = Vars(Contact(Var("Yang Bo"), Var("[email protected]")))
  dom.render(document.body, bindingTable(data))
}

你可以发现这种方法要比 ReactJS 简单很多,因为:

  • 你可以简单地为 Binding.scala 提供参数,而不用像在 ReactJS 中一样传递 props
  • 在 Binding.scala 中,你可以简单地定义参数类型,而不用像在 ReactJS 中一样指定 propTypes
  • 你可以在编译期进行类型检查,而不用像在 ReactJS 中一样只能在运行期间获得异常。

数据绑定表达式的生命周期管理

毫无疑问,Binding.scala 具有的精准数据绑定能力需要监听者对数据源注册。 其他的拥有这种能力的 reactive frameworks 需要用户管理数据绑定的生命周期。

例如,MetaRx 提供了 dispose 方法用于注销监听者。 在表达式改变后,每次调用 mapflatMap 方法,MetaRx 的用户都有义务去调用 dispose 方法, 否则 MetaRx 会有内存泄漏。然而非常不幸的是,对于复杂的绑定表达式,每次都需要手动地正确调用 dispose 实在是太困难了。

另一个 reactive web framework Widok 没有提供任何机制用于管理数据绑定表达式的生命周期。 而这样的结果就是,Widok 一直在泄漏内存。

在 Binding.scala 中,与 MetaRx 以及 Widok 不同,所有的数据绑定表达式都是纯函数式的,不带有任何副作用。 Binding.scala 不会在用户创建表达式式注册任何的监听者, 因此用户并不需要像在 MetaRx 一样手动地注销监听者。

当用户在根表达式中调用 dom.render 或者 Binding.watch 时,Binding.scala 一同创建了所有的监听者,而不仅仅是根表达式的直接监听者。

简而言之,Binding.scala 将函数分为两类:

  • 用户定义的 @dom 方法,用于产生不带副作用的纯函数式的数据绑定表达式。
  • 调用 dom.render 以及 Binding.watch 的函数,自动地管理所有副作用。

HTML 语法和静态类型检查

如你所见,你可以在 Scala 源码文件中的 @dom 方法里嵌入 HTML 语法。 你也可以在 HTML 标签里在 { ... } 中或者在属性值中书写 Scala 代码。

@dom
def notificationBox(message: String): Binding[Div] = {
  <div class="notification" title={ s"Tooltip: $message" }>
    {
      message
    }
  </div>
}

不论 HTML 语法在 Binding.scala 和 ReactJS 之间的相似性, Binding.scala 创建真正的 DOM 而不是 ReactJS 中的虚拟 DOM。

在上述例子中,<div>...</div> 创建了一个类型为 org.scalajs.dom.html.Div 的 DOM 元素。 之后,神奇的 @dom 使得返回值被包装成 Binding 类型。

你甚至可以将 Div 赋值给局部变量并且在它上面调用原生 DOM 方法:

@dom
def notificationBox(message: String): Binding[Div] = {
  val result: Div = <div class="notification" title={ s"Tooltip: $message" }>
    {
      message
    }
  </div>

  result.scrollIntoView()

  result
}

scrollIntoView 方法会在 Div 创建后被调用。 如果你在 Div 上调用了一个并没有定义的方法, Scala 编译器会报告一个编译器错误而不是只有在运行时才会发生错误。 原因是 Scala 是静态类型语言而且 Scala 编译器可以理解 Div 类型。

你可能也注意到了 classtitle。 他们是 DOM 属性或者 Div 上的 HTML 属性。 他们也会被 Scala 编译器进行类型检查。

例如,给出下面的 typo 方法:

@dom
def typo = {
  val myDiv = <div typoProperty="xx">content</div>
  myDiv.typoMethod()
  myDiv
}

Scala 会报告出如下错误:

typo.scala:23: value typoProperty is not a member of org.scalajs.dom.html.Div
        val myDiv = <div typoProperty="xx">content</div>
                     ^
typo.scala:24: value typoMethod is not a member of org.scalajs.dom.html.Div
        myDiv.typoMethod()
              ^

有着静态类型系统的帮助, @dom 方法可以比 ReactJS 组件具有更充足的鲁棒性。

你可以在 scaladoc of scalajs-dom 或者 MDN 上找到一个完整的被支持的属性和方法的列表。

自定义属性

如果你想要关闭属性的静态类型检查,在属性前添加 data: 前缀:

@dom def myCustomDiv = {
  val myDiv = <div data:customAttributeName="attributeValue"></div>
  assert(myDiv.getAttribute("customAttributeName") == "attributeValue")
  myDiv
}

现在 Scala 编译器就不会报错了。

下载

Binding.scala 只有一个很小的代码基。 源代码被分为 4 个库,每个库一个文件。

数据绑定表达式核心 (Binding.scala)

这个模块在 JVM 和 Scala.js 上均可使用。你可以把它加入到你的 build.sbt

// For JVM projects
libraryDependencies += "com.thoughtworks.binding" %% "binding" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
// For Scala.js projects, or JS/JVM cross projects
libraryDependencies += "com.thoughtworks.binding" %%% "binding" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

HTML DOM 集成 (dom.scala)

这个模块只可用于 Scala.js。你可以把它加入到你的 build.sbt

// For Scala.js projects, or JS/JVM cross projects
libraryDependencies += "com.thoughtworks.binding" %%% "dom" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

用于 scala.concurrent.Future 的远程数据绑定(FutureBinding.scala)

这个模块在 JVM 和 Scala.js 上均可使用。你可以把它加入到你的 build.sbt

// For JVM projects
libraryDependencies += "com.thoughtworks.binding" %% "futurebinding" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
// For Scala.js projects, or JS/JVM cross projects
libraryDependencies += "com.thoughtworks.binding" %%% "futurebinding" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

用于 ECMAScript 2015's Promise 的远程数据绑定(JsPromiseBinding.scala)

这个模块只可用于 Scala.js。你可以把它加入到你的 build.sbt

// For Scala.js projects, or JS/JVM cross projects
libraryDependencies += "com.thoughtworks.binding" %%% "jspromisebinding" % "latest.release"

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

其他链接