Skip to main content

Node.js終結者?青出於藍的Deno(一)

· 10 min read
戈頓
Developer & Co-founder of Tecky Academy

有編程經驗的人,都一定會聽聞過Node.jsNode.js基于ChromeV8引擎開發,本身能夠運行JavaScript,在前端開發(Frontend Development)、後端開發(Backend Development)、Android及iOS開發(Android & iOS Development),都有Node.js的蹤影,更帶起全JS開發的潮流,也就是大家常常在Youtube上看到的MEAN Stack(Mongodb,Express,Angular,Node.js),也是以Node.js為中心發展起來的。

然而,Node.js的作者Ryan Dahl卻在2018年六月JsConf EU之上,發表了一個題目為10個Node.js的設計錯誤的短講。出乎意料的是: 原來Ryan2012年後已離開Node.js開發工作,原因是他的興趣主要在伺服器編程(Server Programming)之上,所以轉投了Golang的懷抱,所以已一段長時間沒有用Node.js作開發工作。而更重要的是,他2012年離開時,認為Node.js作為一個後端運行JavaScript程式,已是大功告成。他萬萬沒有想過Node.js會變成今天般無所不在(ubiquitous),因此很多設計上沒有周詳考慮,現在已經是Too late to change, Too big to fail

Ryan Dahl

螢幕截圖自影片10 things I regret about Node.js - Ryan Dahl

綜觀全片,有趣的是Ryan Dahl其實只講了七個問題。提出問題,而不提出答案沒有多大意義,Ryan亦提出了他心目中的解答,也就是本文要介紹的deno

Deno是一個能夠原生運行JavaScriptTypeScript的程式庫,名字由來是將Node的字母重新排列而得來(no-dede-no),標誌是一隻可愛的恐龍,原因也許是因為deno跟恐龍英文dinosaur聽起來很像?筆者也不太清楚。

deno logo

Ryan在2018年時就開始了Deno的開發,事隔兩年,在2020年5月,正好推出了1.0,標誌著Deno技術開始成熟,可以挑戰Node.js了! 現在我們就在以下七個方面,將Node.jsdeno大比拼一番!

Node.js官方API 使用Callback VS Deno原生使用Promise

Node.js開發者都知道著名的Callback Hell(回調地獄),因為Node.js的官方API全是以Callback寫成,即使是讀取檔案如此簡單之事,都需要用到Callback Function(回調函數)。

const fs = require('fs');

fs.readFile('./my-file.txt', function (err, data) {
//<-- 這個就是回調函數
//數據在回調函數之內才能使用
if (err) {
console.log(err);
return;
}
console.log(data.toString());
});

相反,如果使用新的PromiseAPI,程式碼就會大大簡化:

const fs = require('fs');

async function main() {
const data = await fs.readFile('./my-file.txt');
console.log(data.toString());
}

Node.js開發初期早就支援Promise,但在2010年作者覺得不太有用,又移走了Promise,結果兜兜轉轉到了版本0.11才重新加入Promise,浪費了不少開發者的青春在回調地獄之上..... 而由於一開始的官方API 就是使用Callback,因此為了向下兼容(Backward Compatibility),因此即使到了版本14,官方API依然是Callback為主,Promise為輔,變相任何初學者都要同時學習CallbackPromise,變相學習量加倍。

Deno呢?當然不會重蹈覆轍,一開始就完整支援PromiseAsync/Await

const fileContent = await Deno.readTextFile('./my-file.txt');
console.log(fileContent);

Deno不僅支援PromiseAsync/Await,也完整支援TypeScript,連帶Top Level await這樣的新功能也一併支援了。 簡潔程度直迫其他Dynamic Languages(動態語言)啊。

要將字串寫入檔案也很簡單。也是一句完成。

await Deno.writeTextFile('./my-file.txt', 'Hello World!', { append: true });

Node.js無掩雞籠 VS Deno安全沙盒

非香港讀者注:無掩雞籠乃粵語俗語,意指自出自入,無任何保安可言

Node.js的安全性(Security)很有問題嗎?其實當你運行一個Node.js檔案時,如以下這句command,本身已是危機四伏。

node index.js

