從Sun離職後,我“拋棄”了Java,擁抱JavaScript和Node

作者|David Herron

譯者|無明

編輯|覃雲

來源 | 前端之巔

我是前 Sun 公司 Java SE 團隊的一名成員,在工作了 10 多年之後——2009 年 1 月——也就是在甲骨文收購 Sun 公司之前,我離開了公司,然後迷上了 Node.js。

我對 Node.js 的癡迷到了怎樣的程度?自 2010 年以來,我撰寫了大量有關 Node.js 編程的文章,出版了四本與 Node.js 開發有關的書籍,以及與 Node.js 編程有關的其他書籍和衆多教程。

在 Sun 公司工作期間,我相信 Java 就是一切。我在 JavaONE 上發表演講,共同開發了 java.awt.Robot 類,組織 Mustang 迴歸競賽(Java 1.6 版本的漏洞發現競賽),協助推出了“Java 發行許可”,這在後來的 OpenJDK 項目啓動過程中起到了一定的作用。我在 java.net(這個網站現已解散)上每週寫一到兩篇博文,討論 Java 生態系統中所發生的主要事件,並堅持了 6 年。這些博文的主要主題是關於“保衛”Java,因爲總有人在預言 Java 的“死期”。

在這篇文章中,我將會解釋我這個 Java 死忠是如何變成一個 Node.js 和 JavaScript 死忠的。

但其實我並沒有完全脫離 Java。在過去的三年中,我編寫了大量 Java/Spring/Hibernate 代碼。但兩年的 Spring 編碼經歷讓我明白了一個道理:隱藏複雜性並不會帶來簡單性,它只會產生更多的複雜性。

Java 是負擔,Node.js 充滿了樂趣

有些工具是設計師花費數年磨礪和精煉的結果。他們嘗試不同的想法,去掉不必要的屬性,最終得到一個只帶有恰到好處屬性的工具。這些工具的簡潔性甚至達到讓人感到驚豔的程度,但 Java 顯然不屬於這一類。

Spring 是一個非常流行的用於開發 Java Web 應用程序的框架。Spring(特別是 Spring Boot)的核心目的是成爲一個易於使用的預配置的 Java EE 棧。Spring 程序員不需要直接接觸 Servlet、數據持久化、應用程序服務器就可以獲得一個完整的系統。Spring 框架負責處理所有這些細節,你只需要把精力放在業務編碼上。例如,JPA Repository 類爲“findUserByFirstName”方法合成數據庫查詢——你不需要編寫任何查詢代碼,只需按照特定方式給方法命名,並添加到 Repository 中即可,Spring 將負責處理其餘的部分。

這原本是一個偉大的故事,一種很好的體驗,但其實並不然。

當你遇到 Hibernate 的 PersistentObjectException 時,你知道是哪裏出了問題嗎?你可能花了幾天時間才找到問題所在,導致這個異常的原因是發給 REST 端點的 JSON 消息裏帶有 ID 字段。Hibernate 想要自己控制 ID 值,所以拋出了這個令人感到困惑的異常。看,這就是過度簡化所帶來的惡果。除了這個,還有其他成千上萬個同樣令人感到困惑的異常。在 Spring 棧中,一個子系統套着另一個子系統,它們坐等你犯錯,然後再拋出應用程序崩潰異常來懲罰你。

然後,你會看到滿屏的堆棧跟蹤信息,裏面滿是這樣那樣的抽象方法。面對這種級別的抽象,顯然需要更多的邏輯才能找到你想要的內容。如此多的堆棧跟蹤信息不一定是不好的,但它也是在提醒我們:這在內存和性能方面的開銷究竟有多大?

而零代碼的“findUserByFirstName”方法又是如何被執行的?Spring 框架必須解析方法名稱,猜測程序員的意圖,構造類似抽象語法樹的東西,生成一些 SQL 語句……那麼完成這個過程需要多少開銷?

