為什麼 Node.js 不適合大型和商業專案?

JavaScript 和 Node.js 一直都是這幾年的話題,無論是前端還是後端,到處都可見 JavaScript,就好像爬滿了你全身上下,他們不斷地對你說道「嘿!老兄!快來用我吧!」。

為什麼 Node.js 會這麼夯?主要是因為效能快過於 PHP 和 Python 與 Ruby,寫法簡單又容易,而且前後端能夠使用同個語言當然是最好不過了。不過當然也有人簡單就是因為「潮」(嗨!歡迎加入潮流式驅動開發)。

但⋯⋯如果你只是因為這些原因而選用 Node.js,你可以考慮轉往其他語言了(甚至回到 PHP 和 Python 的懷抱),為什麼?這裡有幾點你需要考慮。

毫無結構性

事實上,Node.js 很適合小型和輕量級專案,但當結構越來越龐大的時候,你會遇上這件鳥事。沒錯,Node.js 是弱型態語言,這也令 Node.js 暴露在不具結構性的問題下,令開發者難以掌控物件所擁有的屬性為何、函式會回傳什麼內容。

就算你的編輯器支援了型態提示,多數情況下他們都是 any(任意),這根本沒有幫助。

說到這裡,你可能會想起 TypeScript,但那只是「一時之選」,用上 TypeScript 會使你的專案變得更加複雜,但結構變得更明確了。然而效能還是 Node.js,那麼為什麼不直接轉換到強型態語言呢?

原因很簡單,因為 Node.js 的生態完善,是其他語言所沒有的,但除此之外呢?沒了。

型態難以判斷

如果你曾去面試 Node.js 相關職位的工作,你可能會對 undefined == null 這個問題不陌生,而其答案是 true

沒錯,在 JavaScript 裡面你永遠無法預料兩個形態是否相等。當然,在你有不少採坑經歷之後這對你來說可能已經不成問題了。

在 CodeMash 2012 中 Gary Bernhardt 所演講的「Wat」影片透過有趣的方式點出了 JavaScript 的型態是很神奇的一件事情。

然後如果你不知道,JavaScript 還有 Set 跟 Map 型態

文件、註釋不易撰寫

撰寫 Node.js 的文件與註釋是個難題,比起文件,更多專案擁有的是 README,這讓初心者更好上手,但進階開發者則需要花上更多時間查找自己所需的函式。

至於什麼叫做註釋難以撰寫?通常撰寫 JavaScript 註釋的時候會遵循 JSDoc(或 APIDoc)的方式,像下面這樣。

/**
 * Hello 會透過 WebSocket 並向使用者傳遞指定的打招呼訊息。
 *
 * @param  {String} greeting - 欲發送的打招呼訊息。
 * @return {Boolean} 傳送是否成功。
 */

function Hello(greeting) {  
    // ...
    return true
}

看起來不成問題,對吧?很有條理,又很明確,然而在幾個星期後你就會發現不斷地標記函式的參數、回傳值是多麽累人的事情,過了不久,註釋就會跟不上函式真正的功能,當然你能說這是取決於工程師自己的怠惰,更多能說的是在 JavaScript 中,註釋其實沒有這麼好用。

設想一下你的函式會回傳一個物件,你可以保持勤勞,寫成這樣。

/**
 * @typedef  {Object} User
 * @property {String} nickname 暱稱。
 * @property {String} username 帳號。
 * @property {String} password 密碼。
 */
/**
 * Foo 會建立一個新的使用者並且回傳一個註冊後的使用者物件。
 * 
 * @param  {User} 來自表單的使用者物件。 
 * @return {User} 註冊後的使用者物件。
 */

function Foo(user) {  
    return {
        nickname: "",
        username: "",
        password: "",
    }
}

然後你就可以開始回想起你到底是在哪個文件中定義 User 這個物件了。所以更多時候我會直接打上 @return {Object} User 物件 然後叫其他開發者自己去看統一集中的文件,因為那樣還更好管理物件結構。

可用函式跟不上時代

如果你是個標準的 Node.js 工程師,你就會常常去看 Node Green,因為你永遠不知道 Node.js 到底支援了這個函式沒有。然而你會看到一堆紅色的錯誤表明尚未支援。

接著你就會抱頭痛哭,沒過多久,所有的專案都會直接使用 Babel,這能將尚未支援的函式轉換成相容舊 Node.js 的程式碼,但令程式更長。

套件佔了空間一大半

Node.js 的生態真的很好,然後你就會用上一堆 npm 裡的套件,接著發現 node_modules 資料夾比起你的專案大概多用了二十幾倍的空間。

如果你有兩個專案,還會因為版本相容性的問題而多上另一個 node_modules 資料夾。順帶一提,現在有這麼多的 Electron 軟體,每個軟體都有 node_modules⋯⋯佔用了 100 MB 的空間但實際內容其實只有 10 MB。

額外一點,由於 Node.js 不是編譯式語言,所以當你需要部署 Node.js 專案到伺服器的時候,你還需要執行 npm install(或 yarn install)下載你專案所會用到的所有套件,這會額外花上兩分鐘左右的時間,更別說做持續整合測試了。

回呼地獄

這個問題存在很久了,回呼地獄(Callback Hell)在以前一直是個問題,你可能回想說,這不是早就被解決了嗎,像是透過 Q.jsAsync.js?對,但這還沒完。

回想起這個問題的歷史,一直到 jQuery 推出了 Deferred 才被解決,然後最終被一個 Promise 取代。但在那之後人們不滿又推出了基於 Promise 的 Async/Await,接著你就會發現因為有的人腦袋還跟不上,所以一個專案同時出現了 Callback 還有 Promise 跟 Async/Await。

Node.js 推出的新功能一直以來都是用膠帶在填補以往的坑,除非像 Python 2 到 Python 3 那樣的大轉變,否則這些坑可能都還會在吧。

多工異步麻煩

Node.js 預設就是一個程序只使用單個核心(這大概也是為什麼叫做 Node 的原因),不是很多人都有機會用到 Node.js 中的 Cluster 模塊,一但接觸之後,你就要開始區分 Master 跟 Worker 然後他們是怎麼互相溝通與聯繫的。

到目前為止我對 Node.js 如何發出多工請求、最終取得「所有多工的結果」還不是很清楚。但我能知道的是⋯⋯如果你會用上 Cluster,那麼你就會需要在初期時花上更多時間來有個周詳的規劃(就像規劃微服務那樣)。

後記與逃離 Node.js

其實原本還要寫上 Node.js 在測試方面上的問題,主要是因為 Node.js 沒有內建良好的測試工具與環境,導致需要額外花費時間處理像 Mocha 等⋯的設置問題,不過因為看了看發現已經寫夠多文字了,我想應該也不需要額外補上這一點啦⋯⋯。

不過這也不代表 Node.js 是完全不需要的,畢竟用在客戶端上還是十分地方便(例如 Webpack)。如果你正在考慮在後端使用另一個語言的話,不妨看看比起 Node.js 還要更快而且簡短的語言:Go。

Go 所內建的工具和環境令開發與測試十分容易,而且效能與部署也極快,撰寫 WebSocket 聊天室和 Node.js 相同,十幾行就能搞定了。如果你有興趣學習 Node.js 或者是其他語言(而且比較偏向 Web 服務的話),可以參考看看。