javascript
Sweet.js 用 Readtables 编译 JSX
from http://jlongster.com/Compiling-JSX-with-Sweet.js-using-Readtables
JSX http://facebook.github.io/react/docs/jsx-in-depth.html 是一個 Facebook 項目,給 js 嵌入了 xml-like 語言,它是 React http://facebook.github.io/react/ 中標志性的使用特色。很多人喜歡它并且發現它非常好用。不幸的是他需要獨立的編譯器,并且不能和其他語言混合或者擴展。我用 sweet.js 宏 http://sweetjs.org/ 實現了一個 JSX “編譯器” https://github.com/jlongster/jsx-reader,因而你可以把 jsx 和其他任何宏語言擴展一起放手邊隨時供使用。
我預見,預見有人能給 js 語言添加一些難懂的特性,比如模式匹配 https://github.com/natefaubion/sparkler ,然后我只需要安裝模塊然后使用它。在 js 影響深遠的今天,我認為這種語言擴展能力是非常重要滴。
這很重要,不僅僅是因為我或者你能擁有原生 goroutines https://gobyexample.com/goroutines 或者 原生持久數據解構 http://swannodette.github.io/mori/ 語法 。它更令人難以置信的是,我們用的野生的特性,可能成為 ES 標準的一部分,甚至成為標準的 JS 的一部分。未來的 js 會由于我們的反饋變得更好。我們需要模塊化的方式來擴展 js,一但這個可行,會無縫的與無數的擴展共享成果。
我不準備解釋為啥 sweet.js 宏是這個目標的答案。如果你想聽更多,看我的 JSConf 2014 演講 https://www.youtube.com/watch?v=wTkcGprt5rU 。如果你準備寫負面的評論,請先閱讀這篇 http://jlongster.com/Compiling-JSX-with-Sweet.js-using-Readtables#concerned
我為啥在 sweet.js 中實現一個 JSX 呢?如果你使用 JSX,現在你可以在任何其他的可用宏 https://www.npmjs.org/search?q=sweet-macros 邊上使用 jsx-reader https://github.com/jlongster/jsx-reader。想要原生語法給持久化數據結構?繼續讀下去。。。
遇到的問題
jsx 這么工作:xml 元素轉換為簡單的 js 對象。
var div = <div><h1>{ header }</h1> </div>;這轉化為:
var div = React.Dom.div(null, React.DOM.h1(null, header));http://sweetjs.org/ 直到這周 jsx 都無法把它實現,是什么原因讓這做到了? 可讀表 Readtables http://sweetjs.org/doc/main/sweet.html#reader-extensions
我簡單解釋下 sweet.js 的一些技術環境。Sweet.js 主要工作在口令層面,不是 AST,AST是唯一能擴展語言的組成部分的方式 見 *。其中的算法大量的來自 10 多年的 Lisp Scheme 社區尤其是 Racket http://docs.racket-lang.org/ 的工作基礎之上
我們的想法是,你在一個輕量的口令樹之上提供語言的宏定義,然后展開這些口令。定制的模式匹配 http://sweetjs.org/doc/main/sweet.html#rule-macros 和 全自動衛生 http://sweetjs.org/doc/main/sweet.html#hygiene 讓它可以做非常復雜的擴展,并且你可以從 sourcemaps 上獲得更多。
生成的管道看起來像這樣:
- read 讀一段字符串代碼,產生一個口令樹。它會消除歧義的正則表達式語法,分類,以及其他的有趣的東西。你會得到一個很漂亮的口令樹,上面是原子的語法片段。它是一顆用 {}()[] 作為葉子來作為分隔符口令的樹。
- expand 遍歷口令樹,展開任何找到的宏。它用名字查找宏,用剩下的語法來調用宏。這個階段還會做一個輕快的解析,比如指出一些不合法的表達式
- parse 處理展開后的樹,生成一些真正的 js AST。目前 sweet.js 使用了一個打了一些補丁的 esparima 版本來做這個工作
- generate 從 AST 生成最后的 js 代碼。 sweet.js 使用 escodegen 來做這個工作
expand 階段本質上是給語言解析器添加了展開性。這對很多特性都很好。比如 類型 模塊 等那些需要整個程序的信息,還有那些在解析 parse 階段或者生成階段最好有 AST 支持的特性。現在我們還沒有能展開到那個階段。
很少有特性需要擴展到 read 階段, JSX 就是其中之一。 Lisp 早就有一個叫做 readtables 的東西了,我最近意識到我們的 js 需要類似的東西,以支持 JSX 的實現。因此我實現了它 https://github.com/mozilla/sweet.js/pull/340 !
有幾個原因 jsx 需要作為一個 reader 生效而不是作為宏:
Reader 擴展允許你安裝一個自定義的 reader 當在源碼中遇到一個特殊的單詞的時候調用。你能讀取你需要的足夠多的源碼,然后返回一個口令組。reader 擴展只能被用標點的 punctuators 觸發。(比如 < % 等符號),因此你不能做比如改變引號起作用的方式這類可怕的事情。
一個可讀表是一個給 reader 擴展用的字符表。在這里 http://sweetjs.org/doc/main/sweet.html#reader-extensions 的這個文檔中來深入了解。
- 關于這點我有許多思考和論點,我會在未來的文章中展開來說。在 AST 層面工作是一個很棒的特性,這需要整個程序,比如類型和代碼分析的優化。看這篇 http://blog.fogus.me/2012/04/25/the-clojurescript-compilation-pipeline/ 來了解關于這個想法的一個非常好的解釋。
npm 上可用的: jsx-reader
現在我們有了可讀表,我們可以實現 jsx 啦!我已經用 jsx-reader https://github.com/jlongster/jsx-reader 做到了。他是一個文字的 jsx 編譯器端口,機智的做到了所有的空白規則以及其他的邊緣情況。(希望如此)
加載一個 reader 擴展,傳入模塊名給 sweet.js 用 -l 編譯 sjs 。這是所有的步驟:
$ npm install sweet.js $ npm install jsx-reader $ sjs -l jsx-reader file.js當然你也可以加載任何其他的宏,一起作用在你的文件上。這是一個組合的很漂亮的語言擴展(試試 es6-macroshttps://github.com/jlongster/es6-macros)
我已經創建了一個 webpack loader https://github.com/jlongster/sweetjs-loader 和一個 gulp loader https://github.com/jlongster/gulp-sweetjs 而且是最新的支持 readtable 加載的。
這是測試版的軟件我在一些小的原生的 jsx 編譯器的測試用例上測試通過了,此外它也在一些大文件上工作良好。However,依然有一些小 bugs 和邊緣情況需要被人們發現他之后修補。
你不僅僅能獲得可靠的 sourcemaps (傳入 -c 給 sjs),而且你也會比原生 jsx complier 有更好的錯誤提示。例如:如果你忘了關閉一個標簽:
var div = <div><h1>Title</h1><p> </div>你會得到一個漂亮的,感覺的錯誤信息,明確的指出你的代碼中的錯誤
SyntaxError: [JSX] Expected corresponding closing tag for p 4: </div>^at Object.readtables.readerAPI.throwSyntaxError (/Users/james/tmp/poop/node_modules/sweet.js/lib/parser.js:5075:23)有一個退步就是,這比原生的 jsx compiler 慢。一個 2000 行代碼的大文件會花費 ~.7s 來編譯(排序了預熱時間,因為你會在大多數項目中使用監聽器 watcher),原生的只要 ~0.4s。實際上,這不是特別明顯,因為大多數文件是很多的更小的東西,大部分在幾百毫秒就編譯完了。當然,sweet.js 會更努力滴優化這個時間滴。
例子: 持久化數據結構
React 和持久化數據結構一起工作會表現得更好。問題是 js 原生沒有它們,但是幸運的是有 mori http://swannodette.github.io/mori/ 這些庫可用。只是你再也不能用對象字面值了;你不得不 mori.vector(1,2,3) 來代替 [1,2,3];
如果我們給 mori 實現一個字面的語法會怎樣?可能你就用 #[1,2,3] 來創建持久的矢量,而 #{x: 1, y: 2} 來創建持久的表 map 。那也太棒了吧!(不幸的是我現在還沒做到,但我非常渴望做到這點。。。)
現在每個用 jsx 的人都可以在 React 中用我的字面語法來持久化數據結構。這真的是一個給力的工具包。
jsx 的 read 算法
給 js 添加新的語法,特別擁有大塊展示區域的比如 jsx。必須仔細的實現。它必須 100% 向后兼容,并且和 ES.next 特性一樣在腦中完成。
jsx-reader 和 原生 jsx 編譯器都查找 < 口令并觸發一個 jsx 表達式的解析。雖然這個有一個關鍵的不同。你可能注意到 jsx-reader 作為一個 reader,被源代碼調用的時候是沒有環境的。原生的 jsx 編譯器用被猴子們打了補丁 monkeypatches 的 esprima 來調用 < ,當解析一個表達式的時候,因此它更容易保證正確的解析。 < 在 js 表達式中從不會有效,因此它能這么用它。
jsx-reader 也是用 < 調用,當它在源碼的任何地方出現的時候,甚至是比較大小的操作符的時候。這讓人提心吊膽的;我們需要更仔細。但是我想到了一個可行的算法。你不會一直需要一個 ast。
jsx-reader 開始解析 < 然后任何它后面的東西作為 jsx 表達式,如果它遇到一些確定不期望的點,它停下來。大多數時候,它能很早的發覺 < 之后是或者不是 jsx 表達式。算法來了:
我很快的打出了這些,可能有更好的方式啦,但是你應該了解了這思路。主要是一般情況下很容易消除歧義,但是所有的邊緣情況都需要生效。邊緣情況不是特別高性能,因為我們的 reader 可能要做很多工作然后丟棄它,但是 99.9% 的情況下不會發生這種事情。
在我們的算法中,如果我們說 read 而不說相應的的 “如果失敗了 bail”,它會拋出一個錯誤。我們依然可以給出好的錯誤提示,當遇到二義性的 js 邊緣情況的時候。
這兒有幾個我們的 reader bails的地兒:
- if (x < y) {} 它 bails 因為它查找一個 y 后面的屬性 以及 ) 不是一個合法的標識符字符
- if (x < y > z) {} 它 bails 是因為它 reads 所有的到文件末尾的方式但沒有找到一個閉合的標簽。這只發生在頂級的元素上,而且是個性能的最差的情況。但是 x < y > z 不會做你想要的,而且沒人會這么用
- if (x < div > y < /foo> /) {} 這是最難懂的情況,他是完全有效的 js 。它 bails 因為它最后讀取了一個正則
我們獲取這個實際的優點:表達式像 x < y foo 在 js 中不會存在。這兒,它既不找 = 來解析屬性,也不找 > 來關閉元素,而是直接報錯,如果它沒找到的話。
你擔心嗎?
宏調用,弄混了一些人,還有很多遲疑的認為它們是好用的東西。你可能也這么想,而且爭論一件事情:像 readtables 這樣的東西標志著我們進入了很深的地方。
我請你們仔細想想 sweet.js。給它 5 分鐘。也許幾個小時。玩玩它:設置一個 gulp 監控 wathcer,從 npm 安裝一些宏并且用它。別直接反對它你實際上除非你理解了我們嘗試用它來解決的一些問題。許多人們給出的爭論都沒有很容易體會到那種場景(但有一些有!)
不管那些,甚至你認為這是錯誤的方法。他當然是有效的。軟件工業對我老說最困惑的一件事情就是:我們對其他人到底能有多惡毒,所以請做些建設性的事情。
今天就試試 jsx-reader https://github.com/jlongster/jsx-reader 如果發現 bugs https://github.com/jlongster/jsx-reader/issues
以上;
總結
以上是生活随笔為你收集整理的Sweet.js 用 Readtables 编译 JSX的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hadoop--HDFS的Shell相关
- 下一篇: GC参考手册 —— GC 调优(基础篇)