在反反覆覆經歷這樣的過程之後,在花了大量時間學習你本不該學習的東西之後,你可能會得出相同的結論:隱藏複雜性並不會帶來簡單性,它只會產生更多的複雜性。

Node.js 是清流

Spring 和 Java EE 非常複雜,而 Node.js 卻是一股清流。首先是 Ryan Dahl 在覈心 Node.js 平臺上所應用的設計美學。他追求別樣的東西,花了數年時間磨練和改進了一系列核心的 Node.js 設計理想,最終得到一個輕量級的單線程系統。它巧妙地利用了 JavaScript 匿名函數進行異步回調,成爲一個實現了異步機制的運行時庫。

然後是 JavaScript 語言本身。JavaScript 程序員似乎更喜歡無樣板的代碼,這樣他們的意圖才能發揮作用。

我們可以通過實現監聽器的例子來說明 Java 和 JavaScript 之間的差別。在 Java 中,監聽器需要實現抽象接口,還需要指定很多囉七八嗦的細節。程序員的意圖的這些繁瑣的樣板中漸漸淹沒。

而在 JavaScript 中,可以使用最簡單的匿名函數——閉包。你不需要實現什麼抽象接口,只需要編寫所需的代碼,沒有多餘的樣板。

大多數編程語言都試圖掩蓋程序員的意圖,這讓理解代碼變得更加困難。

但在 Node.js 中有一點需要注意:回調地獄。

沒有完美的解決方案

在 JavaScript 中,我們一直難以解決兩個與異步相關的問題。一個是 Node.js 中被稱爲“回調地獄”的東西。我們很容易就掉入深層嵌套回調函數的陷阱,每個嵌套都會使代碼複雜化,讓錯誤和結果的處理變得更加困難。但 JavaScript 語言並沒有爲程序員提供正確表達異步執行的方式。

於是,出現了一些第三方庫,它們承諾可以簡化異步執行。這是另一個通過隱藏複雜性帶來更多複雜性的例子。

const async = require(‘async’);

const fs = require(‘fs’);

const cat = function(filez, fini) {

async.eachSeries(filez, function(filenm, next) {

fs.readFile(filenm, ‘utf8’, function(err, data) {

if (err) return next(err);

process.stdout.write(data, ‘utf8’, function(err) {

if (err) next(err);

else next();

});

});

},

function(err) {

if (err) fini(err);

else fini();

});

};

cat(process.argv.slice(2), function(err) {

if (err) console.error(err.stack);

});

這是個模仿 Unix cat 命令的例子。async 庫非常適合用於簡化異步執行順序,但同時也引入了一堆模板代碼,從而模糊了程序員的意圖。

這裏實際上包含了一個循環,只是沒有使用循環語句和自然的循環結構。此外,錯誤和結果的處理邏輯被放在了回調函數內。在 Node.js 採用 ES 2015 和 ES 2016 之前,我們只能做到這些。

Node.js 10.x 中,等價的代碼是這樣的:

const fs = require(‘fs’).promises;

async function cat(filenmz) {

for (var filenm of filenmz) {

let data = await fs.readFile(filenm, ‘utf8’);

await new Promise((resolve, reject) => {

process.stdout.write(data, ‘utf8’, (err) => {

if (err) reject(err);

else resolve();

});

});

}

}

cat(process.argv.slice(2)).catch(err => {

console.error(err.stack);

});

這段代碼使用 async/await 函數重寫了之前的邏輯。雖然異步邏輯是一樣的,但這次使用了普通的循環結構。錯誤和結果的處理也顯得很自然。這樣的代碼更容易閱讀,也更容易編碼,程序員的意圖也更容易被理解。

唯一的瑕疵是 process.stdout.write 沒有提供 Promise 接口,因此用在異步函數中時需要丟 Promise 進行包裝。

回調地獄問題並不是通過隱藏複雜性才得以解決的。相反,是語言和範式的演變解決了這個問題。通過使用 async 函數,我們的代碼變得更加美觀。

