在 Rails 下模組化整理 CoffeeScript

在 Rails 下面寫 JavaScript 跟 CSS 很方便,因為你可以把 .js.css 以模組化的方式拆成很多小檔案、然後以階層目錄之類的方式來整理檔案。反正最終上 Production 後、AssetPipeline 會幫你把眾多小檔案,以你指定的順序結合成一包大檔案一起載入──例如 application.jsapplication.css

對 CSS 來說這樣整理的方式沒有問題,因為 CSS rules 是只要宣告過就會留在那邊;只有先後順序跟優先層級問題,沒有跨檔案問題。對純 JavaScript 來說其實也蠻好解決,例如 CSS-Tricks 的《How Do You Structure JavaScript? The Module Pattern Edition》 就建議你可以把 script 以模組化的方式包成一個個物件、就可以一個模組一個檔案拆下去整理;最後再依序包回來使用即可。CSS-Tricks 的範例大概如下:

這樣的方法很方便、而且可以把 JS 檔案整理得很漂亮。但是如果你寫的是 CoffeeScript 就麻煩了。

CoffeeScript 拆成不同檔案的麻煩

雖然在 CoffeeScript 下面因為寫 class (相對)非常容易,所以把 code 模組化成各種 class 是最方便的方法;但是問題就出在 CoffeeScript 每個檔案 compile 完出來的 code,都是放在一個立即執行的匿名函數 (anonymous function) 裡面。這個優點在於你的所有變數,會被限制在匿名函數的 scope 裡面而不會向外污染、壞處就在於當你想採取上面 CSS-Trick 的那個模組化整理方法時,你會發現:每個 class 都因為 scope 的關係被限制在個別檔案裡面,根本無法互相溝通。

平常專案如果 script 沒有用 class 用很兇、都只是用 jQuery 在處理頁面元件的話,scope 基本上也不是什麼問題──反正 jQuery 本身就被註冊為全域變數了,一定拿得到。第一次深受其害是在做 Logdown 的時候。Logdown 的編輯器非常倚賴 JavaScript,所以我一開始的時候把它不同部位做成不同 class、最外面再用一個總 class 去管理下面的不同部位。隨著 script 越寫越長、整份 editor.coffee 也變得越來越難讀(因為實在太大了)。等到開始想把他依照模組化的方式,拆成不同小檔案出去的時候,就遇到上一段的窘境了:姑且稱為「scope 陷阱」好了。

解決「Scope 陷阱」的初步嘗試

事已至此,眼前擺著兩個相當直覺的解法可以考慮:

  1. 為了 scope 大局著想,裝死繼續把所有會用到的 class 全部包在同一個 .coffee 裡面。
  2. 那就打破 scope,要求 CoffeeScript 在 compile 的時候不要包匿名函數吧!

直覺是都很直覺,可惜其實蠻掩耳盜鈴的。1 的話根本就什麼也沒做、2 的話則是會導致全域變數一瞬間被倒入很多 class。本來不想污染 global 的、這下可全部都倒進去了。看來只好使用密傳的方法 3 了:

(圖片來源:國立故宮博物院)

最後解法:在全域掛一個 Application Namespace

最後我想到:既然平常引入比較大的外部 Library 其實也都會至少多掛一個全域變數(像 jQuery 就一次註冊了 jQuery 兩個),那我整個專案也註冊一個全域變數應該也不會很過分吧。我只要在一開始先在全域下面掛一個 namespace、之後每個 class 都掛在 namespace 下面就好了。範例大概如下:

這樣就可以突破 scope、在 CoffeeSript 之間跨檔案取用 class;可以依然用模組化的方式去拆開跟整理 .coffee 檔案,又不會過度污染 global 了。

comments powered by Disqus