[关闭]
@atry 2016-09-24T16:23:43.000000Z 字数 4503 阅读 1584

More than React(四)HTML也可以静态编译?

前端 binding.scala data-binding scala.js HTML


《More than React》系列的上一篇文章虚拟DOM已死?比较了 Binding.scala 和其他框架的渲染机制。本篇文章中将介绍 Binding.scala 中的 XHTML 语法。

其他前端框架的问题

对 HTML 的残缺支持

以前我们使用其他前端框架,比如 Cycle.js 、 Widok 、 ScalaTags 时,由于框架不支持 HTML 语法,前端工程师被迫浪费大量时间,手动把 HTML 改写成代码,然后慢慢调试。

就算是支持 HTML 语法的框架,比如 ReactJS ,支持状况也很残缺不全。

比如,在 ReactJS 中,你不能这样写:

  1. class BrokenReactComponent extends React.Component {
  2. render() {
  3. return (
  4. <ol>
  5. <li class="unsupported-class">不支持 class 属性</li>
  6. <li style="background-color: red">不支持 style 属性</li>
  7. <li>
  8. <input type="checkbox" id="unsupported-for"/>
  9. <label for="unsupported-for">不支持 for 属性</label>
  10. </li>
  11. </ol>
  12. );
  13. }
  14. }

前端工程师必须手动把 classfor 属性替换成 classNamehtmlFor,还要把内联的 style 样式从 CSS 语法改成 JSON 语法,代码才能运行:

  1. class WorkaroundReactComponent extends React.Component {
  2. render() {
  3. return (
  4. <ol>
  5. <li className="workaround-class">被迫把 class 改成 className</li>
  6. <li style={{ backgroundColor: "red" }}>被迫把样式表改成 JSON</li>
  7. <li>
  8. <input type="checkbox" id="workaround-for"/>
  9. <label htmlFor="workaround-for">被迫把 for 改成 htmlFor</label>
  10. </li>
  11. </ol>
  12. );
  13. }
  14. }

这种开发方式下,前端工程师虽然可以把 HTML 原型复制粘贴到代码中,但还需要大量改造才能实际运行。比 Cycle.js 、 Widok 或者 ScalaTags 省不了太多事。

不兼容原生 DOM 操作

此外,ReactJS 等一些前端框架,会生成虚拟 DOM 。虚拟 DOM 无法兼容浏览器原生的 DOM API ,导致和 jQuery 、 D3 等其他库协作时困难重重。比如 ReactJS 更新 DOM 对象时常常会破坏掉 jQuery 控件。

Reddit很多人讨论了这个问题。他们没有办法,只能弃用 jQuery。我司的某客户在用了 ReactJS 后也被迫用 ReactJS 重写了大量 jQeury 控件。

Binding.scala 中的 XHTML

现在有了 Binding.scala ,可以在 @dom 方法中,直接编写 XHTML。比如:

  1. @dom def introductionDiv = {
  2. <div style="font-size:0.8em">
  3. <h3>Binding.scala的优点</h3>
  4. <ul>
  5. <li>简单</li>
  6. <li>概念少<br/>功能多</li>
  7. </ul>
  8. </div>
  9. }

以上代码会被编译,直接创建真实的 DOM 对象,而没有虚拟 DOM 。

Binding.scala 对浏览器原生 DOM 的支持很好,你可以在这些 DOM 对象上调用 DOM API ,与 D3 、 jQuery 等其他库交互也完全没有问题。

ReactJS 对 XHTML 语法的残缺不全。相比之下,Binding.scala 支持完整的 XHTML 语法,前端工程师可以直接把设计好的 HTML 原型复制粘贴到代码中,整个网站就可以运行了。

Binding.scala 中 XHTML 的类型

@dom 方法中 XHTML 对象的类型是 Node 的派生类。

比如,<div></div> 的类型就是 HTMLDivElement,而 <button></button> 的类型就是 HTMLButtonElement

此外, @dom 注解会修改整个方法的返回值,包装成一个 Binding

  1. @dom def typedButton: Binding[HTMLButtonElement] = {
  2. <button>按钮</button>
  3. }

注意typedButton是个原生的HTMLButtonElement,所以可以直接对它调用 DOM API。比如:

  1. @dom val autoPrintln: Binding[Unit] = {
  2. println(typedButton.bind.innerHTML) // 在控制台中打印按钮内部的 HTML
  3. }
  4. autoPrintln.watch()