通過明確定義的類型和接口提升清晰度

當我還是 Java 的死忠時,我堅信嚴格的類型檢查對開發大型的應用程序來說是有百利而無一害的。那個時候,微服務的概念還沒有出現,也沒有 Docker,人們開發的都是單體應用。因爲 Java 具有嚴格的類型檢查,所以 Java 編譯器可以幫你避免很多錯誤——也就是說可以防止你編譯錯誤的代碼。

相比之下,JavaScript 的類型是鬆散。程序員不確定他們收到的對象是什麼類型,那麼程序員怎麼知道該怎麼處理這個對象?

但是,Java 的嚴格類型檢查同樣導致了大量樣板代碼。程序員經常需要進行類型轉換,或以其他方式確保一切都準確無誤。程序員需要花很時間確保類型是準確的,所以使用更多的樣板代碼,希望通過及早捕獲和修復錯誤來節省時間。

程序員不得不使用複雜的大型 IDE,僅僅使用簡單的編輯器是不行的。IDE 爲 Java 程序員提供了一些下拉列表,用於顯示類的可用字段、描述方法的參數,幫助他們構建新的類和進行重構。

然後,你還得使用 Maven……

在 JavaScript 中,不需要聲明變量的類型,所以通常不需要進行類型轉換。因此,代碼更易於閱讀,但可能會出現未編譯錯誤。

這一點會讓你更喜歡 Java 還是痛恨 Java,取決於你自己。十年前,我認爲 Java 的類型系統值得我們花費額外的時間,因爲這樣可以獲得更多的確定性。但在今天,我認爲代價太大了,使用 JavaScript 會要簡單得多。

使用易於測試的小模塊來掃除 bug

Node.js 鼓勵程序員將程序劃分爲小單元,也就是模塊。模塊雖小,卻能從一定程度上解決剛剛提到的問題。

一個模塊應該具備以下特點:

  • 自包含——將相關代碼打包到一個單元中;
  • 強壯的邊界——模塊內部的代碼可以防止外部代碼入侵;
  • 顯式導出——默認情況下,代碼和模塊中的數據不會導出,只將選定的函數和數據暴露給外部;
  • 顯式導入——聲明它們依賴哪些模塊;
  • 可能是獨立的——可以將模塊公開發布到 npm 存儲庫或其他私有存儲庫,方便在應用程序之間共享;
  • 易於理解——更少的代碼意味着更容易理解模塊的用途;
  • 易於測試——小模塊可以輕鬆進行單元測試。

所有這些特點組合在一起,讓 Node.js 模塊更容易測試,並具有明確定義的範圍。

人們對 JavaScript 的恐懼源自它缺乏嚴格的類型檢查,所以可能很容易導致錯誤。但在具有清晰邊界的模塊中,受影響代碼被限於模塊內部。所以,大多數問題被安全地隱藏在模塊的邊界內。

鬆散類型問題的另一個解決方案是進行更多的測試。

你必須將節省下來的一部分時間(因爲編寫 JavaScript 代碼更容易)用在測試上。你的測試用例必須捕獲編譯器可能捕獲的錯誤。

對於那些想要在 JavaScript 中使用靜態檢查類型的人,可以考慮使用 TypeScript。我沒有使用 TypeScript,但聽說它很不錯。它與 JavaScript 兼容,同時提供了有用的類型檢查和其他特性。

但我們的重點是 Node.js 和 JavaScript。

包管理

一想起 Maven 我就頭大。據說一個人要麼愛它,要麼鄙視它,沒有第三種選擇。

問題是,Java 生態系統中並沒有一個核心的包管理系統。Maven 和 Gradle 其實也很不錯,但它們並不像 Node.js 的包管理系統那樣有用、可用和強大。

在 Node.js 世界中,有兩個優秀的包管理系統,首先是 npm 和 npm 存儲庫。

