Elm 語言開發微信小程式
由於工作需要,最近進行了一些目前很熱門的微信小程式開發,技術選型的過程和結果都有些值得分享的體會,嘗試做個簡要的介紹。
先說結果,核心的邏輯採用了 Elm 語言開發,編譯到 JavaScript ,介面顯示還是標準的 JavaScript 和 WXML。
Elm 是什麼?
官網的簡介:
A delightful language for reliable webapps. Generate JavaScript with great performance and no runtime exceptions.
翻譯成中文大約是:
一門開發網頁應用的令人愉悅的語言,生成高效能、沒有執行時例外的 JavaScript 程式碼
過去兩週,寫了大約 5000 行的 Elm 程式碼,感覺上面的描述還是挺靠譜的,和手寫 JavaScript 相比,確實令人愉悅。下面簡單分析下技術上 Elm 是如何做到的。
(這篇文章以概念和經驗介紹為主,就基本不上程式碼了,以後儘量有後續的詳細介紹)
強型別,靜態型別的編譯語言
個人認為這是 Elm 和 JavaScript 最大的區別,JavaScript 不會對程式碼訪問的資料做任何的型別檢查,只有實際執行後才知道結果會怎樣,可能會出現空指標,未定義變數,資料型別、格式不匹配等等各種問題,而且測試執行通過也不代表今後的執行還是正確的,因為將來的輸入資料可能會有變化。
常見的解決方法有資料檢查、程式碼檢查工具(例如 Facebook FlowType)、程式碼擴充套件(例如微軟的 TypeScript)等等。基本上都是補丁的方式,而且並非強制的,不能徹底解決問題。
作為編譯語言, Elm 需要定義資料、函式的型別(也支援自動型別推定),會在編譯時進行檢查,只有所有的函式呼叫完全符合所宣告的型別時才能編譯通過。而且 Elm 中沒有空指標的概念,對於可能為空的情況必須明確宣告,並做相應的處理。
函式式語言,不可變資料
函式式其實是個歷史悠久的概念,不過由於各種歷史原因,目前的主流語言大多以物件導向為核心,導致很多人(包括我自己)都對函式式語言不瞭解,並且常常會有很難學,很難用的印象。數年之前用 XMonad 作為主力視窗管理器(現在偶爾也還會用),配置檔案需要用 Haskell 寫,在沒學語法的情況下參考其他人的例子配了個相當滿意的配置,一直想認真學一下,不過一直也沒抽出時間來。
其實如果把物件看成資料結構和運算元據結構的方法的結合,在直觀的層面上和函式式的方式並沒有本質的區別,像是 C# 裡面的 Extension 就是應用了語法糖的方式,偽裝成成員方法的外在函式。
而不可變資料才是讓函數語言程式設計截然不同的原因,如果還是以物件的眼光來看待的話,可以理解成每次對於物件的修改都產生了一個獨立的新物件,它們之間完全隔離,彼此沒有任何影響。隨之而來的各種好處是巨大的,例如對於併發的處理,快取的處理等等。
另一個重要的特性就是高階函式、閉包,雖然現在的主流語言基本上也都提供了支援,也很大程度上改善了語言的描述性,但在離開了不可變的情況下,並不能提供同樣的強大支援。
Elm 架構
Elm 架構是構架在語言層面之上的系統組織形式,也有點像是 Elm 中的入口(main 函式),獨到之處在於它是完整的執行週期管理,並且在 Elm 中,似乎沒有其它的方式,只能以這一模式執行,貌似很死板,實際用起來還很適用。
如果你對於 Flux,Redux,有過了解的話,基本上也已經瞭解 Elm 架構了,它們的設計都受到了 Elm 的很大影響,基於同樣的理念。
為了避免這篇文章太長而無法完成,就不詳細介紹了,具體的細節可以參考官方的入門文件。
成果與心得
雖然 Evan Czaplicki (Elm 作者)非常強調 Elm 對初學者的友好,也花了不少精力提供了不錯的文件和工具,但是真正把一門新語言應用到實際專案中始終都是個挑戰,另外微信小程式與標準的 Web 開發也有不少區別,需要額外的時間和精力。
語法和類庫的層面就不提了,語法寫習慣了就好,核心的類庫還是挺小的,文件也很清晰,就是往往沒有明確的示例,需要一些試驗才能真正理解,在開發週期比較緊的時候,壓力還是很大的。
不過在這次的經驗上,Elm 從入門到達到相對高效的狀態比想象的要快,感覺設計思路上非常清晰,對於設計場景很適合。我自身的方面是各方面開發經驗和接觸過的語言還算多,函式式語言之前有過不到兩個月的 Erlang 經驗,多年的習慣是 Vim 開發,打日誌除錯為主,對 IDE 沒太大需求。
柯里化(Currying)以及管道操作符
insertInt : String -> Int -> DataDict -> DataDict
insertInt key val =
Dict.insert key (Json.Encode.int val)
這是一個極其簡單的函式的宣告,第一行是型別的定義,一堆箭頭,讓人有點暈,如果描述一下的話,版本 A 是這樣的:
- insertInt 是一個函式,有一個輸入,型別是 String,輸出型別是函式 insertInt_A1
- insertInt_A1 也是一個函式,有一個輸入,型別是 Int,輸出型別是函式 insertInt_A2
insertInt_A2 還是一個函式,有一個輸入,型別是 DataDict,輸出型別也是 DataDict
type alias Data = Json.Encode.Value type alias DataDict = (Dict.Dict String Data)
DataDict 就是一個字典,鍵的型別是 String,值的型別是一個 Json 資料
對於一個所有實現僅有一行的函式來說,還真是顯得有點過於複雜了,其實這還不算完,還有版本 B:
- insertInt 是一個函式,有兩個輸入,型別是 String 和 Int,輸出型別是函式 insertInt_B
- insertInt_B 也是一個函式,有一個輸入,型別是 DataDict,輸出型別也是 DataDict
或者是比較容易理解的版本 C:
- insertInt 是一個函式,有三個輸入,型別是 String,Int,DataDict,輸出型別是DataDict
那麼哪個是正確的版本呢,全都是,取決於使用的方式。定義的時候其實也是一樣,從程式碼上看比較像是版本 B,有兩個輸入引數,而你完全可以用版本 A 或是版本 C 的方式來使用。
一旦開始用這樣的眼光來看待多引數的函式,你會有一種發現了新世界的感覺,函式之間的重用非常方便,而實現起來又極為簡單。概念上這是屬於所謂柯里化(Currying)
的範疇,使用上需要一些經驗的積累,才能達到得心應手的狀態。
encode : Type -> Data
encode info =
empty
|> insertString "nickName" info.nickName
|> insertInt "gender" (Gender.encode info.gender)
|> insertString "city" info.city
|> insertString "province" info.province
|> insertString "country" info.country
|> insertString "avatarUrl" info.avatarUrl
|> dictToData
這段程式碼是用了上面定義的函式來生成一個 Json 資料的過程,其中的 |>
表示的是把之前的資料作為後面函式呼叫的後一個引數,在合適的情境下,會讓程式碼很清晰。
友情提示:用過 Elixir 的碼農注意了,Elixir 的管道符是變成第一個引數的,別弄混了。
另外,Elm 中還有其它幾個特殊符號:<|
,>>
,都很有用,這裡就不細說了,當然有時難免還是得加括號的。
單一行為的串聯
update : Msg -> Model -> (Model, Cmd Msg)
在對於一個事件做處理時,往往需要做多種操作,更新資料,傳送新訊息,執行外部訪問,等等,程式碼漸漸的就難以清晰的看出其中的意圖來了,開始時也困惑了一陣子,後來找到了 elm-update-extra 這個包,一下子就清楚了,其實核心的思想就是引入中間的環節,多個環節就可以連線起來了
op : (Model, Cmd Msg) -> (Model, Cmd Msg)
例如
(updateModel <| SocketModel.setOnline True)
>> (updateModel <| SocketModel.updateChannel Channel.onJoin)
>> (addCmd <| toCmd DoJoinChannel topic res)
就更新了資料模型中的兩個部分,並且傳送了一個新的訊息,有了這幾個簡單的函式(updateModel, addCmd, toCmd)的幫助,程式碼又變得很好讀了,強烈推薦。
- https://github.com/ccapndave/elm-update-extra
友情提示:如果實現上既帶進來了舊的 model,又利用了其它的環節,一定注意不要把 model 弄混,如果錯誤的把舊的值傳下去,會導致資料的丟失。
子模組的拆分和互動
文件中的示例是標準的 Todo 應用,邏輯很簡單,並不能完全解決實際應用的需求,個人體會最大的需求是更好的模組化,把不同部分的邏輯互相隔離,經過一些調研,選擇了 elm-component-updater 來支援模組化的組織,以及模組之間的互動,效果很滿意,強烈推薦。
- https://github.com/mpdairy/elm-component-updater
友情提示:一定化些時間把裡面的示例完全看懂,明白了以後概念是很清晰的,實際使用中也很靈活。
這個話題有點大,要說清楚得不小的篇幅,只能留到以後了。
對微信小程式 API 的封裝
微信提供的是 JavaScript 的介面,雖然文件還不錯,但並不能很好的與 Elm 相結合,在熟悉了 Elm 之後,就嘗試著做了一個封裝,效果很好,可以進行型別檢查,也完全是以 Elm 的方式在訪問相關的介面。
這部分目前只實現了用到的幾個介面,新增更多的介面實現上都比較簡單直接,在成熟的時候會開源出來。
和介面部分的結合
由於微信小程式並不提供 Dom 的訪問,Elm 中很強大的 Virtual Dom 並不能被用到,目前是在資料模型發生變化時傳送更新給 JavaScript 端,再呼叫 setData(),完成頁面渲染。
理想情況當然是能夠實現相容 Virtual Dom 的方式,不過技術上有一定的難度,目前還沒有很好的方案。另一方面目前的模式也還是很清晰的,JavaScript 只負責簡單的資料傳遞,修改請求也是用生成事件的方式回傳給 Elm 的,所以雖然不是最優,立刻修改的需求也並不強烈。
elm-css 的應用
雖然也做過些網頁相關的工作,不過基本上不具備 CSS 的技能,現在是個小團隊,也得自己寫寫,學了語法,寫起來還是有 JavaScript 的感覺,沒有編譯期的檢查,往往只能頻繁的嘗試,偶爾也會因為格式的問題(寫錯鍵值、單位等等)產生問題,當時如果沒發現,就成了隱患。
還好有其他人也有同樣的感覺,發現了 elm-css 這個用 Elm 寫 CSS 的工具,感覺其它的那些 CSS 工具都太弱了,所有的定義都有相應的型別,以及可接受的輸入,編譯期的檢查保證了只能生成有效的 CSS,對於程式設計師來說是最自然、高效的方式,強烈推薦。
- https://github.com/rtfeldman/elm-css
JavaScript 互操作
port modelOut : JsModel.Type -> Cmd msg
port msgIn : ((String, String, Params) -> msg) -> Sub msg
只有兩個介面,一個是把最新的資料模型傳給 JavaScript,一個是給 Elm 傳送訊息,其實也就夠用了。
比較麻煩的是這裡的 model 只能使用與 JavaScript 相容的 Elm 資料結構,像是 Union 就不能用,實際應用中是加了一層處理,把完整的 model 做了一次包裝,或者裁剪掉可以不用的部分,或是編碼成支援的格式,不是太完美,也增加了程式碼量,好在比較簡單直接,不會顯著降低程式碼質量。
最完美的方案是如果能解決介面部分的 Elm 化,就不需要這兩個介面了, 那麼相關聯的程式碼也都可以刪掉了。
令人愉悅的重構
過程中最讓人愉悅的部分大概是程式碼的演化與重構了,不論是邏輯關係從一對一改為一對多,改變模組的覆蓋功能,調整外部請求的流程,往往能比預期更快的完成,確實常常是編譯通過,一次執行就通過了,感覺上像是有了很多自動實現的單元測試,重構程式碼還不用重構測試,每次都感覺選擇 Elm 實在是太正確了,否則在 JavaScript 的世界裡,不知要花多少時間。
過程和思考
編碼時往往容易被問題帶著走,也常常會發現在用正確的方式解決著錯誤的問題,尤其是相對反常規的做法,更是會有隱藏的風險。
如何作出引入 Elm 的選擇
之前幾年都是以 C# 為主,對於 JavaScript 這樣的解釋型,動態弱型別語言不是很有興趣,以前公司的 Python 專案也遇到過不少測試沒能覆蓋,上線遇到"驚喜"的先例。
之前看過 Elm 的材料,沒實際做過專案,印象還是挺好的。
接到任務時的第一反應是照著教程用最簡單的方式儘快出個 Demo 就好了,一切都按照官方文件來,儘量不引入外部依賴。實際上手才發現沒那麼簡單,官方沒有提到任何對資料的管理方式,純手寫邏輯又太不可控,考慮是否引入 Redux 這樣的框架,以前 nodejs 用過的 async 庫比較大,引入了一個支援 waterfall 的 weachy,再加一個訊息轉發的 postal,附帶著又帶進來 lodash, 這樣下來依賴也越來越多,而且還是很重的拼湊的感覺,有入坑的預感。
於是用了一個週末的時間嘗試了 Elm 方案,效果出乎意料的好,依賴全部刪掉,重寫了部分核心功能,直覺上是個正確的方向,後來搞定了微信介面的封裝,又解決了行為串聯,和子模組組織的設計之後,開發效率開始上來了,質量上比之前的 JavaScript 版本則是質的提高。
前期磨合
全新的語言、架構,產生大量的細節問題,要解決、調研,如果是個純粹的練手專案,那麼有足夠的時間,而當前的專案又需要儘早上線,說實話壓力是很大的,每天都得加班加點,還好進展一直都有,大概在寫到第三、四天以後,體會到了視角的變化,感覺能從函式式的角度來理解系統了,之後就進入了比較順利的階段。
開發效率與體驗
這個其實確實不好衡量,例如一個非常熟練的 JavaScript 程式設計師,仍然很可能可以比我做的更快,去掉前期學習的部分,差距會更大。
自己的感覺是很不錯的,拋掉學習的成本,程式碼的增長還是很快的,尤其是質量很好,不會帶來複雜性失控的問題。
至於開發體驗的話,對我來說是近乎完美的體驗,在可預見的將來,我想都不會回到原生 JavaScript 開發的方式上去,而且也必定會更加深入的採用函式式的技術或方式進行開發。
潛在風險?
對於這一類編譯到 JavaScript 的語言來說,首要的問題是編譯器是否穩定可靠,如果不行的化除錯難度就太大了,Elm 的編譯器本身是用 Haskell 寫的,雖然是開源的,我也還沒有具體看過,到目前為止還沒有碰到過任何這方面的問題。
Elm 現在的版本是 0.18,並不會保證新版本的完全向後相容,像是之前 0.17 更新的時候把原本的 JavaScript 互操作方式改了,又把 RFP (Reactive Functional Programming)的部分做了較大的調整,社群裡也有些意見,有些包不更新的化,新的版本中也沒法用。我看到的是改的結果確實很好,遷移的難度也不大,還是利大於弊的狀態。
剩下的就是小眾選擇的通病了,難找人,難找資料,資訊基本都是英文的。相關的包也少的多,不過另一方面,像 Node.js 或者 Python 這麼多的包,要找到合用的也挺難的,做對比也費時費力,往往讓人很焦慮。
痛點與將來
上面主要是優點,也還是遺留了一些痛點,篇幅所限,就不展開了。
- JavaScript 互操作的限制
- 相似型別的程式碼重用
- 模式化的程式碼
- 完全替代介面端 JavaScript 和 WXML?
附錄
術語
- 不可變資料 Immutable Data
- 函式式語言 Functional Language
- 強型別 Strong Type
- 靜態型別 Static Type
- 柯里化 Currying
- 執行時例外 Runtime Exception
連結
相關文章
- Elm 語言初體驗
- 微信小程式開發微信小程式
- 微信小程式開發小記微信小程式
- 【小程式】微信小程式開發實踐微信小程式
- 【小程式】微信小程式開發準備微信小程式
- 微信小程式開發教程微信小程式
- 微信小程式開發2微信小程式
- 微信開發與小程式
- 快速開發微信小程式微信小程式
- 微信小程式開發總結微信小程式
- 開發微信小程式的作用微信小程式
- 微信小程式藍芽開發微信小程式藍芽
- 微信小程式雲開發6微信小程式
- 微信小程式開發神器-Grace微信小程式
- 微信小程式開發--『狗蛋TV』微信小程式
- 使用mpvue開發微信小程式Vue微信小程式
- 使用TypeScript開發微信小程式TypeScript微信小程式
- 微信小程式開發精講微信小程式
- Elm 0.14 釋出,函式式反應式程式語言函式
- 微信小程式開發之大神之路最全微信小程式開發教程(視訊+精品文章)微信小程式
- 【微信小程式開發】梔子手作花花微信小程式商城開發最佳實踐微信小程式
- 微信小程式--聊天室小程式(雲開發)微信小程式
- 微信小程式之-NBA線上直播小程式開發微信小程式
- 微信小程式開發風口下,微信小程式該如何運營?微信小程式
- 微信小程式開發系列二:微信小程式的檢視設計微信小程式
- 微信小程式開發系列教程三:微信小程式的除錯方法微信小程式除錯
- 微信小程式開發系列七:微信小程式的頁面跳轉微信小程式
- 微信開發必備工具 php和java開發語言PHPJava
- 微信小程式開發(持續更新)微信小程式
- 原生微信小程式開發記錄微信小程式
- 微信小程式開發注意事項微信小程式
- 微信小程式---快速上手雲開發微信小程式
- 開發微信小程式的個人感想微信小程式
- 微信小程式開發初體驗微信小程式
- 微信小程式學習:雲開發微信小程式
- 快速學會開發微信小程式微信小程式
- 微信小程式雲開發如何上手微信小程式
- java-微信小程式開發工具Java微信小程式