这段代码中,typedButton.bind.innerHTML 调用了 DOM API HTMLButtonElement.innerHTML。通过autoPrintln.watch(),每当按钮发生更新,autoPrintln中的代码就会执行一次。

其他 HTML 节点

Binding.scala 支持 HTML 注释:

  1. @dom def comment = {
  2. <!-- 你看不见我 -->
  3. }

Binding.scala 也支持 CDATA 块:

  1. @dom def inlineStyle = {
  2. <section>
  3. <style><![CDATA[
  4. .highlight {
  5. background-color:gold
  6. }
  7. ]]></style>
  8. <p class="highlight">Binding.scala真好用!</p>
  9. </section>
  10. }

内嵌 Scala 代码

除了可以把 XHTML 内嵌在 Scala 代码中的 @dom 方法中,Binding.scala 还支持用 { ... } 语法把 Scala 代码内嵌到 XHTML 中。比如:

  1. @dom def randomParagraph = {
  2. <p>生成一个随机数: { math.random.toString }</p>
  3. }

XHTML 中内嵌的 Scala 代码可以用 .bind 绑定变量或者调用其他 @dom 方法,比如:

  1. val now = Var(new Date)
  2. window.setInterval(1000) { now := new Date }
  3. @dom def render = {
  4. <div>
  5. 现在时间:{ now.bind.toString }
  6. { introductionDiv.bind }
  7. { inlineStyle.bind }
  8. { typedButton.bind }
  9. { comment.bind }
  10. { randomParagraph.bind }
  11. </div>
  12. }

上述代码渲染出的网页中,时间会动态改变。

强类型的 XHTML

Binding.scala 中的 XHTML 都支持静态类型检查。比如:

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

由于以上代码有拼写错误,编译器就会报错:

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

内联 CSS 属性

style 属性设置内联样式时,style 的值是个字符串。比如:

  1. @dom def invalidInlineStyle = {
  2. <div style="color: blue; typoStyleName: typoStyleValue"></div>
  3. }

以上代码中设置的 typoStyleName 样式名写错了,但编译器并没有报错。

要想让编译器能检查内联样式,可以用 style: 前缀而不用 style 属性。比如:

  1. @dom def invalidInlineStyle = {
  2. <div style:color="blue" style:typoStyleName="typoStyleValue"></div>
  3. }

那么编译器就会报错:

  1. typo.scala:28: value typoStyleName is not a member of org.scalajs.dom.raw.CSSStyleDeclaration
  2. <div style:color="blue" style:typoStyleName="typoStyleValue"></div>
  3. ^

这样一来,可以在编写代码时就知道属性有没有写对。不像原生 JavaScript / HTML / CSS 那样,遇到 bug 也查不出来。

自定义属性

如果你需要绕开对属性的类型检查,以便为 HTML 元素添加定制数据,你可以属性加上 data: 前缀,比如:

  1. @dom def myCustomDiv = {
  2. <div data:customAttributeName="attributeValue"></div>
  3. }

这样一来 Scala 编译器就不会报错了。

结论

本文的完整 DEMO 请访问 ScalaFiddle

从这些示例可以看出,Binding.scala 一方面支持完整的 XHTML ,可以从高保真 HTML 原型无缝移植到动态网页中,开发过程极为顺畅。另一方面,Binding.scala 可以在编译时静态检查 XHTML 中出现语法错误和语义错误,从而避免 bug 。

以下表格对比了 ReactJS 和 Binding.scala 对 HTML 语法的支持程度:

ReactJSBinding.scala
是否支持HTML语法? 残缺支持 完整支持
是否支持标准的style属性? 不支持,必须改用 JSON 语法 支持,既支持标准的style属性也支持style:前缀
是否支持标准的class属性? 不支持,必须改用className 支持,既支持class也支持className
是否支持标准的for属性? 不支持,必须改用htmlFor 支持,既支持for也支持htmlFor
是否支持HTML注释? 不支持 支持
是否兼容原生DOM操作? 不兼容 兼容
是否兼容jQuery? 不兼容 兼容
能否在编译时检查出错误? 不能

我将在下一篇文章中介绍 Binding.scala 如何实现服务器发送请求并在页面显示结果的流程。

相关链接

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注