有了 npm,我們就相當於有了一個很好的模式用來描述包依賴性。依賴關係可以是嚴格的(指定具體的版本),或者使用通配符表示最新版本。Node.js 社區已經向 npm 存儲庫發佈了數十萬個包。

不僅僅是 Node.js 工程師,前端工程師也可以使用 npm 存儲庫。以前他們使用 Bower,現在 Bower 已被棄用,他們現在可以在 npm 存儲庫中找到所有可用的前端 JavaScript 庫。很多前端框架,如 Vue.js CLI 和 Webpack,都是基於 Node.js 開發的。

Node.js 的另一個包管理系統是 yarn,它也是從 npm 存儲庫中拉取包,並使用與 npm 相同的配置文件。yarn 的主要優點運行得更快。

性能

曾幾何時,Java 和 JavaScript 都因爲運行速度慢而橫遭指責。

它們都需要通過編譯器將源代碼轉換爲由虛擬機執行的字節碼。虛擬機通常會進一步將字節碼編譯爲本地代碼,並使用各種優化技術。

Java 和 JavaScript 都有很大的動機讓代碼運行得更快。在 Java 和 Node.js 中,動機就是讓服務器端代碼運行得更快。而在瀏覽器端,動機是獲得更好的客戶端應用程序性能。

甲骨文的 JDK 使用了 HotSpot,這是一個具有多種字節代碼編譯策略的超級虛擬機。HotSpot 經過高度優化,可以生成非常快的代碼。

至於 JavaScript,我們不禁在想:我們怎麼能期望在瀏覽器中運行的 JavaScript 代碼能夠實現複雜的應用程序?基於瀏覽器 JavaScript 實現辦公文檔處理套件似乎是件不可能實現的事情?是騾子是馬,拉出來溜溜就知道了。這篇文章是我用谷歌文檔寫的,它性能非常好。瀏覽器端 JavaScript 的性能每年都在飛漲。

Node.js 直接受益於這一趨勢,因爲它使用的是 Chrome 的 V8 引擎。

下面是 Peter Marshall 的演講視頻鏈接,他是谷歌的一名工程師,主要負責 V8 引擎的性能增強工作。他在視頻中描述了爲什麼 V8 引擎使用 Turbofan 虛擬機替換了 Crankshaft 虛擬機。

V8 引擎中的高性能 JavaScript:

https://youtu.be/YqOhBezMx1o

在機器學習領域,數據科學家通常使用 R 語言或 Python,因爲他們十分依賴快速數值計算。但由於各種原因,JavaScript 在這方面表現很差。不過,有人正在開發一種用於數值計算的標準 JavaScript 庫。

JavaScript 中的數值計算:

https://youtu.be/1ORaKEzlnys

另一個視頻演示瞭如何通過 TensorFlow.js 在 JavaScript 中使用 TensorFlow。它提供了一個類似於 TensorFlow Python 的 API,可以導入預訓練模型。它運行在瀏覽器中,可用於分析實時視頻,從中識別出經過訓練的對象。

基於 JavaScript 的機器學習:

https://youtu.be/YB-kfeNIPCE

在另一個演講視頻中,IBM 的 Chris Bailey 討論了 Node.js 的性能和可伸縮性問題,特別是在 Docker/Kubernetes 部署方面。他從一組基準測試開始,演示了 Node.js 在 I/O 吞吐量、應用程序啓動時間和內存佔用方面遠遠超過 Spring Boot。此外,得益於 V8 引擎的改進,Node.js 每次發佈的新版在性能方面都有顯著的提升。

Node.js 的性能和高度可伸縮的微服務:

https://youtu.be/Fbhhc4jtGW4

在上面的這個視頻中,Bailey 說我們不應該在 Node.js 中運行計算密集型的代碼。因爲 Node.js 採用了單線程模型,長時間運行計算密集型任務會導致事件阻塞。

