Node.js終結者?青出於藍的Deno(一)
有編程經驗的人,都一定會聽聞過Node.js,Node.js基于Chrome的V8引擎開發,本身能夠運行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。

螢幕截圖自影片10 things I regret about Node.js - Ryan Dahl
綜觀全片,有趣的是Ryan Dahl其實只講了七個問題。提出問題,而不提出答案沒有多大意義,Ryan亦提出了他心目中的解答,也就是本文要介紹的deno。
Deno是一個能夠原生運行JavaScript及TypeScript的程式庫,名字由來是將Node的字母重新排列而得來(no-de到de-no),標誌是一隻可愛的恐龍,原因也許是因為deno跟恐龍英文dinosaur聽起來很像?筆者也不太清楚。

Ryan在2018年時就開始了Deno的開發,事隔兩年,在2020年5月,正好推出了1.0,標誌著Deno技術開始成熟,可以挑戰Node.js了!
現在我們就在以下七個方面,將Node.js及deno大比拼一番!
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為輔,變相任何初學者都要同時學習Callback及Promise,變相學習量加倍。
Deno呢?當然不會重蹈覆轍,一開始就完整支援Promise與Async/Await。
const fileContent = await Deno.readTextFile('./my-file.txt');
console.log(fileContent);
Deno不僅支援Promise及Async/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專有,不論是Python、PHP、C#、Java,這個問題一樣存在。所以,傳統程式語言其實是「無掩雞籠」的!
Deno則不然,由於Node.js及Deno都是基於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的著名套件,例如Tensorflow、Bcrypt的話。都一定會見過以下這個惱人的錯誤。

GYP全名是Generate Your Project,用作生成其他組建系統的元組建系統(Meta-build system for build system),最主要用途在生成一些不是原生JavaScript的程式庫時用到,由於Tensorflow、Bcrypt都會用到native code,因此這個煩人的問題,就代表你編譯native code時出現問題!又不知要耗費多少除錯的時間了...
那為何Node.js最終會使用了GYP呢?其實是由於原本V8引擎一開始是使用GYP的,所以Node.js亦仿隨。但後來V8用了另一個元組建系統GN,Node.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這個後起之秀!
