More than React系列文章:
《More than React(一)為什麼ReactJS不適合複雜的前端專案?》
《More than React(二)React.Component損害了複用性?》
《More than React(四)HTML也可以靜態編譯?》
《More than React》系列的上一篇文章《虛擬DOM已死?》比較了Binding.scala和其他框架的渲染機制。本篇文章中將介紹Binding.scala中的XHTML語法。
其他前端框架的問題
對HTML的殘缺支援
以前我們使用其他前端框架,比如Cycle.js 、Widok、ScalaTags時,由於框架不支援 HTML語法,前端工程師被迫浪費大量時間,手動把HTML改寫成程式碼,然後慢慢除錯。
就算是支援HTML語法的框架,比如ReactJS,支援狀況也很殘缺不全。
比如,在ReactJS中,你不能這樣寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class BrokenReactComponent extends React.Component { render() { return ( <ol> <li class="unsupported-class">不支援 class 屬性</li> <li style="background-color: red">不支援 style 屬性</li> <li> <input type="checkbox" id="unsupported-for"/> <label for="unsupported-for">不支援 for 屬性</label> </li> </ol> ); } } |
前端工程師必須手動把 class
和 for
屬性替換成 className
和 htmlFor
,還要把內聯的 style
樣式從CSS語法改成JSON語法,程式碼才能執行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class WorkaroundReactComponent extends React.Component { render() { return ( <ol> <li className="workaround-class">被迫把 class 改成 className</li> <li style={{ backgroundColor: "red" }}>被迫把樣式表改成 JSON</li> <li> <input type="checkbox" id="workaround-for"/> <label htmlFor="workaround-for">被迫把 for 改成 htmlFor</label> </li> </ol> ); } } |
這種開發方式下,前端工程師雖然可以把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 2 3 4 5 6 7 8 9 |
@dom def introductionDiv = { <div style="font-size:0.8em"> <h3>Binding.scala的優點</h3> <ul> <li>簡單</li> <li>概念少<br/>功能多</li> </ul> </div> } |
以上程式碼會被編譯,直接建立真實的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 2 3 |
@dom def typedButton: Binding[HTMLButtonElement] = { <button>按鈕</button> } |
注意typedButton
是個原生的HTMLButtonElement
,所以可以直接對它呼叫 DOM API。比如:
1 2 3 4 |
@dom val autoPrintln: Binding[Unit] = { println(typedButton.bind.innerHTML) // 在控制檯中列印按鈕內部的 HTML } autoPrintln.watch() |
這段程式碼中,typedButton.bind.innerHTML
呼叫了 DOM API HTMLButtonElement.innerHTML。通過autoPrintln.watch()
,每當按鈕發生更新,autoPrintln
中的程式碼就會執行一次。
其他HTML節點
Binding.scala支援HTML註釋:
1 2 3 |
@dom def comment = { <!-- 你看不見我 --> } |
Binding.scala也支援CDATA塊:
1 2 3 4 5 6 7 8 9 10 |
@dom def inlineStyle = { <section> <style><![CDATA[ .highlight { background-color:gold } ]]></style> <p class="highlight">Binding.scala真好用!</p> </section> } |
內嵌Scala程式碼
除了可以把XHTML內嵌在Scala程式碼中的 @dom
方法中,Binding.scala 還支援用 { ... }
語法把 Scala 程式碼內嵌到XHTML中。比如:
1 2 3 |
@dom def randomParagraph = { <p>生成一個隨機數: { math.random.toString }</p> } |
XHTML中內嵌的Scala程式碼可以用 .bind
繫結變數或者呼叫其他 @dom
方法,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
val now = Var(new Date) window.setInterval(1000) { now := new Date } @dom def render = { <div> 現在時間:{ now.bind.toString } { introductionDiv.bind } { inlineStyle.bind } { typedButton.bind } { comment.bind } { randomParagraph.bind } </div> } |
上述程式碼渲染出的網頁中,時間會動態改變。
強型別的 XHTML
Binding.scala中的XHTML 都支援靜態型別檢查。比如:
1 2 3 4 5 |
@dom def typo = { val myDiv = <div typoProperty="xx">content</div> myDiv.typoMethod() myDiv } |
由於以上程式碼有拼寫錯誤,編譯器就會報錯:
1 2 3 4 5 6 |
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() ^ |
內聯CSS屬性
用 style
屬性設定內聯樣式時,style
的值是個字串。比如:
1 2 3 |
@dom def invalidInlineStyle = { <div style="color: blue; typoStyleName: typoStyleValue"></div> } |
以上程式碼中設定的 typoStyleName
樣式名寫錯了,但編譯器並沒有報錯。
要想讓編譯器能檢查內聯樣式,可以用 style:
字首而不用 style
屬性。比如:
1 2 3 |
@dom def invalidInlineStyle = { <div style:color="blue" style:typoStyleName="typoStyleValue"></div> } |
那麼編譯器就會報錯:
1 2 3 |
typo.scala:28: value typoStyleName is not a member of org.scalajs.dom.raw.CSSStyleDeclaration <div style:color="blue" style:typoStyleName="typoStyleValue"></div> ^ |
這樣一來,可以在編寫程式碼時就知道屬性有沒有寫對。不像原生JavaScript / HTML / CSS那樣,遇到bug也查不出來。
自定義屬性
如果你需要繞開對屬性的型別檢查,以便為HTML元素新增定製資料,你可以屬性加上 data:
字首,比如:
1 2 3 |
@dom def myCustomDiv = { <div data:customAttributeName="attributeValue"></div> } |
這樣一來Scala編譯器就不會報錯了。
結論
本文的完整DEMO請訪問 ScalaFiddle。
從這些示例可以看出,Binding.scala 一方面支援完整的XHTML ,可以從高保真HTML 原型無縫移植到動態網頁中,開發過程極為順暢。另一方面,Binding.scala 可以在編譯時靜態檢查XHTML中出現語法錯誤和語義錯誤,從而避免bug 。
以下表格對比了ReactJS和Binding.scala對HTML語法的支援程度:
ReactJS | Binding.scala | |
---|---|---|
是否支援HTML語法? | 殘缺支援 | 完整支援 |
是否支援標準的style 屬性? |
不支援,必須改用 JSON 語法 | 支援,既支援標準的style 屬性也支援style: 字首 |
是否支援標準的class 屬性? |
不支援,必須改用className |
支援,既支援class 也支援className |
是否支援標準的for 屬性? |
不支援,必須改用htmlFor |
支援,既支援for 也支援htmlFor |
是否支援HTML註釋? | 不支援 | 支援 |
是否相容原生DOM操作? | 不相容 | 相容 |
是否相容jQuery? | 不相容 | 相容 |
能否在編譯時檢查出錯誤? | 不能 | 能 |
我將在下一篇文章中介紹 Binding.scala 如何實現伺服器傳送請求並在頁面顯示結果的流程。