如果 JavaScript 的改進還無法滿足你的應用程序的要求,還有其他兩種方法可以將本地代碼直接集成到 Node.js 中。最直接的方法是使用 Node.js 本地代碼模塊。Node.js 工具鏈中包含了 node-gyp,可用於處理與本地代碼模塊的鏈接。下面的視頻演示瞭如何集成 Rust 庫和 Node.js:

JavaScript 與 Rust 集成,遠比你想象得簡單:

https://youtu.be/Pfbw4YPrwf4

WebAssembly 可以將其他語言編譯爲運行速度非常快的 JavaScript 子集。WebAssembly 是一種可在 JavaScript 引擎內運行的可執行代碼的可移植格式。下面的視頻做了一個很好的概述,並演示瞭如何使用 WebAssembly 在 Node.js 中運行代碼。

在 NodeJS 中使用 WebAssembly:

https://youtu.be/hYrg3GNn1As

富 Internet 應用程序(RIA)

十年前,軟件行業一直熱議利用快速的 JavaScript 引擎實現富 Internet 應用程序,從而取代桌面應用程序。

這個故事實際上在二十多年前就已經開始了。Sun 公司和 Netscape 公司達成了共識,在 Netscape Navigator 中使用 Java 小程序(Applet)。

JavaScript 語言在某種程度上是作爲 Java 小程序的腳本語言而開發出來的。服務器端有 Java Servlet,客戶端有 Java Applet,這樣就可以在兩端使用同樣的一門編程語言。然而,由於各種原因,這種美好的願望並沒有實現。

十年前,JavaScript 開始變得足夠強大,可以實現複雜的應用程序。因此,RIA 被認爲是 Java 客戶端應用程序的終結者。

今天,我們開始看到 RIA 的想法得以實現。服務器端的 Node.js 和兩端都有的 JavaScript 讓這一切成爲可能。

當然,Java 作爲桌面應用程序平臺的消亡並不是因爲 JavaScript RIA,而是因爲 Sun 公司忽視了客戶端技術。Sun 公司把注意力放在要求快速服務器端性能的企業客戶身上。當時我還在 Sun 公司任職,我親眼看着這件事情發生。真正殺死 Applet 的是幾年前在 Java 插件和 Java Web Start 中發現的一個安全漏洞。這個漏洞導致全球一致呼籲停止使用 Java Applet 和 Java Web Start 應用程序。

我們仍然可以開發其他類型的 Java 桌面應用程序,NetBeans 和 Eclipse IDE 之間的競爭仍然存在。但是,Java 在這個領域工作是停滯不前的,除了開發工具之外,很少有基於 Java 的應用程序。

JavaFX 是個例外

10 年前,JavaFX 意欲成爲 Sun 公司對 iPhone 的反擊。它用於開發基於 Java 的手機 GUI 應用程序,想把 Flash 和 iOS 應用程序打垮。然而,這一切都沒有發生。JavaFX 現在仍然可以使用,但沒有了當初的喧囂。

這個領域的所有興奮點都發生在 React、Vue.js 和類似的框架上,JavaScript 和 Node.js 在很大程度上要得益於此。

結 論

現在,開發服務器端應用程序有很多選擇。我們不再侷限於“P”開頭的語言(Perl、PHP、Python)和 Java,我們還有 Node.js、Ruby、Haskell、Go、Rust 等等。

至於爲什麼我會轉向 Node.js,很明顯,我更喜歡在使用 Node.js 編程時的那種自由的感覺。Java 成了負擔,而 Node.js 沒有這樣的負擔。如果我再次拿起 Java,那肯定是因爲有人付了錢。

每個應用程序都有其真實需求。只是因爲個人喜歡而一直使用 Node.js 也不見得是對的。在選擇一門語言或一個框架時總歸是有技術方面的考量的。例如,我最近完成的一些工作涉及 XBRL 文檔,由於最好的 XBRL 庫是用 Python 實現的,所以就有必要學習 Python。

英文原文

https://blog.sourcerer.io/why-is-a-java-guy-so-excited-about-node-js-and-javascript-7cfc423efb44

相关文章