Elm提供的語言級響應性
在不斷發展的JavaScript編程領域,響應性編程技術正變得愈加流行。這一系列文章試圖向大家介紹該方法目前的進展,介紹各種可用技術,以及該領域產生的變化。從Elm等新語言到Angular 2對RxJS的支持,無論從事什么工作的開發者均有相關新技術可供使用。
InfoQ的這篇文章已包含在“響應性JavaScript”系列文章中。你可以訂閱RSS并在內容更新后獲得通知。
響應性編程可以讓JavaScript程序員的生活變得更美好...但如果能使用圍繞響應性量身定制的語言來編程呢?
Elm編程語言 的目標就是如此。雖然其他實現響應性的方法會對JavaScript進行漸進式的改進,而Elm會從最基礎的地方開始重新構建這一切。Elm的誕生不是為了回答諸如“JavaScript如何能變得更好”之類的問題,而是為了回答“構建Web用戶界面時,最棒的整體開發者體驗是怎樣的”這樣的問題。
事實證明如果以此為目標進行設計,最終將獲得一種與JavaScript截然不同的語言!為了實現這一目標,Elm采用了一些JavaScript完全不具備的特征:
- 以響應性作為對交互做出響應的唯一系統
- 通過一種一致的方式管理不同特效(Effect)
- 通過更好用的編譯器提前排除大量Bug
借助這些特征,最終獲得了Elm,一種可編譯為JavaScript,但摒棄了JavaScript弱點的語言。我們經常聽說有人在生產環境中運行的Elm代碼從未遇到一個運行時異常,甚至最令人擔心的“未定義(Undefined)”也從未出現過。
Elm到底是如何實現這種截然不同的體驗的?這一切都源自它的架構。
Elm的架構
你可能已經聽說過, Elm架構 的一些靈感來自Redux和其他響應式JavaScript庫。該架構會將應用程序拆分為三個簡單的部件:
-
模型(Model)
-
更新(Update)
-
視圖(View)
模型是一種代表整個應用程序狀態的常量值,更新是一種獲取當前模型和消息(消息是一種描述模型所期望變化的常量值)并返回修訂后模型的函數,視圖是一種接受當前模型并返回所需DOM結構表征的函數。
Elm的運行時通過連接這三種部件即可組成響應式應用程序。當用戶點擊一個按鈕后,只允許出現一個結果:向更新函數發送一條消息。這樣就可以在相關內容之間實現良好的區分:所有應用程序邏輯均通過更新函數實現,所有渲染邏輯都通過視圖函數實現,所有應用程序狀態均存儲在模型中。
Elm架構可以通過一種簡單的方式實現模塊化。如果需要對一些渲染邏輯進行分隔,可以編寫另一個接受主視圖函數調用的視圖函數。如果模型變得大到離譜,可以建立一個更小的模型并將其嵌入主模型中。如果需要一個自行管理自己狀態的獨立組件,可以為其創建模型、視圖和更新,并將其以子“對象”的方式委派給父模型、視圖和更新。
這種從根本上進行簡化的架構需要一定的適應過程,但隨著代碼基規模的擴大,這種方法可以確保一切井然有序。無論調用多少幫助(Helper)函數,所有應用程序狀態均能嵌套在主模型中,所有應用程序邏輯均能嵌套在主更新函數中,所有渲染邏輯均能嵌套在主視圖函數中。
對全局事件的響應也變得更簡單。編寫一個可以查詢當前模型的訂閱函數,確定應用程序要訂閱的事件(從鍵盤按鍵到WebSocket接收到數據,一切事件均可訂閱),隨后將這些事件轉換為消息并發送給更新函數。
在模型、視圖和更新中使用這種集中化的邏輯,意味著Elm的響應性只產生很少量的清理工作。我們可以創建、重配置,或移除事件偵聽器和可觀察對象(Observable),這意味著需要追蹤更多內容。但Elm架構中無需追蹤這些,只需要通過可選的訂閱函數將消息發送至為onClick等DOM事件始終使用的同一個更新函數即可。
相比其他可編譯為JavaScript的語言,例如CoffeeScript、Dart,以及ClojureScript,Elm最大的不同不僅在于增加的功能,還在于舍棄的功能。這種模型-視圖-更新架構并不是Elm建議的做法,而是編寫應用程序的唯一做法!這意味著Elm包倉庫中的每個庫均圍繞這一想法構建,再也不需要決定備選的渲染策略該如何選擇。開發者只需要通過一種能得到極為完善支持的方法完成自己的工作。
托管的作用
在響應式JavaScript庫文檔中,最常見的一個警告通常是“別在這里產生副作用。”Elm并不需要這樣的警告,因為Elm只支持托管的作用(Managed effect),不會產生副作用,而托管的作用不會產生類似副作用導致的問題。
在托管作用系統中不需要立刻執行作用,而是需要描述希望用數據做些什么。Flux存儲所發起調用產生的副作用可能會立刻發起HTTP請求,而在Elm的更新函數中,除了返回常規的模型更新,還可以返回所要進行的HTTP請求對應的描述。Elm的運行時會負責將這些有關HTTP請求的描述轉換為實際的HTTP請求。
此處最重要的差異在于,在托管作用系統中,API可以可靠地強制指定哪些函數可以返回作用的描述。例如更新函數可以返回新模型以及曾完成的任何作用的描述...但視圖函數只能返回自己需要的DOM的描述。非預期的副作用可在API層面上徹底排除!
更棒的是,托管作用很好地解決了響應式JavaScript領域一些常見問題的一致性。一些API使用Promise,另一些使用回調(Callback)事件,同步方面還有其他副作用...但在Elm中這些都只是任務(Task)。
任務類似于回調,其本身的實例化(Instantiating)是無害的,我們可以對數百個描述HTTP請求的任務進行實例化,但此時不會產生任何網絡活動。一旦將任務從一個函數傳遞至另一個函數并最終交給Elm運行時,隨后才會執行這些任務。任務與Promise類似,可以鏈在一起,并包含了類似的一類錯誤處理機制,如果鏈中任何任務失敗,其余任務將不被執行,整個鏈會處于這個失敗值的狀態下。
圍繞Promise有一個常見痛點:會產生吞咽(Swallow)異常。Elm的任務不會遇到這種問題,因為Elm中完全沒有任何異常!如果某個作用可以失敗,唯一的失敗方法是借助任務內建的失敗處理機制。Elm沒有類似Try/catch的機制,沒有類似Throw的機制,只有任務。
Elm可以實現這一特性是因為所有作用都是以任務的形式實現的,而這也意味著所有失敗的作用必然會使用任務的錯誤處理系統。對比而言,JavaScript的錯誤處理會包含各種例外:被拒絕的Promise,以及有時候可能傳遞給回調的錯誤參數。Elm有關作用的一致性意味著不存在類似沖突失敗機制等問題,例如異常和被拒絕的Promise。
以編譯器為后盾
“只要能編譯,通常就能正常工作”,這種說法對Elm程序員已經很熟悉了。
產生這種說法并不是因為Elm的編譯器具有奇跡般的除錯能力,而是因為該編譯器可以強制確保整個架構盡量簡單。語言級響應性,以及使用托管的作用代替副作用,這些特性可以消除大量可能導致出錯的因素,其余“漏網之魚”通常可以通過編譯器提早發現并解決。
例如拼寫錯誤的字段名就是一種常見錯誤。假設打算輸入phoneNumber但無意中輸入了phoenNumber,此時也許會看到類似這樣的錯誤信息:
最終用戶絕對不會受到這個Bug的影響,因為該錯誤只會出現在編譯時。更棒的是,開發者完全不需要通過調用堆棧的方式回溯并調試,最后才收到一條有關“phoneNumber未定義”之類的錯誤信息。Elm的編譯器不僅可以提前發現這種問題,甚至可以直接標出有問題的代碼行數。
這種體驗最棒的地方在于能夠為代碼重構工作起到的作用。對整個代碼基進行大量大規模的變更不可避免會導致編譯器錯誤(畢竟程序員總會犯各種錯誤),但在解決編譯器錯誤時...正如那句話所說,“通常就能正常工作”。這一點真正讓人感覺耳目一新!所有“未定義”都是函數,都是可以正常工作不會崩潰的代碼。
通過這種方式開發者還可以免費獲得全面的測試能力。Elm的編譯器可以自動驗證所有組件是否以合理的方式連接在一起,使得開發者無需自行開發其他能實現與Elm編譯器同等程度的預防性測試方法。開發者編寫的測試數量更少,但代碼變得更可靠。
更棒的是, Elm的包管理器 可以感知這些保證,并使用這些保證 強制實施自動化的語義版本控制 。如果任何人試圖發布包含破壞性API變更的包,包管理器會拒絕發布,除非變更包含到主版本號的Bump。如果想了解任何包的任何兩個版本之間API的差異,可以運行類似elm-package diff NoRedInk/elm-rails 2.0.0 3.0.0這樣的命令,借此查看2.0.0和3.0.0版包之間的變化。
Elm的 JavaScript互操作系統 按照設計可以維持這些保證。此時并不需要與JavaScript共享(可能非常易于崩潰的)代碼,Elm應用程序可以用類似于與服務器或Web工作進程通信的方式與JavaScript代碼通信:往返發送數據。唯一的差別在于:并不需要通過網絡或Web工作進程傳輸數據,Elm會與其他語言傳輸數據。
很多團隊會談到“Elm領域”和“JavaScript領域”,這兩種代碼基應用了不同的規則。在Elm領域開發者可以很放松并確信Elm編譯器可以防止代碼崩潰。在JavaScript領域就不那么確信了,開發者在內心深處會深信不疑地覺得代碼中某處存在著測試未發現的忘掉的Null檢查。
通過保持這兩個領域相互獨立,僅通過數據進行通信,Elm在提供最佳響應式編程體驗的同時使得開發者能夠繼續從龐大的JavaScript庫生態系統獲益。
總結
在目前可供Web開發者選擇的各種可用選項中,Elm對響應性提供了最全面的支持:從語言本身實現了響應性。除了在設計上可以幫助開發者從最基礎的響應性設計中獲得最大化收益的編譯器外,Elm還為開發者提供了:
- 運行時零異常成為常態—“只要能編譯,通常就能正常工作。”
- 通過語言提供一類支持,簡單的應用程序架構。
- 在鏈式作用和錯誤處理過程中使用一致、令人愉悅的API。
- 無需擔心有問題的副作用影響響應性。
- 實用的編譯時錯誤信息幫助減少測試數量的同時實現更可靠的代碼。
Elm依然是一種相對較新的語言,但 NoRedInk 、 Prezi 、 Futurice 、 Gizra 、 CircuitHub 等很多公司已經開始使用這種語言開發生產應用程序。
如果想要進一步了解Elm,可以參考下列資源:
-
Elm簡介 – Elm創造者Evan Czaplicki提供的指南
-
使用Elm構建可實時驗證的注冊表單 – 針對JavaScript程序員提供的教程
-
重新思索所有實踐:用Elm構建應用程序 - ReactConf 訪談
-
讓后端團隊嫉妒:Elm在生產中的應用 - Strange Loop訪談
-
Elm in Action ,Manning Publications 即將出版的圖書
來自:http://www.infoq.com/cn/articles/language-reactivity-with-elm