index.js擁有你現有使用者(Current User)的所有權限(All Permissions)。假如這個js檔是由黑客所寫之病毒檔案,每當你運行時,就會將你所有個人檔案加密(Encryption),然後再向你勒索bitcoin才會解密(典型CryptoLocker旳攻擊方法)。除非你逐行細心閱讀該檔案內容,否則這樣的攻擊是無法避免的。此問題不只是Node.js專有,不論是PythonPHPC#Java,這個問題一樣存在。所以,傳統程式語言其實是「無掩雞籠」的!

Deno則不然,由於Node.jsDeno都是基於V8引擎所開發的,而V8又是Google Chrome的核心套件,瀏覽器的安全性要求非常高,這樣的安全問題,在瀏覽器世界其實早已解決,只是作者在之前編寫Node.js時,沒有加入這些防禦措施。

上面readFile的例子,如果你用deno直接運行,寫法是deno run read-file.ts

$ deno run read-file.ts
Check file:///your/path/to/deno-test.ts
error: Uncaught PermissionDenied: read access to "./my-file.txt", run again with the --allow-read flag
at unwrapResponse (rt/10_dispatch_json.js:25:13)
at sendAsync (rt/10_dispatch_json.js:76:12)
at async open (rt/30_files.js:52:17)
at async Object.readTextFile (rt/40_read_file.js:30:18)
at async file:///your/path/to/deno-test.ts:5:21

出現了PermissionDenied錯誤,因為deno預設是沒有讀取檔案的權限。因此deno run read-file.ts無法讀取檔案。 加上-allow-read的選項,才是正確用法:

deno run --allow-read read-file.ts

同理,要寫入檔案,我們就需要選項--allow-write,否則就會得到以下錯誤。

error: Uncaught PermissionDenied: write access to "./my-file.txt", run again with the --allow-write flag

為何要開發者親自輸入呢?因為Deno建立了一個沙盒(Sandbox),將程式在其中運行:沙盒內之程式碼除非獲得授權,否則無法與電腦其他任何資料、任何硬件互動。因此保證了運行deno run deno-test.ts這個動作百分百分安全。Deno除了--allow-read--allow-write控制檔案讀寫之外,還有其他不同權限控制: 包括--allow-net控制網絡存取、--allow-run控制子進程存取等等。以沙盒保證程式運行的安全性,在芸芸程式語言中是首創,將運行程式的安全性放在第一位。

Node.js組建系統GYP VS Deno FFI(開發中)

如果大家安裝過一些Node.js的著名套件,例如TensorflowBcrypt的話。都一定會見過以下這個惱人的錯誤。

GYP Error

GYP全名是Generate Your Project,用作生成其他組建系統的元組建系統(Meta-build system for build system),最主要用途在生成一些不是原生JavaScript的程式庫時用到,由於TensorflowBcrypt都會用到native code,因此這個煩人的問題,就代表你編譯native code時出現問題!又不知要耗費多少除錯的時間了...

那為何Node.js最終會使用了GYP呢?其實是由於原本V8引擎一開始是使用GYP的,所以Node.js亦仿隨。但後來V8用了另一個元組建系統GNNode.js就陰差陽錯下成了唯一使用GYP的主流程式庫。而GYP的格式很古怪,你說是JSON,但其實又有Python的語法,就像兩者的合體。

{
'target_name': 'hello',
'sources': [
'kitty.cc',
],
'include_dirs': [
'shared_stuff/public', # Merged, list item prepended due to include_dirs+
'headers',
],
'link_settings': {
'libraries': [
'-lm',
'-lshared_stuff', # Merged, list item appended
],
'library_dirs': [
'/usr/lib',
],
},
'test': 1, # Merged, int value replaced
}

GYP這個怪胎,就導致了許多編譯(Compilation)上的問題,燃燒了多少的青春... 筆者在開發時,也會盡量避免需要gyp的套件,因為通常問題多多。

Deno則採取更「正常」的做法,也就是使用FFI(Foreign Function interface),方便編程者直接將其他語言的程式與Deno一齊使用。不過這個功能正在開發之中,所以大家請拭目以待。

稍息一下

篇幅所限,這次就先理解這三點,下篇我們會繼續漫談Deno這個後起之秀!